@socialneuron/mcp-server 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/http.js +2558 -160
  3. package/dist/index.js +2851 -175
  4. package/package.json +2 -2
package/dist/http.js CHANGED
@@ -304,7 +304,7 @@ __export(api_keys_exports, {
304
304
  async function validateApiKey(apiKey) {
305
305
  const supabaseUrl = getSupabaseUrl();
306
306
  try {
307
- const anonKey = process.env.SUPABASE_ANON_KEY || process.env.SOCIALNEURON_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY || CLOUD_SUPABASE_ANON_KEY;
307
+ const anonKey = getCloudAnonKey();
308
308
  const response = await fetch(
309
309
  `${supabaseUrl}/functions/v1/mcp-auth?action=validate-key-public`,
310
310
  {
@@ -339,13 +339,12 @@ var init_api_keys = __esm({
339
339
  // src/lib/supabase.ts
340
340
  var supabase_exports = {};
341
341
  __export(supabase_exports, {
342
- CLOUD_SUPABASE_ANON_KEY: () => CLOUD_SUPABASE_ANON_KEY,
343
- CLOUD_SUPABASE_URL: () => CLOUD_SUPABASE_URL,
342
+ fetchCloudConfig: () => fetchCloudConfig,
344
343
  getAuthMode: () => getAuthMode,
345
344
  getAuthenticatedApiKey: () => getAuthenticatedApiKey,
346
- getAuthenticatedEmail: () => getAuthenticatedEmail,
347
345
  getAuthenticatedExpiresAt: () => getAuthenticatedExpiresAt,
348
346
  getAuthenticatedScopes: () => getAuthenticatedScopes,
347
+ getCloudAnonKey: () => getCloudAnonKey,
349
348
  getDefaultProjectId: () => getDefaultProjectId,
350
349
  getDefaultUserId: () => getDefaultUserId,
351
350
  getMcpRunId: () => getMcpRunId,
@@ -370,11 +369,47 @@ function getSupabaseClient() {
370
369
  }
371
370
  return client2;
372
371
  }
372
+ async function fetchCloudConfig() {
373
+ if (_cloudConfig) return _cloudConfig;
374
+ const envUrl = process.env.SOCIALNEURON_CLOUD_SUPABASE_URL || process.env.SUPABASE_URL;
375
+ const envAnon = process.env.SUPABASE_ANON_KEY || process.env.SOCIALNEURON_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY;
376
+ if (envUrl && envAnon) {
377
+ _cloudConfig = { supabaseUrl: envUrl, anonKey: envAnon };
378
+ return _cloudConfig;
379
+ }
380
+ try {
381
+ const resp = await fetch(CLOUD_CONFIG_URL, {
382
+ signal: AbortSignal.timeout(5e3)
383
+ });
384
+ if (!resp.ok) {
385
+ throw new Error(`Config fetch failed: ${resp.status}`);
386
+ }
387
+ const config = await resp.json();
388
+ _cloudConfig = config;
389
+ return _cloudConfig;
390
+ } catch (err) {
391
+ const msg = err instanceof Error ? err.message : String(err);
392
+ throw new Error(
393
+ `Failed to fetch cloud config from ${CLOUD_CONFIG_URL}: ${msg}. Set SUPABASE_URL and SUPABASE_ANON_KEY environment variables as a fallback.`
394
+ );
395
+ }
396
+ }
373
397
  function getSupabaseUrl() {
374
398
  if (SUPABASE_URL) return SUPABASE_URL;
375
399
  const cloudOverride = process.env.SOCIALNEURON_CLOUD_SUPABASE_URL;
376
400
  if (cloudOverride) return cloudOverride;
377
- return CLOUD_SUPABASE_URL;
401
+ if (_cloudConfig) return _cloudConfig.supabaseUrl;
402
+ throw new Error(
403
+ "Supabase URL not configured. Run: npx @socialneuron/mcp-server setup"
404
+ );
405
+ }
406
+ function getCloudAnonKey() {
407
+ const envAnon = process.env.SUPABASE_ANON_KEY || process.env.SOCIALNEURON_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY;
408
+ if (envAnon) return envAnon;
409
+ if (_cloudConfig) return _cloudConfig.anonKey;
410
+ throw new Error(
411
+ "Supabase anon key not available. Call fetchCloudConfig() first or set SUPABASE_ANON_KEY."
412
+ );
378
413
  }
379
414
  function getServiceKey() {
380
415
  if (!SUPABASE_SERVICE_KEY) {
@@ -421,6 +456,12 @@ async function getDefaultProjectId() {
421
456
  return null;
422
457
  }
423
458
  async function initializeAuth() {
459
+ if (!SUPABASE_URL) {
460
+ try {
461
+ await fetchCloudConfig();
462
+ } catch {
463
+ }
464
+ }
424
465
  const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
425
466
  const apiKey = await loadApiKey2();
426
467
  if (apiKey) {
@@ -431,7 +472,6 @@ async function initializeAuth() {
431
472
  _authMode = "api-key";
432
473
  authenticatedUserId = result.userId;
433
474
  authenticatedScopes = result.scopes && result.scopes.length > 0 ? result.scopes : ["mcp:read"];
434
- authenticatedEmail = result.email || null;
435
475
  authenticatedExpiresAt = result.expiresAt || null;
436
476
  console.error(
437
477
  "[MCP] Authenticated via API key (prefix: " + apiKey.substring(0, 6) + "..." + apiKey.slice(-4) + ")"
@@ -487,9 +527,6 @@ function getMcpRunId() {
487
527
  function getAuthenticatedScopes() {
488
528
  return authenticatedScopes;
489
529
  }
490
- function getAuthenticatedEmail() {
491
- return authenticatedEmail;
492
- }
493
530
  function getAuthenticatedExpiresAt() {
494
531
  return authenticatedExpiresAt;
495
532
  }
@@ -528,7 +565,7 @@ async function logMcpToolInvocation(args) {
528
565
  captureToolEvent(args).catch(() => {
529
566
  });
530
567
  }
531
- var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY, projectIdCache;
568
+ var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedExpiresAt, authenticatedApiKey, MCP_RUN_ID, CLOUD_CONFIG_URL, _cloudConfig, projectIdCache;
532
569
  var init_supabase = __esm({
533
570
  "src/lib/supabase.ts"() {
534
571
  "use strict";
@@ -540,19 +577,18 @@ var init_supabase = __esm({
540
577
  _authMode = "service-role";
541
578
  authenticatedUserId = null;
542
579
  authenticatedScopes = [];
543
- authenticatedEmail = null;
544
580
  authenticatedExpiresAt = null;
545
581
  authenticatedApiKey = null;
546
582
  MCP_RUN_ID = randomUUID();
547
- CLOUD_SUPABASE_URL = "https://rhukkjscgzauutioyeei.supabase.co";
548
- CLOUD_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJodWtranNjZ3phdXV0aW95ZWVpIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ4NjM4ODYsImV4cCI6MjA4MDQzOTg4Nn0.JVtrviGvN0HaSh0JFS5KNl5FAB5ffG5Y1IMZsQFUrNQ";
583
+ CLOUD_CONFIG_URL = process.env.SOCIALNEURON_CONFIG_URL || "https://mcp.socialneuron.com/config";
584
+ _cloudConfig = null;
549
585
  projectIdCache = /* @__PURE__ */ new Map();
550
586
  }
551
587
  });
552
588
 
553
589
  // src/http.ts
554
590
  import express from "express";
555
- import { randomUUID as randomUUID3 } from "node:crypto";
591
+ import { randomUUID as randomUUID4 } from "node:crypto";
556
592
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
557
593
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
558
594
 
@@ -582,6 +618,9 @@ var TOOL_SCOPES = {
582
618
  get_best_posting_times: "mcp:read",
583
619
  extract_brand: "mcp:read",
584
620
  get_brand_profile: "mcp:read",
621
+ get_brand_runtime: "mcp:read",
622
+ explain_brand_system: "mcp:read",
623
+ check_brand_consistency: "mcp:read",
585
624
  get_ideation_context: "mcp:read",
586
625
  get_credit_balance: "mcp:read",
587
626
  get_budget_status: "mcp:read",
@@ -597,6 +636,7 @@ var TOOL_SCOPES = {
597
636
  generate_image: "mcp:write",
598
637
  check_status: "mcp:read",
599
638
  render_demo_video: "mcp:write",
639
+ render_template_video: "mcp:write",
600
640
  save_brand_profile: "mcp:write",
601
641
  update_platform_voice: "mcp:write",
602
642
  create_storyboard: "mcp:write",
@@ -635,7 +675,19 @@ var TOOL_SCOPES = {
635
675
  // mcp:read (usage is read-only)
636
676
  get_mcp_usage: "mcp:read",
637
677
  list_plan_approvals: "mcp:read",
638
- search_tools: "mcp:read"
678
+ search_tools: "mcp:read",
679
+ // mcp:read (pipeline readiness + status are read-only)
680
+ check_pipeline_readiness: "mcp:read",
681
+ get_pipeline_status: "mcp:read",
682
+ // mcp:autopilot (pipeline orchestration + approval automation)
683
+ run_content_pipeline: "mcp:autopilot",
684
+ auto_approve_plan: "mcp:autopilot",
685
+ create_autopilot_config: "mcp:autopilot",
686
+ // mcp:read (suggestions are read-only, no credit cost)
687
+ suggest_next_content: "mcp:read",
688
+ // mcp:analytics (digest and anomalies are analytics-scoped)
689
+ generate_performance_digest: "mcp:analytics",
690
+ detect_anomalies: "mcp:analytics"
639
691
  };
640
692
  function hasScope(userScopes, required) {
641
693
  if (userScopes.includes(required)) return true;
@@ -646,6 +698,147 @@ function hasScope(userScopes, required) {
646
698
  return false;
647
699
  }
648
700
 
701
+ // src/lib/tool-annotations.ts
702
+ var ACRONYMS = {
703
+ youtube: "YouTube",
704
+ tiktok: "TikTok",
705
+ mcp: "MCP",
706
+ url: "URL",
707
+ ai: "AI",
708
+ api: "API",
709
+ dm: "DM",
710
+ id: "ID"
711
+ };
712
+ function toTitle(name) {
713
+ return name.split("_").map((w) => ACRONYMS[w] ?? w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
714
+ }
715
+ var SCOPE_DEFAULTS = {
716
+ "mcp:read": {
717
+ readOnlyHint: true,
718
+ destructiveHint: false,
719
+ idempotentHint: true,
720
+ openWorldHint: false
721
+ },
722
+ "mcp:write": {
723
+ readOnlyHint: false,
724
+ destructiveHint: true,
725
+ idempotentHint: false,
726
+ openWorldHint: false
727
+ },
728
+ "mcp:distribute": {
729
+ readOnlyHint: false,
730
+ destructiveHint: true,
731
+ idempotentHint: false,
732
+ openWorldHint: true
733
+ },
734
+ "mcp:analytics": {
735
+ readOnlyHint: true,
736
+ destructiveHint: false,
737
+ idempotentHint: true,
738
+ openWorldHint: false
739
+ },
740
+ "mcp:comments": {
741
+ readOnlyHint: false,
742
+ destructiveHint: true,
743
+ idempotentHint: false,
744
+ openWorldHint: true
745
+ },
746
+ "mcp:autopilot": {
747
+ readOnlyHint: false,
748
+ destructiveHint: true,
749
+ idempotentHint: false,
750
+ openWorldHint: false
751
+ }
752
+ };
753
+ var OVERRIDES = {
754
+ // Destructive tools
755
+ delete_comment: { destructiveHint: true },
756
+ moderate_comment: { destructiveHint: true },
757
+ // Read-only tools in non-read scopes (must also clear destructiveHint from scope default)
758
+ list_comments: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
759
+ list_autopilot_configs: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
760
+ get_autopilot_status: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
761
+ check_status: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
762
+ get_content_plan: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
763
+ list_plan_approvals: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
764
+ // Analytics tool that triggers side effects (data refresh)
765
+ refresh_platform_analytics: { readOnlyHint: false, idempotentHint: true },
766
+ // Write tools that are idempotent
767
+ save_brand_profile: { idempotentHint: true },
768
+ update_platform_voice: { idempotentHint: true },
769
+ update_autopilot_config: { idempotentHint: true },
770
+ update_content_plan: { idempotentHint: true },
771
+ respond_plan_approval: { idempotentHint: true },
772
+ // Distribution is open-world (publishes to external platforms)
773
+ schedule_post: { openWorldHint: true },
774
+ schedule_content_plan: { openWorldHint: true },
775
+ // Extraction reads external URLs
776
+ extract_url_content: { openWorldHint: true },
777
+ extract_brand: { openWorldHint: true },
778
+ // Pipeline: read-only tools
779
+ check_pipeline_readiness: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
780
+ get_pipeline_status: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
781
+ // Pipeline: orchestration tools (non-idempotent, may schedule externally)
782
+ run_content_pipeline: { openWorldHint: true },
783
+ auto_approve_plan: { idempotentHint: true },
784
+ // Suggest: read-only
785
+ suggest_next_content: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
786
+ // Digest/Anomalies: read-only analytics
787
+ generate_performance_digest: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
788
+ detect_anomalies: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
789
+ };
790
+ function buildAnnotationsMap() {
791
+ const map = /* @__PURE__ */ new Map();
792
+ for (const [toolName, scope] of Object.entries(TOOL_SCOPES)) {
793
+ const defaults = SCOPE_DEFAULTS[scope];
794
+ if (!defaults) {
795
+ map.set(toolName, {
796
+ title: toTitle(toolName),
797
+ readOnlyHint: false,
798
+ destructiveHint: true,
799
+ idempotentHint: false,
800
+ openWorldHint: true
801
+ });
802
+ continue;
803
+ }
804
+ const overrides = OVERRIDES[toolName] ?? {};
805
+ map.set(toolName, {
806
+ title: toTitle(toolName),
807
+ readOnlyHint: overrides.readOnlyHint ?? defaults.readOnlyHint,
808
+ destructiveHint: overrides.destructiveHint ?? defaults.destructiveHint,
809
+ idempotentHint: overrides.idempotentHint ?? defaults.idempotentHint,
810
+ openWorldHint: overrides.openWorldHint ?? defaults.openWorldHint
811
+ });
812
+ }
813
+ return map;
814
+ }
815
+ function applyAnnotations(server) {
816
+ const annotations = buildAnnotationsMap();
817
+ const registeredTools = server._registeredTools;
818
+ if (!registeredTools || typeof registeredTools !== "object") {
819
+ console.warn("[annotations] Could not access _registeredTools \u2014 annotations not applied");
820
+ return;
821
+ }
822
+ const entries = Object.entries(registeredTools);
823
+ let applied = 0;
824
+ for (const [toolName, tool] of entries) {
825
+ const ann = annotations.get(toolName);
826
+ if (ann && typeof tool.update === "function") {
827
+ tool.update({
828
+ annotations: {
829
+ title: ann.title,
830
+ readOnlyHint: ann.readOnlyHint,
831
+ destructiveHint: ann.destructiveHint,
832
+ idempotentHint: ann.idempotentHint,
833
+ openWorldHint: ann.openWorldHint
834
+ }
835
+ });
836
+ applied++;
837
+ }
838
+ }
839
+ console.log(`[annotations] Applied annotations to ${applied}/${entries.length} tools`);
840
+ }
841
+
649
842
  // src/tools/ideation.ts
650
843
  import { z } from "zod";
651
844
 
@@ -787,6 +980,8 @@ async function callEdgeFunction(functionName, body, options) {
787
980
  var CATEGORY_CONFIGS = {
788
981
  posting: { maxTokens: 30, refillRate: 30 / 60 },
789
982
  // 30 req/min
983
+ generation: { maxTokens: 20, refillRate: 20 / 60 },
984
+ // 20 req/min — AI content generation (mcp:write)
790
985
  screenshot: { maxTokens: 10, refillRate: 10 / 60 },
791
986
  // 10 req/min
792
987
  read: { maxTokens: 60, refillRate: 60 / 60 }
@@ -1363,12 +1558,24 @@ function sanitizeDbError(error) {
1363
1558
  }
1364
1559
  return "Database operation failed. Please try again.";
1365
1560
  }
1561
+ function sanitizeError(error) {
1562
+ const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
1563
+ if (process.env.NODE_ENV !== "production") {
1564
+ console.error("[Error]", msg);
1565
+ }
1566
+ for (const [pattern, userMessage] of ERROR_PATTERNS) {
1567
+ if (pattern.test(msg)) {
1568
+ return userMessage;
1569
+ }
1570
+ }
1571
+ return "An unexpected error occurred. Please try again.";
1572
+ }
1366
1573
 
1367
1574
  // src/tools/content.ts
1368
1575
  init_request_context();
1369
1576
 
1370
1577
  // src/lib/version.ts
1371
- var MCP_VERSION = "1.6.0";
1578
+ var MCP_VERSION = "1.7.0";
1372
1579
 
1373
1580
  // src/tools/content.ts
1374
1581
  var MAX_CREDITS_PER_RUN = Math.max(
@@ -5439,6 +5646,22 @@ var COMPOSITIONS = [
5439
5646
  durationInFrames: 450,
5440
5647
  fps: 30,
5441
5648
  description: "Product ad - 15s ultra-short"
5649
+ },
5650
+ {
5651
+ id: "DataVizDashboard",
5652
+ width: 1080,
5653
+ height: 1920,
5654
+ durationInFrames: 450,
5655
+ fps: 30,
5656
+ description: "Animated data dashboard - KPIs, bar chart, donut chart, line chart (15s, 9:16)"
5657
+ },
5658
+ {
5659
+ id: "ReviewsTestimonial",
5660
+ width: 1080,
5661
+ height: 1920,
5662
+ durationInFrames: 600,
5663
+ fps: 30,
5664
+ description: "Customer review testimonial with star animations and review carousel (dynamic duration, 9:16)"
5442
5665
  }
5443
5666
  ];
5444
5667
  function registerRemotionTools(server) {
@@ -5446,13 +5669,6 @@ function registerRemotionTools(server) {
5446
5669
  "list_compositions",
5447
5670
  "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.",
5448
5671
  {},
5449
- {
5450
- title: "List Compositions",
5451
- readOnlyHint: true,
5452
- destructiveHint: false,
5453
- idempotentHint: true,
5454
- openWorldHint: false
5455
- },
5456
5672
  async () => {
5457
5673
  const lines = [`${COMPOSITIONS.length} Remotion compositions available:`, ""];
5458
5674
  for (const comp of COMPOSITIONS) {
@@ -5481,13 +5697,6 @@ function registerRemotionTools(server) {
5481
5697
  "JSON string of input props to pass to the composition. Each composition accepts different props. Omit for defaults."
5482
5698
  )
5483
5699
  },
5484
- {
5485
- title: "Render Demo Video",
5486
- readOnlyHint: false,
5487
- destructiveHint: false,
5488
- idempotentHint: false,
5489
- openWorldHint: false
5490
- },
5491
5700
  async ({ composition_id, output_format, props }) => {
5492
5701
  const startedAt = Date.now();
5493
5702
  const userId = await getDefaultUserId();
@@ -5619,6 +5828,134 @@ function registerRemotionTools(server) {
5619
5828
  }
5620
5829
  }
5621
5830
  );
5831
+ server.tool(
5832
+ "render_template_video",
5833
+ "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.",
5834
+ {
5835
+ composition_id: z7.string().describe(
5836
+ 'Remotion composition ID. Examples: "DataVizDashboard", "ReviewsTestimonial", "CaptionedClip". Use list_compositions to see all available IDs.'
5837
+ ),
5838
+ input_props: z7.string().describe(
5839
+ "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}."
5840
+ ),
5841
+ aspect_ratio: z7.enum(["9:16", "1:1", "16:9"]).optional().describe('Output aspect ratio. Defaults to "9:16" (vertical).')
5842
+ },
5843
+ async ({ composition_id, input_props, aspect_ratio }) => {
5844
+ const startedAt = Date.now();
5845
+ const userId = await getDefaultUserId();
5846
+ const rateLimit = checkRateLimit("generation", `render_template:${userId}`);
5847
+ if (!rateLimit.allowed) {
5848
+ await logMcpToolInvocation({
5849
+ toolName: "render_template_video",
5850
+ status: "rate_limited",
5851
+ durationMs: Date.now() - startedAt,
5852
+ details: { retryAfter: rateLimit.retryAfter }
5853
+ });
5854
+ return {
5855
+ content: [
5856
+ {
5857
+ type: "text",
5858
+ text: `Rate limit exceeded. Retry in ~${rateLimit.retryAfter}s.`
5859
+ }
5860
+ ],
5861
+ isError: true
5862
+ };
5863
+ }
5864
+ const comp = COMPOSITIONS.find((c) => c.id === composition_id);
5865
+ if (!comp) {
5866
+ await logMcpToolInvocation({
5867
+ toolName: "render_template_video",
5868
+ status: "error",
5869
+ durationMs: Date.now() - startedAt,
5870
+ details: { error: "Unknown composition", compositionId: composition_id }
5871
+ });
5872
+ return {
5873
+ content: [
5874
+ {
5875
+ type: "text",
5876
+ text: `Unknown composition "${composition_id}". Available: ${COMPOSITIONS.map((c) => c.id).join(", ")}`
5877
+ }
5878
+ ],
5879
+ isError: true
5880
+ };
5881
+ }
5882
+ let inputProps;
5883
+ try {
5884
+ inputProps = JSON.parse(input_props);
5885
+ } catch {
5886
+ await logMcpToolInvocation({
5887
+ toolName: "render_template_video",
5888
+ status: "error",
5889
+ durationMs: Date.now() - startedAt,
5890
+ details: { error: "Invalid input_props JSON" }
5891
+ });
5892
+ return {
5893
+ content: [{ type: "text", text: `Invalid JSON in input_props.` }],
5894
+ isError: true
5895
+ };
5896
+ }
5897
+ try {
5898
+ const { data, error } = await callEdgeFunction("create-remotion-job", {
5899
+ compositionId: composition_id,
5900
+ inputProps,
5901
+ outputs: [
5902
+ {
5903
+ aspectRatio: aspect_ratio || "9:16",
5904
+ resolution: "1080p",
5905
+ codec: "h264"
5906
+ }
5907
+ ]
5908
+ });
5909
+ if (error || !data?.success) {
5910
+ throw new Error(error || data?.error || "Failed to create render job");
5911
+ }
5912
+ await logMcpToolInvocation({
5913
+ toolName: "render_template_video",
5914
+ status: "success",
5915
+ durationMs: Date.now() - startedAt,
5916
+ details: {
5917
+ compositionId: composition_id,
5918
+ jobId: data.jobId,
5919
+ creditsCharged: data.creditsCharged
5920
+ }
5921
+ });
5922
+ return {
5923
+ content: [
5924
+ {
5925
+ type: "text",
5926
+ text: [
5927
+ `Render job created successfully.`,
5928
+ ` Composition: ${composition_id}`,
5929
+ ` Job ID: ${data.jobId}`,
5930
+ ` Credits charged: ${data.creditsCharged}`,
5931
+ ` Estimated duration: ${data.estimatedDurationSeconds}s`,
5932
+ ` Content ID: ${data.contentHistoryId}`,
5933
+ ``,
5934
+ `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.`
5935
+ ].join("\n")
5936
+ }
5937
+ ]
5938
+ };
5939
+ } catch (err) {
5940
+ const message = err instanceof Error ? err.message : String(err);
5941
+ await logMcpToolInvocation({
5942
+ toolName: "render_template_video",
5943
+ status: "error",
5944
+ durationMs: Date.now() - startedAt,
5945
+ details: { error: message, compositionId: composition_id }
5946
+ });
5947
+ return {
5948
+ content: [
5949
+ {
5950
+ type: "text",
5951
+ text: `Failed to create render job: ${message}`
5952
+ }
5953
+ ],
5954
+ isError: true
5955
+ };
5956
+ }
5957
+ }
5958
+ );
5622
5959
  }
5623
5960
 
5624
5961
  // src/tools/insights.ts
@@ -7027,7 +7364,6 @@ ${"=".repeat(40)}
7027
7364
  }
7028
7365
 
7029
7366
  // src/tools/autopilot.ts
7030
- init_supabase();
7031
7367
  import { z as z15 } from "zod";
7032
7368
  function asEnvelope11(data) {
7033
7369
  return {
@@ -7046,46 +7382,35 @@ function registerAutopilotTools(server) {
7046
7382
  active_only: z15.boolean().optional().describe("If true, only return active configs. Defaults to false (show all)."),
7047
7383
  response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7048
7384
  },
7049
- {
7050
- title: "List Autopilot Configs",
7051
- readOnlyHint: true,
7052
- destructiveHint: false,
7053
- idempotentHint: true,
7054
- openWorldHint: false
7055
- },
7056
7385
  async ({ active_only, response_format }) => {
7057
7386
  const format = response_format ?? "text";
7058
- const supabase = getSupabaseClient();
7059
- const userId = await getDefaultUserId();
7060
- let query = supabase.from("autopilot_configs").select(
7061
- "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"
7062
- ).eq("user_id", userId).order("created_at", { ascending: false });
7063
- if (active_only) {
7064
- query = query.eq("is_active", true);
7065
- }
7066
- const { data: configs, error } = await query;
7067
- if (error) {
7387
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
7388
+ action: "list-autopilot-configs",
7389
+ active_only: active_only ?? false
7390
+ });
7391
+ if (efError) {
7068
7392
  return {
7069
7393
  content: [
7070
7394
  {
7071
7395
  type: "text",
7072
- text: `Error fetching autopilot configs: ${sanitizeDbError(error)}`
7396
+ text: `Error fetching autopilot configs: ${efError}`
7073
7397
  }
7074
7398
  ],
7075
7399
  isError: true
7076
7400
  };
7077
7401
  }
7402
+ const configs = result?.configs ?? [];
7078
7403
  if (format === "json") {
7079
7404
  return {
7080
7405
  content: [
7081
7406
  {
7082
7407
  type: "text",
7083
- text: JSON.stringify(asEnvelope11(configs || []), null, 2)
7408
+ text: JSON.stringify(asEnvelope11(configs), null, 2)
7084
7409
  }
7085
7410
  ]
7086
7411
  };
7087
7412
  }
7088
- if (!configs || configs.length === 0) {
7413
+ if (configs.length === 0) {
7089
7414
  return {
7090
7415
  content: [
7091
7416
  {
@@ -7130,18 +7455,11 @@ ${"=".repeat(40)}
7130
7455
  {
7131
7456
  config_id: z15.string().uuid().describe("The autopilot config ID to update."),
7132
7457
  is_active: z15.boolean().optional().describe("Enable or disable this autopilot config."),
7133
- 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"]).'),
7458
+ 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"]).'),
7134
7459
  schedule_time: z15.string().optional().describe('Time to run in HH:MM format (24h, user timezone). E.g., "09:00".'),
7135
7460
  max_credits_per_run: z15.number().optional().describe("Maximum credits per execution."),
7136
7461
  max_credits_per_week: z15.number().optional().describe("Maximum credits per week.")
7137
7462
  },
7138
- {
7139
- title: "Update Autopilot Config",
7140
- readOnlyHint: false,
7141
- destructiveHint: false,
7142
- idempotentHint: true,
7143
- openWorldHint: false
7144
- },
7145
7463
  async ({
7146
7464
  config_id,
7147
7465
  is_active,
@@ -7150,22 +7468,7 @@ ${"=".repeat(40)}
7150
7468
  max_credits_per_run,
7151
7469
  max_credits_per_week
7152
7470
  }) => {
7153
- const supabase = getSupabaseClient();
7154
- const userId = await getDefaultUserId();
7155
- const updates = {};
7156
- if (is_active !== void 0) updates.is_active = is_active;
7157
- if (max_credits_per_run !== void 0) updates.max_credits_per_run = max_credits_per_run;
7158
- if (max_credits_per_week !== void 0) updates.max_credits_per_week = max_credits_per_week;
7159
- if (schedule_days || schedule_time) {
7160
- const { data: existing } = await supabase.from("autopilot_configs").select("schedule_config").eq("id", config_id).eq("user_id", userId).single();
7161
- const existingSchedule = existing?.schedule_config || {};
7162
- updates.schedule_config = {
7163
- ...existingSchedule,
7164
- ...schedule_days ? { days: schedule_days } : {},
7165
- ...schedule_time ? { time: schedule_time } : {}
7166
- };
7167
- }
7168
- if (Object.keys(updates).length === 0) {
7471
+ if (is_active === void 0 && !schedule_days && !schedule_time && max_credits_per_run === void 0 && max_credits_per_week === void 0) {
7169
7472
  return {
7170
7473
  content: [
7171
7474
  {
@@ -7175,18 +7478,37 @@ ${"=".repeat(40)}
7175
7478
  ]
7176
7479
  };
7177
7480
  }
7178
- 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();
7179
- if (error) {
7481
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
7482
+ action: "update-autopilot-config",
7483
+ config_id,
7484
+ is_active,
7485
+ schedule_days,
7486
+ schedule_time,
7487
+ max_credits_per_run,
7488
+ max_credits_per_week
7489
+ });
7490
+ if (efError) {
7180
7491
  return {
7181
7492
  content: [
7182
7493
  {
7183
7494
  type: "text",
7184
- text: `Error updating config: ${sanitizeDbError(error)}`
7495
+ text: `Error updating config: ${efError}`
7185
7496
  }
7186
7497
  ],
7187
7498
  isError: true
7188
7499
  };
7189
7500
  }
7501
+ const updated = result?.updated;
7502
+ if (!updated) {
7503
+ return {
7504
+ content: [
7505
+ {
7506
+ type: "text",
7507
+ text: result?.message || "No changes applied."
7508
+ }
7509
+ ]
7510
+ };
7511
+ }
7190
7512
  return {
7191
7513
  content: [
7192
7514
  {
@@ -7205,26 +7527,19 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
7205
7527
  {
7206
7528
  response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7207
7529
  },
7208
- {
7209
- title: "Get Autopilot Status",
7210
- readOnlyHint: true,
7211
- destructiveHint: false,
7212
- idempotentHint: true,
7213
- openWorldHint: false
7214
- },
7215
7530
  async ({ response_format }) => {
7216
7531
  const format = response_format ?? "text";
7217
- const supabase = getSupabaseClient();
7218
- const userId = await getDefaultUserId();
7219
- const { data: configs } = await supabase.from("autopilot_configs").select(
7220
- "id, recipe_id, is_active, schedule_config, last_run_at, credits_used_this_week, max_credits_per_week"
7221
- ).eq("user_id", userId).eq("is_active", true);
7222
- 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);
7223
- const { data: approvals } = await supabase.from("approval_queue").select("id, status, created_at").eq("user_id", userId).eq("status", "pending");
7532
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", { action: "autopilot-status" });
7533
+ if (efError) {
7534
+ return {
7535
+ content: [{ type: "text", text: `Error fetching autopilot status: ${efError}` }],
7536
+ isError: true
7537
+ };
7538
+ }
7224
7539
  const statusData = {
7225
- activeConfigs: configs?.length || 0,
7226
- recentRuns: recentRuns || [],
7227
- pendingApprovals: approvals?.length || 0
7540
+ activeConfigs: result?.activeConfigs ?? 0,
7541
+ recentRuns: [],
7542
+ pendingApprovals: result?.pendingApprovals ?? 0
7228
7543
  };
7229
7544
  if (format === "json") {
7230
7545
  return {
@@ -7245,22 +7560,93 @@ ${"=".repeat(40)}
7245
7560
  text += `Pending Approvals: ${statusData.pendingApprovals}
7246
7561
 
7247
7562
  `;
7248
- if (statusData.recentRuns.length > 0) {
7249
- text += `Recent Runs:
7563
+ text += `No recent runs.
7250
7564
  `;
7251
- for (const run of statusData.recentRuns) {
7252
- text += ` ${run.id.substring(0, 8)}... \u2014 ${run.status} (${run.started_at})
7253
- `;
7254
- }
7255
- } else {
7256
- text += `No recent runs.
7257
- `;
7258
- }
7259
7565
  return {
7260
7566
  content: [{ type: "text", text }]
7261
7567
  };
7262
7568
  }
7263
7569
  );
7570
+ server.tool(
7571
+ "create_autopilot_config",
7572
+ "Create a new autopilot configuration for automated content pipeline execution. Defines schedule, credit budgets, and approval mode.",
7573
+ {
7574
+ name: z15.string().min(1).max(100).describe("Name for this autopilot config"),
7575
+ project_id: z15.string().uuid().describe("Project to run autopilot for"),
7576
+ mode: z15.enum(["recipe", "pipeline"]).default("pipeline").describe("Mode: recipe (legacy) or pipeline (new orchestration)"),
7577
+ schedule_days: z15.array(z15.enum(["mon", "tue", "wed", "thu", "fri", "sat", "sun"])).min(1).describe("Days of the week to run"),
7578
+ schedule_time: z15.string().describe('Time to run in HH:MM format (24h). E.g., "09:00"'),
7579
+ timezone: z15.string().optional().describe('Timezone (e.g., "America/New_York"). Defaults to UTC.'),
7580
+ max_credits_per_run: z15.number().min(0).optional().describe("Maximum credits per execution"),
7581
+ max_credits_per_week: z15.number().min(0).optional().describe("Maximum credits per week"),
7582
+ approval_mode: z15.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe("How to handle post approvals"),
7583
+ is_active: z15.boolean().default(true).describe("Whether to activate immediately"),
7584
+ response_format: z15.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
7585
+ },
7586
+ async ({
7587
+ name,
7588
+ project_id,
7589
+ mode,
7590
+ schedule_days,
7591
+ schedule_time,
7592
+ timezone,
7593
+ max_credits_per_run,
7594
+ max_credits_per_week,
7595
+ approval_mode,
7596
+ is_active,
7597
+ response_format
7598
+ }) => {
7599
+ const format = response_format ?? "text";
7600
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
7601
+ action: "create-autopilot-config",
7602
+ name,
7603
+ projectId: project_id,
7604
+ mode,
7605
+ schedule_days,
7606
+ schedule_time,
7607
+ timezone,
7608
+ max_credits_per_run,
7609
+ max_credits_per_week,
7610
+ approval_mode,
7611
+ is_active
7612
+ });
7613
+ if (efError) {
7614
+ return {
7615
+ content: [
7616
+ {
7617
+ type: "text",
7618
+ text: `Error creating autopilot config: ${efError}`
7619
+ }
7620
+ ],
7621
+ isError: true
7622
+ };
7623
+ }
7624
+ const created = result?.created;
7625
+ if (!created) {
7626
+ return {
7627
+ content: [{ type: "text", text: "Failed to create config." }],
7628
+ isError: true
7629
+ };
7630
+ }
7631
+ if (format === "json") {
7632
+ return {
7633
+ content: [{ type: "text", text: JSON.stringify(asEnvelope11(created), null, 2) }]
7634
+ };
7635
+ }
7636
+ return {
7637
+ content: [
7638
+ {
7639
+ type: "text",
7640
+ text: `Autopilot config created: ${created.id}
7641
+ Name: ${name}
7642
+ Mode: ${mode}
7643
+ Schedule: ${schedule_days.join(", ")} @ ${schedule_time}
7644
+ Active: ${is_active}`
7645
+ }
7646
+ ]
7647
+ };
7648
+ }
7649
+ );
7264
7650
  }
7265
7651
 
7266
7652
  // src/tools/extraction.ts
@@ -9003,10 +9389,28 @@ var TOOL_CATALOG = [
9003
9389
  scope: "mcp:read"
9004
9390
  },
9005
9391
  {
9006
- name: "save_brand_profile",
9007
- description: "Save or update brand profile",
9008
- module: "brand",
9009
- scope: "mcp:write"
9392
+ name: "get_brand_runtime",
9393
+ description: "Get the full 4-layer brand runtime (messaging, voice, visual, constraints)",
9394
+ module: "brandRuntime",
9395
+ scope: "mcp:read"
9396
+ },
9397
+ {
9398
+ name: "explain_brand_system",
9399
+ description: "Explain brand completeness, confidence, and recommendations",
9400
+ module: "brandRuntime",
9401
+ scope: "mcp:read"
9402
+ },
9403
+ {
9404
+ name: "check_brand_consistency",
9405
+ description: "Check content text for brand voice/vocabulary/claim consistency",
9406
+ module: "brandRuntime",
9407
+ scope: "mcp:read"
9408
+ },
9409
+ {
9410
+ name: "save_brand_profile",
9411
+ description: "Save or update brand profile",
9412
+ module: "brand",
9413
+ scope: "mcp:write"
9010
9414
  },
9011
9415
  {
9012
9416
  name: "update_platform_voice",
@@ -9040,6 +9444,12 @@ var TOOL_CATALOG = [
9040
9444
  module: "remotion",
9041
9445
  scope: "mcp:read"
9042
9446
  },
9447
+ {
9448
+ name: "render_template_video",
9449
+ description: "Render a template video in the cloud via async job",
9450
+ module: "remotion",
9451
+ scope: "mcp:write"
9452
+ },
9043
9453
  // youtube-analytics
9044
9454
  {
9045
9455
  name: "fetch_youtube_analytics",
@@ -9212,6 +9622,58 @@ var TOOL_CATALOG = [
9212
9622
  description: "Search and discover available MCP tools",
9213
9623
  module: "discovery",
9214
9624
  scope: "mcp:read"
9625
+ },
9626
+ // pipeline
9627
+ {
9628
+ name: "check_pipeline_readiness",
9629
+ description: "Pre-flight check before running a content pipeline",
9630
+ module: "pipeline",
9631
+ scope: "mcp:read"
9632
+ },
9633
+ {
9634
+ name: "run_content_pipeline",
9635
+ description: "End-to-end content pipeline: plan \u2192 quality \u2192 approve \u2192 schedule",
9636
+ module: "pipeline",
9637
+ scope: "mcp:autopilot"
9638
+ },
9639
+ {
9640
+ name: "get_pipeline_status",
9641
+ description: "Check status of a pipeline run",
9642
+ module: "pipeline",
9643
+ scope: "mcp:read"
9644
+ },
9645
+ {
9646
+ name: "auto_approve_plan",
9647
+ description: "Batch auto-approve posts meeting quality thresholds",
9648
+ module: "pipeline",
9649
+ scope: "mcp:autopilot"
9650
+ },
9651
+ // suggest
9652
+ {
9653
+ name: "suggest_next_content",
9654
+ description: "Suggest next content topics based on performance data",
9655
+ module: "suggest",
9656
+ scope: "mcp:read"
9657
+ },
9658
+ // digest
9659
+ {
9660
+ name: "generate_performance_digest",
9661
+ description: "Generate a performance summary with trends and recommendations",
9662
+ module: "digest",
9663
+ scope: "mcp:analytics"
9664
+ },
9665
+ {
9666
+ name: "detect_anomalies",
9667
+ description: "Detect significant performance changes (spikes, drops, viral)",
9668
+ module: "digest",
9669
+ scope: "mcp:analytics"
9670
+ },
9671
+ // autopilot (addition)
9672
+ {
9673
+ name: "create_autopilot_config",
9674
+ description: "Create a new autopilot configuration",
9675
+ module: "autopilot",
9676
+ scope: "mcp:autopilot"
9215
9677
  }
9216
9678
  ];
9217
9679
  function getToolsByModule(module) {
@@ -9227,59 +9689,1750 @@ function searchTools(query) {
9227
9689
  );
9228
9690
  }
9229
9691
 
9230
- // src/tools/discovery.ts
9231
- function registerDiscoveryTools(server) {
9692
+ // src/tools/discovery.ts
9693
+ function registerDiscoveryTools(server) {
9694
+ server.tool(
9695
+ "search_tools",
9696
+ 'Search available tools by name, description, module, or scope. Use "name" detail (~50 tokens) for quick lookup, "summary" (~500 tokens) for descriptions, "full" for complete input schemas. Start here if unsure which tool to call \u2014 filter by module (e.g. "planning", "content", "analytics") to narrow results.',
9697
+ {
9698
+ query: z20.string().optional().describe("Search query to filter tools by name or description"),
9699
+ module: z20.string().optional().describe('Filter by module name (e.g. "planning", "content", "analytics")'),
9700
+ scope: z20.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
9701
+ detail: z20.enum(["name", "summary", "full"]).default("summary").describe(
9702
+ 'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
9703
+ )
9704
+ },
9705
+ {
9706
+ title: "Search Tools",
9707
+ readOnlyHint: true,
9708
+ destructiveHint: false,
9709
+ idempotentHint: true,
9710
+ openWorldHint: false
9711
+ },
9712
+ async ({ query, module, scope, detail }) => {
9713
+ let results = [...TOOL_CATALOG];
9714
+ if (query) {
9715
+ results = searchTools(query);
9716
+ }
9717
+ if (module) {
9718
+ const moduleTools = getToolsByModule(module);
9719
+ results = results.filter((t) => moduleTools.some((mt) => mt.name === t.name));
9720
+ }
9721
+ if (scope) {
9722
+ const scopeTools = getToolsByScope(scope);
9723
+ results = results.filter((t) => scopeTools.some((st) => st.name === t.name));
9724
+ }
9725
+ let output;
9726
+ switch (detail) {
9727
+ case "name":
9728
+ output = results.map((t) => t.name);
9729
+ break;
9730
+ case "summary":
9731
+ output = results.map((t) => ({ name: t.name, description: t.description }));
9732
+ break;
9733
+ case "full":
9734
+ default:
9735
+ output = results;
9736
+ break;
9737
+ }
9738
+ return {
9739
+ content: [
9740
+ {
9741
+ type: "text",
9742
+ text: JSON.stringify({ toolCount: results.length, tools: output }, null, 2)
9743
+ }
9744
+ ]
9745
+ };
9746
+ }
9747
+ );
9748
+ }
9749
+
9750
+ // src/tools/pipeline.ts
9751
+ import { z as z21 } from "zod";
9752
+ import { randomUUID as randomUUID3 } from "node:crypto";
9753
+ init_supabase();
9754
+
9755
+ // src/lib/parse-utils.ts
9756
+ function extractJsonArray2(text) {
9757
+ try {
9758
+ const parsed = JSON.parse(text);
9759
+ if (Array.isArray(parsed)) return parsed;
9760
+ for (const key of ["posts", "plan", "content", "items", "results"]) {
9761
+ if (parsed[key] && Array.isArray(parsed[key])) return parsed[key];
9762
+ }
9763
+ } catch {
9764
+ }
9765
+ const match = text.match(/\[[\s\S]*\]/);
9766
+ if (match) {
9767
+ try {
9768
+ return JSON.parse(match[0]);
9769
+ } catch {
9770
+ }
9771
+ }
9772
+ return null;
9773
+ }
9774
+
9775
+ // src/tools/pipeline.ts
9776
+ function asEnvelope16(data) {
9777
+ return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
9778
+ }
9779
+ var PLATFORM_ENUM2 = z21.enum([
9780
+ "youtube",
9781
+ "tiktok",
9782
+ "instagram",
9783
+ "twitter",
9784
+ "linkedin",
9785
+ "facebook",
9786
+ "threads",
9787
+ "bluesky"
9788
+ ]);
9789
+ var BASE_PLAN_CREDITS = 15;
9790
+ var SOURCE_EXTRACTION_CREDITS = 5;
9791
+ function registerPipelineTools(server) {
9792
+ server.tool(
9793
+ "check_pipeline_readiness",
9794
+ "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.",
9795
+ {
9796
+ project_id: z21.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
9797
+ platforms: z21.array(PLATFORM_ENUM2).min(1).describe("Target platforms to check"),
9798
+ estimated_posts: z21.number().min(1).max(50).default(5).describe("Estimated posts to generate"),
9799
+ response_format: z21.enum(["text", "json"]).optional().describe("Response format")
9800
+ },
9801
+ async ({ project_id, platforms, estimated_posts, response_format }) => {
9802
+ const format = response_format ?? "text";
9803
+ const startedAt = Date.now();
9804
+ try {
9805
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
9806
+ const estimatedCost = BASE_PLAN_CREDITS + estimated_posts * 2;
9807
+ const { data: readiness, error: readinessError } = await callEdgeFunction(
9808
+ "mcp-data",
9809
+ {
9810
+ action: "pipeline-readiness",
9811
+ platforms,
9812
+ estimated_posts,
9813
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
9814
+ },
9815
+ { timeoutMs: 15e3 }
9816
+ );
9817
+ if (readinessError || !readiness) {
9818
+ throw new Error(readinessError ?? "No response from mcp-data");
9819
+ }
9820
+ const credits = readiness.credits;
9821
+ const connectedPlatforms = readiness.connected_platforms;
9822
+ const missingPlatforms = readiness.missing_platforms;
9823
+ const hasBrand = readiness.has_brand;
9824
+ const pendingApprovals = readiness.pending_approvals;
9825
+ const insightAge = readiness.insight_age;
9826
+ const insightsFresh = readiness.insights_fresh;
9827
+ const blockers = [];
9828
+ const warnings = [];
9829
+ if (credits < estimatedCost) {
9830
+ blockers.push(`Insufficient credits: ${credits} available, ~${estimatedCost} needed`);
9831
+ }
9832
+ if (missingPlatforms.length > 0) {
9833
+ blockers.push(`Missing connected accounts: ${missingPlatforms.join(", ")}`);
9834
+ }
9835
+ if (!hasBrand) {
9836
+ warnings.push("No brand profile found. Content will use generic voice.");
9837
+ }
9838
+ if (pendingApprovals > 0) {
9839
+ warnings.push(`${pendingApprovals} pending approval(s) from previous runs.`);
9840
+ }
9841
+ if (!insightsFresh) {
9842
+ warnings.push(
9843
+ insightAge === null ? "No performance insights available. Pipeline will skip optimization." : `Insights are ${insightAge} days old. Consider refreshing analytics.`
9844
+ );
9845
+ }
9846
+ const result = {
9847
+ ready: blockers.length === 0,
9848
+ checks: {
9849
+ credits: {
9850
+ available: credits,
9851
+ estimated_cost: estimatedCost,
9852
+ sufficient: credits >= estimatedCost
9853
+ },
9854
+ connected_accounts: { platforms: connectedPlatforms, missing: missingPlatforms },
9855
+ brand_profile: { exists: hasBrand },
9856
+ pending_approvals: { count: pendingApprovals },
9857
+ insights_available: {
9858
+ count: readiness.latest_insight ? 1 : 0,
9859
+ fresh: insightsFresh,
9860
+ last_generated_at: readiness.latest_insight?.generated_at ?? null
9861
+ }
9862
+ },
9863
+ blockers,
9864
+ warnings
9865
+ };
9866
+ const durationMs = Date.now() - startedAt;
9867
+ logMcpToolInvocation({
9868
+ toolName: "check_pipeline_readiness",
9869
+ status: "success",
9870
+ durationMs,
9871
+ details: { ready: result.ready, blockers: blockers.length, warnings: warnings.length }
9872
+ });
9873
+ if (format === "json") {
9874
+ return {
9875
+ content: [{ type: "text", text: JSON.stringify(asEnvelope16(result), null, 2) }]
9876
+ };
9877
+ }
9878
+ const lines = [];
9879
+ lines.push(`Pipeline Readiness: ${result.ready ? "READY" : "NOT READY"}`);
9880
+ lines.push("=".repeat(40));
9881
+ lines.push(
9882
+ `Credits: ${credits} available, ~${estimatedCost} needed \u2014 ${credits >= estimatedCost ? "OK" : "BLOCKED"}`
9883
+ );
9884
+ lines.push(
9885
+ `Accounts: ${connectedPlatforms.length} connected${missingPlatforms.length > 0 ? ` (missing: ${missingPlatforms.join(", ")})` : " \u2014 OK"}`
9886
+ );
9887
+ lines.push(`Brand: ${hasBrand ? "OK" : "Missing (will use generic voice)"}`);
9888
+ lines.push(`Pending Approvals: ${pendingApprovals}`);
9889
+ lines.push(
9890
+ `Insights: ${insightsFresh ? "Fresh" : insightAge === null ? "None available" : `${insightAge} days old`}`
9891
+ );
9892
+ if (blockers.length > 0) {
9893
+ lines.push("");
9894
+ lines.push("BLOCKERS:");
9895
+ for (const b of blockers) lines.push(` \u2717 ${b}`);
9896
+ }
9897
+ if (warnings.length > 0) {
9898
+ lines.push("");
9899
+ lines.push("WARNINGS:");
9900
+ for (const w of warnings) lines.push(` ! ${w}`);
9901
+ }
9902
+ return { content: [{ type: "text", text: lines.join("\n") }] };
9903
+ } catch (err) {
9904
+ const durationMs = Date.now() - startedAt;
9905
+ const message = err instanceof Error ? err.message : String(err);
9906
+ logMcpToolInvocation({
9907
+ toolName: "check_pipeline_readiness",
9908
+ status: "error",
9909
+ durationMs,
9910
+ details: { error: message }
9911
+ });
9912
+ return {
9913
+ content: [{ type: "text", text: `Readiness check failed: ${message}` }],
9914
+ isError: true
9915
+ };
9916
+ }
9917
+ }
9918
+ );
9919
+ server.tool(
9920
+ "run_content_pipeline",
9921
+ "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.",
9922
+ {
9923
+ project_id: z21.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
9924
+ topic: z21.string().optional().describe("Content topic (required if no source_url)"),
9925
+ source_url: z21.string().optional().describe("URL to extract content from"),
9926
+ platforms: z21.array(PLATFORM_ENUM2).min(1).describe("Target platforms"),
9927
+ days: z21.number().min(1).max(7).default(5).describe("Days to plan"),
9928
+ posts_per_day: z21.number().min(1).max(3).default(1).describe("Posts per platform per day"),
9929
+ approval_mode: z21.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe(
9930
+ "auto: approve all passing quality. review_all: flag everything. review_low_confidence: auto-approve high scorers."
9931
+ ),
9932
+ auto_approve_threshold: z21.number().min(0).max(35).default(28).describe(
9933
+ "Quality score threshold for auto-approval (used in auto/review_low_confidence modes)"
9934
+ ),
9935
+ max_credits: z21.number().optional().describe("Credit budget cap"),
9936
+ dry_run: z21.boolean().default(false).describe("If true, skip scheduling and return plan only"),
9937
+ skip_stages: z21.array(z21.enum(["research", "quality", "schedule"])).optional().describe("Stages to skip"),
9938
+ response_format: z21.enum(["text", "json"]).default("json")
9939
+ },
9940
+ async ({
9941
+ project_id,
9942
+ topic,
9943
+ source_url,
9944
+ platforms,
9945
+ days,
9946
+ posts_per_day,
9947
+ approval_mode,
9948
+ auto_approve_threshold,
9949
+ max_credits,
9950
+ dry_run,
9951
+ skip_stages,
9952
+ response_format
9953
+ }) => {
9954
+ const startedAt = Date.now();
9955
+ const pipelineId = randomUUID3();
9956
+ const stagesCompleted = [];
9957
+ const stagesSkipped = [];
9958
+ const errors = [];
9959
+ let creditsUsed = 0;
9960
+ if (!topic && !source_url) {
9961
+ return {
9962
+ content: [{ type: "text", text: "Either topic or source_url is required." }],
9963
+ isError: true
9964
+ };
9965
+ }
9966
+ const skipSet = new Set(skip_stages ?? []);
9967
+ try {
9968
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
9969
+ const estimatedCost = BASE_PLAN_CREDITS + (source_url ? SOURCE_EXTRACTION_CREDITS : 0);
9970
+ const { data: budgetData } = await callEdgeFunction(
9971
+ "mcp-data",
9972
+ {
9973
+ action: "run-pipeline",
9974
+ plan_status: "budget-check",
9975
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
9976
+ },
9977
+ { timeoutMs: 1e4 }
9978
+ );
9979
+ const availableCredits = budgetData?.credits ?? 0;
9980
+ const creditLimit = max_credits ?? availableCredits;
9981
+ if (availableCredits < estimatedCost) {
9982
+ return {
9983
+ content: [
9984
+ {
9985
+ type: "text",
9986
+ text: `Insufficient credits: ${availableCredits} available, ~${estimatedCost} needed.`
9987
+ }
9988
+ ],
9989
+ isError: true
9990
+ };
9991
+ }
9992
+ stagesCompleted.push("budget_check");
9993
+ await callEdgeFunction(
9994
+ "mcp-data",
9995
+ {
9996
+ action: "run-pipeline",
9997
+ plan_status: "create",
9998
+ pipeline_id: pipelineId,
9999
+ config: {
10000
+ topic,
10001
+ source_url,
10002
+ platforms,
10003
+ days,
10004
+ posts_per_day,
10005
+ approval_mode,
10006
+ auto_approve_threshold,
10007
+ dry_run,
10008
+ skip_stages: skip_stages ?? []
10009
+ },
10010
+ current_stage: "planning",
10011
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
10012
+ },
10013
+ { timeoutMs: 1e4 }
10014
+ );
10015
+ const resolvedTopic = topic ?? source_url ?? "Content plan";
10016
+ const { data: planData, error: planError } = await callEdgeFunction(
10017
+ "social-neuron-ai",
10018
+ {
10019
+ type: "generation",
10020
+ prompt: buildPlanPrompt(resolvedTopic, platforms, days, posts_per_day, source_url),
10021
+ model: "gemini-2.5-flash",
10022
+ responseFormat: "json",
10023
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
10024
+ },
10025
+ { timeoutMs: 6e4 }
10026
+ );
10027
+ if (planError || !planData) {
10028
+ errors.push({ stage: "planning", message: planError ?? "No AI response" });
10029
+ await callEdgeFunction(
10030
+ "mcp-data",
10031
+ {
10032
+ action: "run-pipeline",
10033
+ plan_status: "update",
10034
+ pipeline_id: pipelineId,
10035
+ status: "failed",
10036
+ stages_completed: stagesCompleted,
10037
+ errors,
10038
+ current_stage: null,
10039
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
10040
+ },
10041
+ { timeoutMs: 1e4 }
10042
+ );
10043
+ return {
10044
+ content: [
10045
+ { type: "text", text: `Planning failed: ${planError ?? "No AI response"}` }
10046
+ ],
10047
+ isError: true
10048
+ };
10049
+ }
10050
+ creditsUsed += BASE_PLAN_CREDITS;
10051
+ if (!dry_run) {
10052
+ try {
10053
+ await callEdgeFunction(
10054
+ "mcp-data",
10055
+ {
10056
+ action: "run-pipeline",
10057
+ plan_status: "deduct-credits",
10058
+ credits_used: BASE_PLAN_CREDITS,
10059
+ reason: `Pipeline ${pipelineId.slice(0, 8)}: content plan generation`
10060
+ },
10061
+ { timeoutMs: 1e4 }
10062
+ );
10063
+ } catch (deductErr) {
10064
+ errors.push({
10065
+ stage: "planning",
10066
+ message: `Credit deduction failed: ${deductErr instanceof Error ? deductErr.message : String(deductErr)}`
10067
+ });
10068
+ }
10069
+ }
10070
+ stagesCompleted.push("planning");
10071
+ const rawText = String(planData.text ?? planData.content ?? "");
10072
+ const postsArray = extractJsonArray2(rawText);
10073
+ const posts = (postsArray ?? []).map((p) => ({
10074
+ id: String(p.id ?? randomUUID3().slice(0, 8)),
10075
+ day: Number(p.day ?? 1),
10076
+ date: String(p.date ?? ""),
10077
+ platform: String(p.platform ?? ""),
10078
+ content_type: p.content_type ?? "caption",
10079
+ caption: String(p.caption ?? ""),
10080
+ title: p.title ? String(p.title) : void 0,
10081
+ hashtags: Array.isArray(p.hashtags) ? p.hashtags.map(String) : void 0,
10082
+ hook: String(p.hook ?? ""),
10083
+ angle: String(p.angle ?? ""),
10084
+ visual_direction: p.visual_direction ? String(p.visual_direction) : void 0,
10085
+ media_type: p.media_type ? String(p.media_type) : void 0
10086
+ }));
10087
+ let postsApproved = 0;
10088
+ let postsFlagged = 0;
10089
+ if (!skipSet.has("quality")) {
10090
+ for (const post of posts) {
10091
+ const quality = evaluateQuality({
10092
+ caption: post.caption,
10093
+ title: post.title,
10094
+ platforms: [post.platform],
10095
+ threshold: auto_approve_threshold
10096
+ });
10097
+ post.quality = {
10098
+ score: quality.total,
10099
+ max_score: quality.maxTotal,
10100
+ passed: quality.passed,
10101
+ blockers: quality.blockers
10102
+ };
10103
+ if (approval_mode === "auto" && quality.passed) {
10104
+ post.status = "approved";
10105
+ postsApproved++;
10106
+ } else if (approval_mode === "review_low_confidence") {
10107
+ if (quality.total >= auto_approve_threshold && quality.blockers.length === 0) {
10108
+ post.status = "approved";
10109
+ postsApproved++;
10110
+ } else {
10111
+ post.status = "needs_edit";
10112
+ postsFlagged++;
10113
+ }
10114
+ } else {
10115
+ post.status = "pending";
10116
+ postsFlagged++;
10117
+ }
10118
+ }
10119
+ stagesCompleted.push("quality_check");
10120
+ } else {
10121
+ stagesSkipped.push("quality_check");
10122
+ for (const post of posts) {
10123
+ post.status = "approved";
10124
+ postsApproved++;
10125
+ }
10126
+ }
10127
+ const planId = randomUUID3();
10128
+ if (resolvedProjectId) {
10129
+ const startDate = /* @__PURE__ */ new Date();
10130
+ startDate.setDate(startDate.getDate() + 1);
10131
+ const endDate = new Date(startDate);
10132
+ endDate.setDate(endDate.getDate() + days - 1);
10133
+ await callEdgeFunction(
10134
+ "mcp-data",
10135
+ {
10136
+ action: "run-pipeline",
10137
+ plan_status: "persist-plan",
10138
+ pipeline_id: pipelineId,
10139
+ plan_id: planId,
10140
+ topic: resolvedTopic,
10141
+ status: postsFlagged > 0 ? "in_review" : "approved",
10142
+ plan_payload: {
10143
+ plan_id: planId,
10144
+ topic: resolvedTopic,
10145
+ platforms,
10146
+ posts,
10147
+ start_date: startDate.toISOString().split("T")[0],
10148
+ end_date: endDate.toISOString().split("T")[0],
10149
+ estimated_credits: estimatedCost,
10150
+ generated_at: (/* @__PURE__ */ new Date()).toISOString()
10151
+ },
10152
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
10153
+ },
10154
+ { timeoutMs: 1e4 }
10155
+ );
10156
+ }
10157
+ stagesCompleted.push("persist_plan");
10158
+ if (postsFlagged > 0 && resolvedProjectId) {
10159
+ const userId = await getDefaultUserId();
10160
+ const resolvedApprovalRows = posts.filter((p) => p.status !== "approved").map((post) => ({
10161
+ plan_id: planId,
10162
+ post_id: post.id,
10163
+ project_id: resolvedProjectId,
10164
+ user_id: userId,
10165
+ status: "pending",
10166
+ original_post: post,
10167
+ auto_approved: false
10168
+ }));
10169
+ if (resolvedApprovalRows.length > 0) {
10170
+ await callEdgeFunction(
10171
+ "mcp-data",
10172
+ {
10173
+ action: "run-pipeline",
10174
+ plan_status: "upsert-approvals",
10175
+ posts: resolvedApprovalRows
10176
+ },
10177
+ { timeoutMs: 1e4 }
10178
+ );
10179
+ }
10180
+ }
10181
+ if (postsApproved > 0 && resolvedProjectId) {
10182
+ const userId = await getDefaultUserId();
10183
+ const autoApprovedRows = posts.filter((p) => p.status === "approved").map((post) => ({
10184
+ plan_id: planId,
10185
+ post_id: post.id,
10186
+ project_id: resolvedProjectId,
10187
+ user_id: userId,
10188
+ status: "approved",
10189
+ original_post: post,
10190
+ auto_approved: true
10191
+ }));
10192
+ if (autoApprovedRows.length > 0) {
10193
+ await callEdgeFunction(
10194
+ "mcp-data",
10195
+ {
10196
+ action: "run-pipeline",
10197
+ plan_status: "upsert-approvals",
10198
+ posts: autoApprovedRows
10199
+ },
10200
+ { timeoutMs: 1e4 }
10201
+ );
10202
+ }
10203
+ }
10204
+ let postsScheduled = 0;
10205
+ if (!dry_run && !skipSet.has("schedule") && postsApproved > 0) {
10206
+ const approvedPosts = posts.filter((p) => p.status === "approved");
10207
+ for (const post of approvedPosts) {
10208
+ if (creditsUsed >= creditLimit) {
10209
+ errors.push({ stage: "schedule", message: "Credit limit reached" });
10210
+ break;
10211
+ }
10212
+ try {
10213
+ const { error: schedError } = await callEdgeFunction(
10214
+ "schedule-post",
10215
+ {
10216
+ platform: post.platform,
10217
+ caption: post.caption,
10218
+ title: post.title,
10219
+ hashtags: post.hashtags,
10220
+ media_url: post.media_url,
10221
+ scheduled_at: post.schedule_at,
10222
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
10223
+ },
10224
+ { timeoutMs: 15e3 }
10225
+ );
10226
+ if (schedError) {
10227
+ errors.push({
10228
+ stage: "schedule",
10229
+ message: `Failed to schedule ${post.id}: ${schedError}`
10230
+ });
10231
+ } else {
10232
+ postsScheduled++;
10233
+ }
10234
+ } catch (schedErr) {
10235
+ errors.push({
10236
+ stage: "schedule",
10237
+ message: `Failed to schedule ${post.id}: ${schedErr instanceof Error ? schedErr.message : String(schedErr)}`
10238
+ });
10239
+ }
10240
+ }
10241
+ stagesCompleted.push("schedule");
10242
+ } else if (dry_run) {
10243
+ stagesSkipped.push("schedule");
10244
+ } else if (skipSet.has("schedule")) {
10245
+ stagesSkipped.push("schedule");
10246
+ }
10247
+ const finalStatus = errors.length > 0 && stagesCompleted.length <= 2 ? "failed" : postsFlagged > 0 ? "awaiting_approval" : "completed";
10248
+ await callEdgeFunction(
10249
+ "mcp-data",
10250
+ {
10251
+ action: "run-pipeline",
10252
+ plan_status: "update",
10253
+ pipeline_id: pipelineId,
10254
+ status: finalStatus,
10255
+ plan_id: planId,
10256
+ stages_completed: stagesCompleted,
10257
+ stages_skipped: stagesSkipped,
10258
+ current_stage: null,
10259
+ posts_generated: posts.length,
10260
+ posts_approved: postsApproved,
10261
+ posts_scheduled: postsScheduled,
10262
+ posts_flagged: postsFlagged,
10263
+ credits_used: creditsUsed,
10264
+ errors,
10265
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
10266
+ },
10267
+ { timeoutMs: 1e4 }
10268
+ );
10269
+ const durationMs = Date.now() - startedAt;
10270
+ logMcpToolInvocation({
10271
+ toolName: "run_content_pipeline",
10272
+ status: "success",
10273
+ durationMs,
10274
+ details: {
10275
+ pipeline_id: pipelineId,
10276
+ posts: posts.length,
10277
+ approved: postsApproved,
10278
+ scheduled: postsScheduled,
10279
+ flagged: postsFlagged
10280
+ }
10281
+ });
10282
+ const resultPayload = {
10283
+ pipeline_id: pipelineId,
10284
+ stages_completed: stagesCompleted,
10285
+ stages_skipped: stagesSkipped,
10286
+ plan_id: planId,
10287
+ posts_generated: posts.length,
10288
+ posts_approved: postsApproved,
10289
+ posts_scheduled: postsScheduled,
10290
+ posts_flagged: postsFlagged,
10291
+ credits_used: creditsUsed,
10292
+ credits_remaining: availableCredits - creditsUsed,
10293
+ dry_run,
10294
+ 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.",
10295
+ errors: errors.length > 0 ? errors : void 0
10296
+ };
10297
+ if (response_format === "json") {
10298
+ return {
10299
+ content: [
10300
+ { type: "text", text: JSON.stringify(asEnvelope16(resultPayload), null, 2) }
10301
+ ]
10302
+ };
10303
+ }
10304
+ const lines = [];
10305
+ lines.push(`Pipeline ${pipelineId.slice(0, 8)}... ${finalStatus.toUpperCase()}`);
10306
+ lines.push("=".repeat(40));
10307
+ lines.push(`Posts generated: ${posts.length}`);
10308
+ lines.push(`Posts approved: ${postsApproved}`);
10309
+ lines.push(`Posts scheduled: ${postsScheduled}`);
10310
+ lines.push(`Posts flagged for review: ${postsFlagged}`);
10311
+ lines.push(`Credits used: ${creditsUsed}`);
10312
+ lines.push(`Stages: ${stagesCompleted.join(" \u2192 ")}`);
10313
+ if (stagesSkipped.length > 0) {
10314
+ lines.push(`Skipped: ${stagesSkipped.join(", ")}`);
10315
+ }
10316
+ if (errors.length > 0) {
10317
+ lines.push("");
10318
+ lines.push("Errors:");
10319
+ for (const e of errors) lines.push(` [${e.stage}] ${e.message}`);
10320
+ }
10321
+ lines.push("");
10322
+ lines.push(`Next: ${resultPayload.next_action}`);
10323
+ return { content: [{ type: "text", text: lines.join("\n") }] };
10324
+ } catch (err) {
10325
+ const durationMs = Date.now() - startedAt;
10326
+ const message = err instanceof Error ? err.message : String(err);
10327
+ logMcpToolInvocation({
10328
+ toolName: "run_content_pipeline",
10329
+ status: "error",
10330
+ durationMs,
10331
+ details: { error: message }
10332
+ });
10333
+ try {
10334
+ await callEdgeFunction(
10335
+ "mcp-data",
10336
+ {
10337
+ action: "run-pipeline",
10338
+ plan_status: "update",
10339
+ pipeline_id: pipelineId,
10340
+ status: "failed",
10341
+ stages_completed: stagesCompleted,
10342
+ errors: [...errors, { stage: "unknown", message }],
10343
+ current_stage: null,
10344
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
10345
+ },
10346
+ { timeoutMs: 1e4 }
10347
+ );
10348
+ } catch {
10349
+ }
10350
+ return {
10351
+ content: [{ type: "text", text: `Pipeline failed: ${message}` }],
10352
+ isError: true
10353
+ };
10354
+ }
10355
+ }
10356
+ );
10357
+ server.tool(
10358
+ "get_pipeline_status",
10359
+ "Check status of a pipeline run, including stages completed, pending approvals, and scheduled posts.",
10360
+ {
10361
+ pipeline_id: z21.string().uuid().optional().describe("Pipeline run ID (omit for latest)"),
10362
+ response_format: z21.enum(["text", "json"]).optional()
10363
+ },
10364
+ async ({ pipeline_id, response_format }) => {
10365
+ const format = response_format ?? "text";
10366
+ const { data: result, error: fetchError } = await callEdgeFunction(
10367
+ "mcp-data",
10368
+ {
10369
+ action: "get-pipeline-status",
10370
+ ...pipeline_id ? { pipeline_id } : {}
10371
+ },
10372
+ { timeoutMs: 1e4 }
10373
+ );
10374
+ if (fetchError) {
10375
+ return {
10376
+ content: [{ type: "text", text: `Error: ${fetchError}` }],
10377
+ isError: true
10378
+ };
10379
+ }
10380
+ const data = result?.pipeline;
10381
+ if (!data) {
10382
+ return {
10383
+ content: [{ type: "text", text: "No pipeline runs found." }]
10384
+ };
10385
+ }
10386
+ if (format === "json") {
10387
+ return {
10388
+ content: [{ type: "text", text: JSON.stringify(asEnvelope16(data), null, 2) }]
10389
+ };
10390
+ }
10391
+ const lines = [];
10392
+ lines.push(
10393
+ `Pipeline ${String(data.id).slice(0, 8)}... \u2014 ${String(data.status).toUpperCase()}`
10394
+ );
10395
+ lines.push("=".repeat(40));
10396
+ lines.push(`Started: ${data.started_at}`);
10397
+ if (data.completed_at) lines.push(`Completed: ${data.completed_at}`);
10398
+ lines.push(
10399
+ `Stages: ${(Array.isArray(data.stages_completed) ? data.stages_completed : []).join(" \u2192 ") || "none"}`
10400
+ );
10401
+ if (Array.isArray(data.stages_skipped) && data.stages_skipped.length > 0) {
10402
+ lines.push(`Skipped: ${data.stages_skipped.join(", ")}`);
10403
+ }
10404
+ lines.push(
10405
+ `Posts: ${data.posts_generated} generated, ${data.posts_approved} approved, ${data.posts_scheduled} scheduled, ${data.posts_flagged} flagged`
10406
+ );
10407
+ lines.push(`Credits used: ${data.credits_used}`);
10408
+ if (data.plan_id) lines.push(`Plan ID: ${data.plan_id}`);
10409
+ const errs = data.errors;
10410
+ if (errs && errs.length > 0) {
10411
+ lines.push("");
10412
+ lines.push("Errors:");
10413
+ for (const e of errs) lines.push(` [${e.stage}] ${e.message}`);
10414
+ }
10415
+ return { content: [{ type: "text", text: lines.join("\n") }] };
10416
+ }
10417
+ );
10418
+ server.tool(
10419
+ "auto_approve_plan",
10420
+ "Batch auto-approve posts in a content plan that meet quality thresholds. Posts below the threshold are flagged for manual review.",
10421
+ {
10422
+ plan_id: z21.string().uuid().describe("Content plan ID"),
10423
+ quality_threshold: z21.number().min(0).max(35).default(26).describe("Minimum quality score to auto-approve"),
10424
+ response_format: z21.enum(["text", "json"]).default("json")
10425
+ },
10426
+ async ({ plan_id, quality_threshold, response_format }) => {
10427
+ const startedAt = Date.now();
10428
+ try {
10429
+ const { data: loadResult, error: loadError } = await callEdgeFunction("mcp-data", { action: "auto-approve-plan", plan_id }, { timeoutMs: 1e4 });
10430
+ if (loadError) {
10431
+ return {
10432
+ content: [{ type: "text", text: `Failed to load plan: ${loadError}` }],
10433
+ isError: true
10434
+ };
10435
+ }
10436
+ const stored = loadResult?.plan;
10437
+ if (!stored?.plan_payload) {
10438
+ return {
10439
+ content: [
10440
+ { type: "text", text: `No content plan found for plan_id=${plan_id}` }
10441
+ ],
10442
+ isError: true
10443
+ };
10444
+ }
10445
+ const plan = stored.plan_payload;
10446
+ const posts = Array.isArray(plan.posts) ? plan.posts : [];
10447
+ let autoApproved = 0;
10448
+ let flagged = 0;
10449
+ let rejected = 0;
10450
+ const details = [];
10451
+ for (const post of posts) {
10452
+ const quality = evaluateQuality({
10453
+ caption: post.caption,
10454
+ title: post.title,
10455
+ platforms: [post.platform],
10456
+ threshold: quality_threshold
10457
+ });
10458
+ if (quality.total >= quality_threshold && quality.blockers.length === 0) {
10459
+ post.status = "approved";
10460
+ post.quality = {
10461
+ score: quality.total,
10462
+ max_score: quality.maxTotal,
10463
+ passed: true,
10464
+ blockers: []
10465
+ };
10466
+ autoApproved++;
10467
+ details.push({ post_id: post.id, action: "approved", score: quality.total });
10468
+ } else if (quality.total >= quality_threshold - 5) {
10469
+ post.status = "needs_edit";
10470
+ post.quality = {
10471
+ score: quality.total,
10472
+ max_score: quality.maxTotal,
10473
+ passed: false,
10474
+ blockers: quality.blockers
10475
+ };
10476
+ flagged++;
10477
+ details.push({ post_id: post.id, action: "flagged", score: quality.total });
10478
+ } else {
10479
+ post.status = "rejected";
10480
+ post.quality = {
10481
+ score: quality.total,
10482
+ max_score: quality.maxTotal,
10483
+ passed: false,
10484
+ blockers: quality.blockers
10485
+ };
10486
+ rejected++;
10487
+ details.push({ post_id: post.id, action: "rejected", score: quality.total });
10488
+ }
10489
+ }
10490
+ const newStatus = flagged === 0 && rejected === 0 ? "approved" : "in_review";
10491
+ const userId = await getDefaultUserId();
10492
+ const resolvedRows = posts.map((post) => ({
10493
+ plan_id,
10494
+ post_id: post.id,
10495
+ project_id: stored.project_id,
10496
+ user_id: userId,
10497
+ status: post.status === "approved" ? "approved" : post.status === "rejected" ? "rejected" : "pending",
10498
+ original_post: post,
10499
+ auto_approved: post.status === "approved"
10500
+ }));
10501
+ await callEdgeFunction(
10502
+ "mcp-data",
10503
+ {
10504
+ action: "auto-approve-plan",
10505
+ plan_id,
10506
+ plan_status: newStatus,
10507
+ plan_payload: { ...plan, posts },
10508
+ posts: resolvedRows
10509
+ },
10510
+ { timeoutMs: 1e4 }
10511
+ );
10512
+ const durationMs = Date.now() - startedAt;
10513
+ logMcpToolInvocation({
10514
+ toolName: "auto_approve_plan",
10515
+ status: "success",
10516
+ durationMs,
10517
+ details: { plan_id, auto_approved: autoApproved, flagged, rejected }
10518
+ });
10519
+ const resultPayload = {
10520
+ plan_id,
10521
+ auto_approved: autoApproved,
10522
+ flagged_for_review: flagged,
10523
+ rejected,
10524
+ details,
10525
+ plan_status: newStatus
10526
+ };
10527
+ if (response_format === "json") {
10528
+ return {
10529
+ content: [
10530
+ { type: "text", text: JSON.stringify(asEnvelope16(resultPayload), null, 2) }
10531
+ ]
10532
+ };
10533
+ }
10534
+ const lines = [];
10535
+ lines.push(`Auto-Approve Results for Plan ${plan_id.slice(0, 8)}...`);
10536
+ lines.push("=".repeat(40));
10537
+ lines.push(`Auto-approved: ${autoApproved}`);
10538
+ lines.push(`Flagged for review: ${flagged}`);
10539
+ lines.push(`Rejected: ${rejected}`);
10540
+ lines.push(`Plan status: ${newStatus}`);
10541
+ if (details.length > 0) {
10542
+ lines.push("");
10543
+ for (const d of details) {
10544
+ lines.push(` ${d.post_id}: ${d.action} (score: ${d.score}/35)`);
10545
+ }
10546
+ }
10547
+ return { content: [{ type: "text", text: lines.join("\n") }] };
10548
+ } catch (err) {
10549
+ const durationMs = Date.now() - startedAt;
10550
+ const message = err instanceof Error ? err.message : String(err);
10551
+ logMcpToolInvocation({
10552
+ toolName: "auto_approve_plan",
10553
+ status: "error",
10554
+ durationMs,
10555
+ details: { error: message }
10556
+ });
10557
+ return {
10558
+ content: [{ type: "text", text: `Auto-approve failed: ${message}` }],
10559
+ isError: true
10560
+ };
10561
+ }
10562
+ }
10563
+ );
10564
+ }
10565
+ function sanitizeTopic(raw) {
10566
+ return raw.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 500);
10567
+ }
10568
+ function buildPlanPrompt(topic, platforms, days, postsPerDay, sourceUrl) {
10569
+ const safeTopic = sanitizeTopic(topic);
10570
+ const parts = [
10571
+ `Generate a ${days}-day content plan for "${safeTopic}" across platforms: ${platforms.join(", ")}.`,
10572
+ `${postsPerDay} post(s) per platform per day.`,
10573
+ sourceUrl ? `Source material URL: ${sourceUrl}` : "",
10574
+ "",
10575
+ "For each post, return a JSON object with:",
10576
+ " id, day, date, platform, content_type, caption, title, hashtags, hook, angle, visual_direction, media_type",
10577
+ "",
10578
+ "Return ONLY a JSON array. No surrounding text."
10579
+ ];
10580
+ return parts.filter(Boolean).join("\n");
10581
+ }
10582
+
10583
+ // src/tools/suggest.ts
10584
+ import { z as z22 } from "zod";
10585
+ init_supabase();
10586
+ function asEnvelope17(data) {
10587
+ return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
10588
+ }
10589
+ function registerSuggestTools(server) {
10590
+ server.tool(
10591
+ "suggest_next_content",
10592
+ "Suggest next content topics based on performance insights, past content, and competitor patterns. No AI call, no credit cost \u2014 purely data-driven recommendations.",
10593
+ {
10594
+ project_id: z22.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
10595
+ count: z22.number().min(1).max(10).default(3).describe("Number of suggestions to return"),
10596
+ response_format: z22.enum(["text", "json"]).optional()
10597
+ },
10598
+ async ({ project_id, count, response_format }) => {
10599
+ const format = response_format ?? "text";
10600
+ const startedAt = Date.now();
10601
+ try {
10602
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
10603
+ action: "suggest-content",
10604
+ projectId: project_id
10605
+ });
10606
+ if (efError) throw new Error(efError);
10607
+ const insights = result?.insights ?? [];
10608
+ const recentContent = result?.recentContent ?? [];
10609
+ const swipeItems = result?.swipeItems ?? [];
10610
+ const hookInsights = insights.filter(
10611
+ (i) => i.insight_type === "top_hooks" || i.insight_type === "winning_hooks"
10612
+ );
10613
+ const recentTopics = new Set(
10614
+ recentContent.map((c) => c.topic?.toLowerCase()).filter(Boolean)
10615
+ );
10616
+ const dataQuality = insights.length >= 10 ? "strong" : insights.length >= 3 ? "moderate" : "weak";
10617
+ const latestInsightDate = insights[0]?.generated_at ?? null;
10618
+ const suggestions = [];
10619
+ for (const insight of hookInsights.slice(0, Math.ceil(count / 2))) {
10620
+ const data = insight.insight_data;
10621
+ const hooks = Array.isArray(data.hooks) ? data.hooks : Array.isArray(data.top_hooks) ? data.top_hooks : [];
10622
+ for (const hook of hooks.slice(0, 2)) {
10623
+ const hookStr = typeof hook === "string" ? hook : String(hook.text ?? hook);
10624
+ if (suggestions.length >= count) break;
10625
+ suggestions.push({
10626
+ topic: `Content inspired by winning hook: "${hookStr.slice(0, 80)}"`,
10627
+ platform: String(data.platform ?? "tiktok"),
10628
+ content_type: "caption",
10629
+ rationale: "This hook pattern performed well in your past content.",
10630
+ confidence: insight.confidence_score ?? 0.7,
10631
+ based_on: ["performance_insights", "hook_analysis"],
10632
+ suggested_hook: hookStr.slice(0, 120),
10633
+ suggested_angle: "Apply this hook style to a fresh topic in your niche."
10634
+ });
10635
+ }
10636
+ }
10637
+ for (const swipe of swipeItems.slice(0, Math.ceil(count / 3))) {
10638
+ if (suggestions.length >= count) break;
10639
+ const title = swipe.title ?? "";
10640
+ if (recentTopics.has(title.toLowerCase())) continue;
10641
+ suggestions.push({
10642
+ topic: `Competitor-inspired: "${title.slice(0, 80)}"`,
10643
+ platform: swipe.platform ?? "instagram",
10644
+ content_type: "caption",
10645
+ rationale: `High-performing competitor content (score: ${swipe.engagement_score ?? "N/A"}).`,
10646
+ confidence: 0.6,
10647
+ based_on: ["niche_swipe_file", "competitor_analysis"],
10648
+ suggested_hook: swipe.hook ?? `Your take on: ${title.slice(0, 60)}`,
10649
+ suggested_angle: "Put your unique spin on this trending topic."
10650
+ });
10651
+ }
10652
+ if (suggestions.length < count) {
10653
+ const recentFormats = recentContent.map((c) => c.content_type).filter(Boolean);
10654
+ const formatCounts = {};
10655
+ for (const f of recentFormats) {
10656
+ formatCounts[f] = (formatCounts[f] ?? 0) + 1;
10657
+ }
10658
+ const allFormats = ["script", "caption", "blog", "hook"];
10659
+ const underusedFormats = allFormats.filter(
10660
+ (f) => (formatCounts[f] ?? 0) < recentFormats.length / allFormats.length * 0.5
10661
+ );
10662
+ for (const fmt of underusedFormats.slice(0, count - suggestions.length)) {
10663
+ suggestions.push({
10664
+ topic: `Try a ${fmt} format \u2014 you haven't used it recently`,
10665
+ platform: "linkedin",
10666
+ content_type: fmt,
10667
+ rationale: `You've posted ${formatCounts[fmt] ?? 0} ${fmt}(s) recently vs ${recentFormats.length} total posts. Diversifying formats can reach new audiences.`,
10668
+ confidence: 0.5,
10669
+ based_on: ["content_history", "format_analysis"],
10670
+ suggested_hook: `Experiment with ${fmt} content for your audience.`,
10671
+ suggested_angle: "Format diversification to increase reach."
10672
+ });
10673
+ }
10674
+ }
10675
+ if (suggestions.length < count) {
10676
+ suggestions.push({
10677
+ topic: "Share a behind-the-scenes look at your process",
10678
+ platform: "instagram",
10679
+ content_type: "caption",
10680
+ rationale: "Behind-the-scenes content consistently drives engagement across platforms.",
10681
+ confidence: 0.4,
10682
+ based_on: ["general_best_practices"],
10683
+ suggested_hook: "Here's what it actually takes to...",
10684
+ suggested_angle: "Authenticity and transparency."
10685
+ });
10686
+ }
10687
+ const durationMs = Date.now() - startedAt;
10688
+ logMcpToolInvocation({
10689
+ toolName: "suggest_next_content",
10690
+ status: "success",
10691
+ durationMs,
10692
+ details: {
10693
+ suggestions: suggestions.length,
10694
+ data_quality: dataQuality,
10695
+ insights_count: insights.length
10696
+ }
10697
+ });
10698
+ const resultPayload = {
10699
+ suggestions: suggestions.slice(0, count),
10700
+ data_quality: dataQuality,
10701
+ last_analysis_at: latestInsightDate
10702
+ };
10703
+ if (format === "json") {
10704
+ return {
10705
+ content: [
10706
+ { type: "text", text: JSON.stringify(asEnvelope17(resultPayload), null, 2) }
10707
+ ]
10708
+ };
10709
+ }
10710
+ const lines = [];
10711
+ lines.push(`Content Suggestions (${suggestions.length})`);
10712
+ lines.push(`Data Quality: ${dataQuality} | Last analysis: ${latestInsightDate ?? "never"}`);
10713
+ lines.push("=".repeat(40));
10714
+ for (let i = 0; i < suggestions.length; i++) {
10715
+ const s = suggestions[i];
10716
+ lines.push(`
10717
+ ${i + 1}. ${s.topic}`);
10718
+ lines.push(` Platform: ${s.platform} | Type: ${s.content_type}`);
10719
+ lines.push(` Hook: "${s.suggested_hook}"`);
10720
+ lines.push(` Angle: ${s.suggested_angle}`);
10721
+ lines.push(` Rationale: ${s.rationale}`);
10722
+ lines.push(` Confidence: ${Math.round(s.confidence * 100)}%`);
10723
+ lines.push(` Based on: ${s.based_on.join(", ")}`);
10724
+ }
10725
+ return { content: [{ type: "text", text: lines.join("\n") }] };
10726
+ } catch (err) {
10727
+ const durationMs = Date.now() - startedAt;
10728
+ const message = err instanceof Error ? err.message : String(err);
10729
+ logMcpToolInvocation({
10730
+ toolName: "suggest_next_content",
10731
+ status: "error",
10732
+ durationMs,
10733
+ details: { error: message }
10734
+ });
10735
+ return {
10736
+ content: [{ type: "text", text: `Suggestion failed: ${message}` }],
10737
+ isError: true
10738
+ };
10739
+ }
10740
+ }
10741
+ );
10742
+ }
10743
+
10744
+ // src/tools/digest.ts
10745
+ import { z as z23 } from "zod";
10746
+ init_supabase();
10747
+
10748
+ // src/lib/anomaly-detector.ts
10749
+ var SENSITIVITY_THRESHOLDS = {
10750
+ low: 50,
10751
+ medium: 30,
10752
+ high: 15
10753
+ };
10754
+ var VIRAL_MULTIPLIER = 10;
10755
+ function aggregateByPlatform(data) {
10756
+ const map = /* @__PURE__ */ new Map();
10757
+ for (const d of data) {
10758
+ const existing = map.get(d.platform) ?? { views: 0, engagement: 0, posts: 0 };
10759
+ existing.views += d.views;
10760
+ existing.engagement += d.engagement;
10761
+ existing.posts += d.posts;
10762
+ map.set(d.platform, existing);
10763
+ }
10764
+ return map;
10765
+ }
10766
+ function pctChange(current, previous) {
10767
+ if (previous === 0) return current > 0 ? 100 : 0;
10768
+ return (current - previous) / previous * 100;
10769
+ }
10770
+ function detectAnomalies(currentData, previousData, sensitivity = "medium", averageViewsPerPost) {
10771
+ const threshold = SENSITIVITY_THRESHOLDS[sensitivity];
10772
+ const anomalies = [];
10773
+ const currentAgg = aggregateByPlatform(currentData);
10774
+ const previousAgg = aggregateByPlatform(previousData);
10775
+ const currentDates = currentData.map((d) => d.date).sort();
10776
+ const period = {
10777
+ current_start: currentDates[0] ?? "",
10778
+ current_end: currentDates[currentDates.length - 1] ?? ""
10779
+ };
10780
+ const allPlatforms = /* @__PURE__ */ new Set([...currentAgg.keys(), ...previousAgg.keys()]);
10781
+ for (const platform2 of allPlatforms) {
10782
+ const current = currentAgg.get(platform2) ?? { views: 0, engagement: 0, posts: 0 };
10783
+ const previous = previousAgg.get(platform2) ?? { views: 0, engagement: 0, posts: 0 };
10784
+ const viewsChange = pctChange(current.views, previous.views);
10785
+ if (Math.abs(viewsChange) >= threshold) {
10786
+ const isSpike = viewsChange > 0;
10787
+ anomalies.push({
10788
+ type: isSpike ? "spike" : "drop",
10789
+ metric: "views",
10790
+ platform: platform2,
10791
+ magnitude: Math.round(viewsChange * 10) / 10,
10792
+ period,
10793
+ affected_posts: [],
10794
+ confidence: Math.min(1, Math.abs(viewsChange) / 100),
10795
+ suggested_action: isSpike ? `Views up ${Math.abs(Math.round(viewsChange))}% on ${platform2}. Analyze what worked and double down.` : `Views down ${Math.abs(Math.round(viewsChange))}% on ${platform2}. Review content strategy and posting frequency.`
10796
+ });
10797
+ }
10798
+ const engagementChange = pctChange(current.engagement, previous.engagement);
10799
+ if (Math.abs(engagementChange) >= threshold) {
10800
+ const isSpike = engagementChange > 0;
10801
+ anomalies.push({
10802
+ type: isSpike ? "spike" : "drop",
10803
+ metric: "engagement",
10804
+ platform: platform2,
10805
+ magnitude: Math.round(engagementChange * 10) / 10,
10806
+ period,
10807
+ affected_posts: [],
10808
+ confidence: Math.min(1, Math.abs(engagementChange) / 100),
10809
+ suggested_action: isSpike ? `Engagement up ${Math.abs(Math.round(engagementChange))}% on ${platform2}. Replicate this content style.` : `Engagement down ${Math.abs(Math.round(engagementChange))}% on ${platform2}. Test different hooks and CTAs.`
10810
+ });
10811
+ }
10812
+ const avgViews = averageViewsPerPost ?? (previous.posts > 0 ? previous.views / previous.posts : 0);
10813
+ if (avgViews > 0 && current.posts > 0) {
10814
+ const currentAvgViews = current.views / current.posts;
10815
+ if (currentAvgViews > avgViews * VIRAL_MULTIPLIER) {
10816
+ anomalies.push({
10817
+ type: "viral",
10818
+ metric: "views",
10819
+ platform: platform2,
10820
+ magnitude: Math.round(currentAvgViews / avgViews * 100) / 100,
10821
+ period,
10822
+ affected_posts: [],
10823
+ confidence: 0.9,
10824
+ suggested_action: `Viral content detected on ${platform2}! Average views per post is ${Math.round(currentAvgViews / avgViews)}x normal. Engage with comments and create follow-up content.`
10825
+ });
10826
+ }
10827
+ }
10828
+ const prevEngRate = previous.views > 0 ? previous.engagement / previous.views : 0;
10829
+ const currEngRate = current.views > 0 ? current.engagement / current.views : 0;
10830
+ const rateChange = pctChange(currEngRate, prevEngRate);
10831
+ if (Math.abs(rateChange) >= threshold && current.posts >= 2 && previous.posts >= 2) {
10832
+ anomalies.push({
10833
+ type: "trend_shift",
10834
+ metric: "engagement_rate",
10835
+ platform: platform2,
10836
+ magnitude: Math.round(rateChange * 10) / 10,
10837
+ period,
10838
+ affected_posts: [],
10839
+ confidence: Math.min(1, Math.min(current.posts, previous.posts) / 5),
10840
+ suggested_action: rateChange > 0 ? `Engagement rate improving on ${platform2}. Current audience is more responsive.` : `Engagement rate declining on ${platform2} despite views. Content may not be resonating \u2014 test new formats.`
10841
+ });
10842
+ }
10843
+ }
10844
+ anomalies.sort((a, b) => Math.abs(b.magnitude) - Math.abs(a.magnitude));
10845
+ return anomalies;
10846
+ }
10847
+
10848
+ // src/tools/digest.ts
10849
+ function asEnvelope18(data) {
10850
+ return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
10851
+ }
10852
+ var PLATFORM_ENUM3 = z23.enum([
10853
+ "youtube",
10854
+ "tiktok",
10855
+ "instagram",
10856
+ "twitter",
10857
+ "linkedin",
10858
+ "facebook",
10859
+ "threads",
10860
+ "bluesky"
10861
+ ]);
10862
+ function registerDigestTools(server) {
10863
+ server.tool(
10864
+ "generate_performance_digest",
10865
+ "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.",
10866
+ {
10867
+ project_id: z23.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
10868
+ period: z23.enum(["7d", "14d", "30d"]).default("7d").describe("Time period to analyze"),
10869
+ include_recommendations: z23.boolean().default(true),
10870
+ response_format: z23.enum(["text", "json"]).optional()
10871
+ },
10872
+ async ({ project_id, period, include_recommendations, response_format }) => {
10873
+ const format = response_format ?? "text";
10874
+ const startedAt = Date.now();
10875
+ try {
10876
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
10877
+ action: "performance-digest",
10878
+ period,
10879
+ projectId: project_id
10880
+ });
10881
+ if (efError) throw new Error(efError);
10882
+ const currentData = result?.currentData ?? [];
10883
+ const previousData = result?.previousData ?? [];
10884
+ const totalViews = currentData.reduce((sum, d) => sum + (d.views ?? 0), 0);
10885
+ const totalEngagement = currentData.reduce((sum, d) => sum + (d.engagement ?? 0), 0);
10886
+ const postIds = new Set(currentData.map((d) => d.post_id));
10887
+ const totalPosts = postIds.size;
10888
+ const avgEngRate = totalViews > 0 ? totalEngagement / totalViews * 100 : 0;
10889
+ const postMetrics = /* @__PURE__ */ new Map();
10890
+ for (const d of currentData) {
10891
+ const existing = postMetrics.get(d.post_id) ?? {
10892
+ views: 0,
10893
+ engagement: 0,
10894
+ platform: d.platform
10895
+ };
10896
+ existing.views += d.views ?? 0;
10897
+ existing.engagement += d.engagement ?? 0;
10898
+ postMetrics.set(d.post_id, existing);
10899
+ }
10900
+ let best = null;
10901
+ let worst = null;
10902
+ for (const [id, metrics] of postMetrics) {
10903
+ if (!best || metrics.views > best.views) {
10904
+ best = {
10905
+ id,
10906
+ platform: metrics.platform,
10907
+ title: null,
10908
+ views: metrics.views,
10909
+ engagement: metrics.engagement
10910
+ };
10911
+ }
10912
+ if (!worst || metrics.views < worst.views) {
10913
+ worst = {
10914
+ id,
10915
+ platform: metrics.platform,
10916
+ title: null,
10917
+ views: metrics.views,
10918
+ engagement: metrics.engagement
10919
+ };
10920
+ }
10921
+ }
10922
+ const platformMap = /* @__PURE__ */ new Map();
10923
+ for (const d of currentData) {
10924
+ const existing = platformMap.get(d.platform) ?? { posts: 0, views: 0, engagement: 0 };
10925
+ existing.views += d.views ?? 0;
10926
+ existing.engagement += d.engagement ?? 0;
10927
+ platformMap.set(d.platform, existing);
10928
+ }
10929
+ const platformPosts = /* @__PURE__ */ new Map();
10930
+ for (const d of currentData) {
10931
+ if (!platformPosts.has(d.platform)) platformPosts.set(d.platform, /* @__PURE__ */ new Set());
10932
+ platformPosts.get(d.platform).add(d.post_id);
10933
+ }
10934
+ for (const [platform2, postSet] of platformPosts) {
10935
+ const existing = platformMap.get(platform2);
10936
+ if (existing) existing.posts = postSet.size;
10937
+ }
10938
+ const platformBreakdown = [...platformMap.entries()].map(([platform2, m]) => ({
10939
+ platform: platform2,
10940
+ ...m
10941
+ }));
10942
+ const periodMap = { "7d": 7, "14d": 14, "30d": 30 };
10943
+ const periodDays = periodMap[period] ?? 7;
10944
+ const now = /* @__PURE__ */ new Date();
10945
+ const currentStart = new Date(now);
10946
+ currentStart.setDate(currentStart.getDate() - periodDays);
10947
+ const prevViews = previousData.reduce((sum, d) => sum + (d.views ?? 0), 0);
10948
+ const prevEngagement = previousData.reduce((sum, d) => sum + (d.engagement ?? 0), 0);
10949
+ const viewsChangePct = prevViews > 0 ? (totalViews - prevViews) / prevViews * 100 : totalViews > 0 ? 100 : 0;
10950
+ const engChangePct = prevEngagement > 0 ? (totalEngagement - prevEngagement) / prevEngagement * 100 : totalEngagement > 0 ? 100 : 0;
10951
+ const recommendations = [];
10952
+ if (include_recommendations) {
10953
+ if (viewsChangePct < -10) {
10954
+ recommendations.push("Views declining \u2014 experiment with new hooks and posting times.");
10955
+ }
10956
+ if (avgEngRate < 2) {
10957
+ recommendations.push(
10958
+ "Engagement rate below 2% \u2014 try more interactive content (questions, polls, CTAs)."
10959
+ );
10960
+ }
10961
+ if (totalPosts < periodDays / 2) {
10962
+ recommendations.push(
10963
+ `Only ${totalPosts} posts in ${periodDays} days \u2014 increase posting frequency.`
10964
+ );
10965
+ }
10966
+ if (platformBreakdown.length === 1) {
10967
+ recommendations.push(
10968
+ "Only posting on one platform \u2014 diversify to reach new audiences."
10969
+ );
10970
+ }
10971
+ if (viewsChangePct > 20) {
10972
+ recommendations.push(
10973
+ "Views growing well! Analyze top performers and replicate those patterns."
10974
+ );
10975
+ }
10976
+ if (engChangePct > 20 && viewsChangePct > 0) {
10977
+ recommendations.push(
10978
+ "Both views and engagement growing \u2014 current strategy is working."
10979
+ );
10980
+ }
10981
+ if (recommendations.length === 0) {
10982
+ recommendations.push(
10983
+ "Performance is stable. Continue current strategy and monitor weekly."
10984
+ );
10985
+ }
10986
+ }
10987
+ const digest = {
10988
+ period,
10989
+ period_start: currentStart.toISOString().split("T")[0],
10990
+ period_end: now.toISOString().split("T")[0],
10991
+ metrics: {
10992
+ total_posts: totalPosts,
10993
+ total_views: totalViews,
10994
+ total_engagement: totalEngagement,
10995
+ avg_engagement_rate: Math.round(avgEngRate * 100) / 100,
10996
+ best_performing: best,
10997
+ worst_performing: worst,
10998
+ platform_breakdown: platformBreakdown
10999
+ },
11000
+ trends: {
11001
+ views: {
11002
+ direction: viewsChangePct > 5 ? "up" : viewsChangePct < -5 ? "down" : "flat",
11003
+ change_pct: Math.round(viewsChangePct * 10) / 10
11004
+ },
11005
+ engagement: {
11006
+ direction: engChangePct > 5 ? "up" : engChangePct < -5 ? "down" : "flat",
11007
+ change_pct: Math.round(engChangePct * 10) / 10
11008
+ }
11009
+ },
11010
+ recommendations,
11011
+ winning_patterns: {
11012
+ hook_types: [],
11013
+ content_formats: [],
11014
+ posting_times: []
11015
+ }
11016
+ };
11017
+ const durationMs = Date.now() - startedAt;
11018
+ logMcpToolInvocation({
11019
+ toolName: "generate_performance_digest",
11020
+ status: "success",
11021
+ durationMs,
11022
+ details: { period, posts: totalPosts, views: totalViews }
11023
+ });
11024
+ if (format === "json") {
11025
+ return {
11026
+ content: [{ type: "text", text: JSON.stringify(asEnvelope18(digest), null, 2) }]
11027
+ };
11028
+ }
11029
+ const lines = [];
11030
+ lines.push(`Performance Digest (${period})`);
11031
+ lines.push(`Period: ${digest.period_start} to ${digest.period_end}`);
11032
+ lines.push("=".repeat(40));
11033
+ lines.push(`Posts: ${totalPosts}`);
11034
+ lines.push(
11035
+ `Views: ${totalViews.toLocaleString()} (${viewsChangePct >= 0 ? "+" : ""}${Math.round(viewsChangePct)}% vs prev period)`
11036
+ );
11037
+ lines.push(
11038
+ `Engagement: ${totalEngagement.toLocaleString()} (${engChangePct >= 0 ? "+" : ""}${Math.round(engChangePct)}% vs prev period)`
11039
+ );
11040
+ lines.push(`Avg Engagement Rate: ${digest.metrics.avg_engagement_rate}%`);
11041
+ if (best) {
11042
+ lines.push(
11043
+ `
11044
+ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleString()} views`
11045
+ );
11046
+ }
11047
+ if (worst && totalPosts > 1) {
11048
+ lines.push(
11049
+ `Worst: ${worst.id.slice(0, 8)}... (${worst.platform}) \u2014 ${worst.views.toLocaleString()} views`
11050
+ );
11051
+ }
11052
+ if (platformBreakdown.length > 0) {
11053
+ lines.push("\nPlatform Breakdown:");
11054
+ for (const p of platformBreakdown) {
11055
+ lines.push(
11056
+ ` ${p.platform}: ${p.posts} posts, ${p.views.toLocaleString()} views, ${p.engagement.toLocaleString()} engagement`
11057
+ );
11058
+ }
11059
+ }
11060
+ if (recommendations.length > 0) {
11061
+ lines.push("\nRecommendations:");
11062
+ for (const r of recommendations) lines.push(` \u2022 ${r}`);
11063
+ }
11064
+ return { content: [{ type: "text", text: lines.join("\n") }] };
11065
+ } catch (err) {
11066
+ const durationMs = Date.now() - startedAt;
11067
+ const message = err instanceof Error ? err.message : String(err);
11068
+ logMcpToolInvocation({
11069
+ toolName: "generate_performance_digest",
11070
+ status: "error",
11071
+ durationMs,
11072
+ details: { error: message }
11073
+ });
11074
+ return {
11075
+ content: [{ type: "text", text: `Digest failed: ${message}` }],
11076
+ isError: true
11077
+ };
11078
+ }
11079
+ }
11080
+ );
11081
+ server.tool(
11082
+ "detect_anomalies",
11083
+ "Detect significant performance changes: spikes, drops, viral content, trend shifts. Compares current period against previous equal-length period. No AI call, no credit cost.",
11084
+ {
11085
+ project_id: z23.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
11086
+ days: z23.number().min(7).max(90).default(14).describe("Days to analyze"),
11087
+ sensitivity: z23.enum(["low", "medium", "high"]).default("medium").describe("Detection sensitivity: low=50%+, medium=30%+, high=15%+ changes"),
11088
+ platforms: z23.array(PLATFORM_ENUM3).optional().describe("Filter to specific platforms"),
11089
+ response_format: z23.enum(["text", "json"]).optional()
11090
+ },
11091
+ async ({ project_id, days, sensitivity, platforms, response_format }) => {
11092
+ const format = response_format ?? "text";
11093
+ const startedAt = Date.now();
11094
+ try {
11095
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
11096
+ action: "detect-anomalies",
11097
+ days,
11098
+ platforms,
11099
+ projectId: project_id
11100
+ });
11101
+ if (efError) throw new Error(efError);
11102
+ const toMetricData = (data) => {
11103
+ const dayMap = /* @__PURE__ */ new Map();
11104
+ for (const d of data) {
11105
+ const date = d.captured_at.split("T")[0];
11106
+ const key = `${date}-${d.platform}`;
11107
+ const existing = dayMap.get(key) ?? {
11108
+ date,
11109
+ platform: d.platform,
11110
+ views: 0,
11111
+ engagement: 0,
11112
+ posts: 0
11113
+ };
11114
+ existing.views += d.views ?? 0;
11115
+ existing.engagement += d.engagement ?? 0;
11116
+ existing.posts += 1;
11117
+ dayMap.set(key, existing);
11118
+ }
11119
+ return [...dayMap.values()];
11120
+ };
11121
+ const currentMetrics = toMetricData(result?.currentData ?? []);
11122
+ const previousMetrics = toMetricData(result?.previousData ?? []);
11123
+ const anomalies = detectAnomalies(
11124
+ currentMetrics,
11125
+ previousMetrics,
11126
+ sensitivity
11127
+ );
11128
+ const durationMs = Date.now() - startedAt;
11129
+ logMcpToolInvocation({
11130
+ toolName: "detect_anomalies",
11131
+ status: "success",
11132
+ durationMs,
11133
+ details: { days, sensitivity, anomalies_found: anomalies.length }
11134
+ });
11135
+ 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.`;
11136
+ const resultPayload = { anomalies, summary };
11137
+ if (format === "json") {
11138
+ return {
11139
+ content: [
11140
+ { type: "text", text: JSON.stringify(asEnvelope18(resultPayload), null, 2) }
11141
+ ]
11142
+ };
11143
+ }
11144
+ const lines = [];
11145
+ lines.push(`Anomaly Detection (${days} days, ${sensitivity} sensitivity)`);
11146
+ lines.push("=".repeat(40));
11147
+ lines.push(summary);
11148
+ if (anomalies.length > 0) {
11149
+ lines.push("");
11150
+ for (let i = 0; i < anomalies.length; i++) {
11151
+ const a = anomalies[i];
11152
+ const arrow = a.magnitude > 0 ? "\u2191" : "\u2193";
11153
+ const magnitudeStr = a.type === "viral" ? `${a.magnitude}x average` : `${Math.abs(a.magnitude)}% change`;
11154
+ lines.push(`${i + 1}. [${a.type.toUpperCase()}] ${a.metric} on ${a.platform}`);
11155
+ lines.push(
11156
+ ` ${arrow} ${magnitudeStr} | Confidence: ${Math.round(a.confidence * 100)}%`
11157
+ );
11158
+ lines.push(` \u2192 ${a.suggested_action}`);
11159
+ }
11160
+ }
11161
+ return { content: [{ type: "text", text: lines.join("\n") }] };
11162
+ } catch (err) {
11163
+ const durationMs = Date.now() - startedAt;
11164
+ const message = err instanceof Error ? err.message : String(err);
11165
+ logMcpToolInvocation({
11166
+ toolName: "detect_anomalies",
11167
+ status: "error",
11168
+ durationMs,
11169
+ details: { error: message }
11170
+ });
11171
+ return {
11172
+ content: [{ type: "text", text: `Anomaly detection failed: ${message}` }],
11173
+ isError: true
11174
+ };
11175
+ }
11176
+ }
11177
+ );
11178
+ }
11179
+
11180
+ // src/tools/brandRuntime.ts
11181
+ import { z as z24 } from "zod";
11182
+ init_supabase();
11183
+ function asEnvelope19(data) {
11184
+ return {
11185
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
11186
+ data
11187
+ };
11188
+ }
11189
+ function registerBrandRuntimeTools(server) {
9232
11190
  server.tool(
9233
- "search_tools",
9234
- 'Search available tools by name, description, module, or scope. Use "name" detail (~50 tokens) for quick lookup, "summary" (~500 tokens) for descriptions, "full" for complete input schemas. Start here if unsure which tool to call \u2014 filter by module (e.g. "planning", "content", "analytics") to narrow results.',
11191
+ "get_brand_runtime",
11192
+ "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.",
9235
11193
  {
9236
- query: z20.string().optional().describe("Search query to filter tools by name or description"),
9237
- module: z20.string().optional().describe('Filter by module name (e.g. "planning", "content", "analytics")'),
9238
- scope: z20.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
9239
- detail: z20.enum(["name", "summary", "full"]).default("summary").describe(
9240
- 'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
9241
- )
11194
+ project_id: z24.string().optional().describe("Project ID. Defaults to current project.")
9242
11195
  },
11196
+ async ({ project_id }) => {
11197
+ const projectId = project_id || await getDefaultProjectId();
11198
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", { action: "brand-profile", projectId });
11199
+ if (efError || !result?.success) {
11200
+ return {
11201
+ content: [
11202
+ {
11203
+ type: "text",
11204
+ text: `Error: ${efError || result?.error || "Failed to fetch brand profile"}`
11205
+ }
11206
+ ],
11207
+ isError: true
11208
+ };
11209
+ }
11210
+ const data = result.profile;
11211
+ if (!data?.profile_data) {
11212
+ return {
11213
+ content: [
11214
+ {
11215
+ type: "text",
11216
+ text: "No brand profile found for this project. Use extract_brand to create one."
11217
+ }
11218
+ ]
11219
+ };
11220
+ }
11221
+ const profile = data.profile_data;
11222
+ const meta = data.extraction_metadata || {};
11223
+ const runtime = {
11224
+ name: profile.name || "",
11225
+ industry: profile.industryClassification || "",
11226
+ positioning: profile.competitivePositioning || "",
11227
+ messaging: {
11228
+ valuePropositions: profile.valuePropositions || [],
11229
+ messagingPillars: profile.messagingPillars || [],
11230
+ contentPillars: (profile.contentPillars || []).map(
11231
+ (p) => `${p.name} (${Math.round(p.weight * 100)}%)`
11232
+ ),
11233
+ socialProof: profile.socialProof || { testimonials: [], awards: [], pressMentions: [] }
11234
+ },
11235
+ voice: {
11236
+ tone: profile.voiceProfile?.tone || [],
11237
+ style: profile.voiceProfile?.style || [],
11238
+ avoidPatterns: profile.voiceProfile?.avoidPatterns || [],
11239
+ preferredTerms: profile.vocabularyRules?.preferredTerms || [],
11240
+ bannedTerms: profile.vocabularyRules?.bannedTerms || []
11241
+ },
11242
+ visual: {
11243
+ colorPalette: profile.colorPalette || {},
11244
+ logoUrl: profile.logoUrl || null,
11245
+ referenceFrameUrl: data.default_style_ref_url || null
11246
+ },
11247
+ audience: profile.targetAudience || {},
11248
+ confidence: {
11249
+ overall: meta.overallConfidence || 0,
11250
+ provider: meta.scrapingProvider || "unknown",
11251
+ pagesScraped: meta.pagesScraped || 0
11252
+ }
11253
+ };
11254
+ const envelope = asEnvelope19(runtime);
11255
+ return {
11256
+ content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
11257
+ };
11258
+ }
11259
+ );
11260
+ server.tool(
11261
+ "explain_brand_system",
11262
+ "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.",
9243
11263
  {
9244
- title: "Search Tools",
9245
- readOnlyHint: true,
9246
- destructiveHint: false,
9247
- idempotentHint: true,
9248
- openWorldHint: false
11264
+ project_id: z24.string().optional().describe("Project ID. Defaults to current project.")
9249
11265
  },
9250
- async ({ query, module, scope, detail }) => {
9251
- let results = [...TOOL_CATALOG];
9252
- if (query) {
9253
- results = searchTools(query);
11266
+ async ({ project_id }) => {
11267
+ const projectId = project_id || await getDefaultProjectId();
11268
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", { action: "brand-profile", projectId });
11269
+ if (efError || !result?.success) {
11270
+ return {
11271
+ content: [
11272
+ { type: "text", text: "No brand profile found. Run extract_brand first." }
11273
+ ]
11274
+ };
9254
11275
  }
9255
- if (module) {
9256
- const moduleTools = getToolsByModule(module);
9257
- results = results.filter((t) => moduleTools.some((mt) => mt.name === t.name));
11276
+ const row = result.profile;
11277
+ if (!row?.profile_data) {
11278
+ return {
11279
+ content: [
11280
+ { type: "text", text: "No brand profile found. Run extract_brand first." }
11281
+ ]
11282
+ };
9258
11283
  }
9259
- if (scope) {
9260
- const scopeTools = getToolsByScope(scope);
9261
- results = results.filter((t) => scopeTools.some((st) => st.name === t.name));
11284
+ const p = row.profile_data;
11285
+ const meta = row.extraction_metadata || {};
11286
+ const sections = [
11287
+ {
11288
+ name: "Identity",
11289
+ fields: [p.name, p.tagline, p.industryClassification, p.competitivePositioning],
11290
+ total: 4
11291
+ },
11292
+ {
11293
+ name: "Voice",
11294
+ fields: [
11295
+ p.voiceProfile?.tone?.length,
11296
+ p.voiceProfile?.style?.length,
11297
+ p.voiceProfile?.avoidPatterns?.length
11298
+ ],
11299
+ total: 3
11300
+ },
11301
+ {
11302
+ name: "Audience",
11303
+ fields: [
11304
+ p.targetAudience?.demographics?.ageRange,
11305
+ p.targetAudience?.psychographics?.painPoints?.length
11306
+ ],
11307
+ total: 2
11308
+ },
11309
+ {
11310
+ name: "Messaging",
11311
+ fields: [
11312
+ p.valuePropositions?.length,
11313
+ p.messagingPillars?.length,
11314
+ p.contentPillars?.length
11315
+ ],
11316
+ total: 3
11317
+ },
11318
+ {
11319
+ name: "Visual",
11320
+ fields: [
11321
+ p.logoUrl,
11322
+ p.colorPalette?.primary !== "#000000" ? p.colorPalette?.primary : null,
11323
+ p.typography
11324
+ ],
11325
+ total: 3
11326
+ },
11327
+ {
11328
+ name: "Vocabulary",
11329
+ fields: [
11330
+ p.vocabularyRules?.preferredTerms?.length,
11331
+ p.vocabularyRules?.bannedTerms?.length
11332
+ ],
11333
+ total: 2
11334
+ },
11335
+ {
11336
+ name: "Video Rules",
11337
+ fields: [p.videoBrandRules?.pacing, p.videoBrandRules?.colorGrading],
11338
+ total: 2
11339
+ }
11340
+ ];
11341
+ const lines = [`Brand System Report: ${p.name || "Unknown"}`, ""];
11342
+ for (const section of sections) {
11343
+ const filled = section.fields.filter((f) => f != null && f !== "" && f !== 0).length;
11344
+ const pct = Math.round(filled / section.total * 100);
11345
+ const icon = pct >= 80 ? "OK" : pct >= 50 ? "PARTIAL" : "MISSING";
11346
+ lines.push(`[${icon}] ${section.name}: ${filled}/${section.total} (${pct}%)`);
9262
11347
  }
9263
- let output;
9264
- switch (detail) {
9265
- case "name":
9266
- output = results.map((t) => t.name);
9267
- break;
9268
- case "summary":
9269
- output = results.map((t) => ({ name: t.name, description: t.description }));
9270
- break;
9271
- case "full":
9272
- default:
9273
- output = results;
9274
- break;
11348
+ lines.push("");
11349
+ lines.push(`Extraction confidence: ${Math.round((meta.overallConfidence || 0) * 100)}%`);
11350
+ lines.push(
11351
+ `Scraping: ${meta.pagesScraped || 0} pages via ${meta.scrapingProvider || "unknown"}`
11352
+ );
11353
+ const recs = [];
11354
+ if (!p.contentPillars?.length) recs.push("Add content pillars for focused ideation");
11355
+ if (!p.vocabularyRules?.preferredTerms?.length)
11356
+ recs.push("Add preferred terms for vocabulary consistency");
11357
+ if (!p.videoBrandRules?.pacing)
11358
+ recs.push("Add video brand rules (pacing, color grading) for storyboard consistency");
11359
+ if (!p.logoUrl) recs.push("Upload a logo for deterministic brand overlay");
11360
+ if ((meta.overallConfidence || 0) < 0.6)
11361
+ recs.push("Re-extract with premium mode for higher confidence");
11362
+ if (recs.length > 0) {
11363
+ lines.push("");
11364
+ lines.push("Recommendations:");
11365
+ recs.forEach((r) => lines.push(` - ${r}`));
9275
11366
  }
9276
11367
  return {
9277
- content: [
9278
- {
9279
- type: "text",
9280
- text: JSON.stringify({ toolCount: results.length, tools: output }, null, 2)
9281
- }
9282
- ]
11368
+ content: [{ type: "text", text: lines.join("\n") }]
11369
+ };
11370
+ }
11371
+ );
11372
+ server.tool(
11373
+ "check_brand_consistency",
11374
+ "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.",
11375
+ {
11376
+ content: z24.string().describe("The content text to check for brand consistency."),
11377
+ project_id: z24.string().optional().describe("Project ID. Defaults to current project.")
11378
+ },
11379
+ async ({ content, project_id }) => {
11380
+ const projectId = project_id || await getDefaultProjectId();
11381
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", { action: "brand-profile", projectId });
11382
+ const row = !efError && result?.success ? result.profile : null;
11383
+ if (!row?.profile_data) {
11384
+ return {
11385
+ content: [
11386
+ {
11387
+ type: "text",
11388
+ text: "No brand profile found. Cannot check consistency without brand data."
11389
+ }
11390
+ ],
11391
+ isError: true
11392
+ };
11393
+ }
11394
+ const profile = row.profile_data;
11395
+ const contentLower = content.toLowerCase();
11396
+ const issues = [];
11397
+ let score = 70;
11398
+ const banned = profile.vocabularyRules?.bannedTerms || [];
11399
+ const bannedFound = banned.filter((t) => contentLower.includes(t.toLowerCase()));
11400
+ if (bannedFound.length > 0) {
11401
+ score -= bannedFound.length * 15;
11402
+ issues.push(`Banned terms found: ${bannedFound.join(", ")}`);
11403
+ }
11404
+ const avoid = profile.voiceProfile?.avoidPatterns || [];
11405
+ const avoidFound = avoid.filter((p) => contentLower.includes(p.toLowerCase()));
11406
+ if (avoidFound.length > 0) {
11407
+ score -= avoidFound.length * 10;
11408
+ issues.push(`Avoid patterns found: ${avoidFound.join(", ")}`);
11409
+ }
11410
+ const preferred = profile.vocabularyRules?.preferredTerms || [];
11411
+ const prefUsed = preferred.filter((t) => contentLower.includes(t.toLowerCase()));
11412
+ score += Math.min(15, prefUsed.length * 5);
11413
+ const fabPatterns = [
11414
+ { regex: /\b\d+[,.]?\d*\s*(%|percent)/gi, label: "unverified percentage" },
11415
+ { regex: /\b(award[- ]?winning|best[- ]selling|#\s*1)\b/gi, label: "unverified ranking" },
11416
+ { regex: /\b(guaranteed|proven to|studies show)\b/gi, label: "unverified claim" }
11417
+ ];
11418
+ for (const { regex, label } of fabPatterns) {
11419
+ regex.lastIndex = 0;
11420
+ if (regex.test(content)) {
11421
+ score -= 10;
11422
+ issues.push(`Potential ${label} detected`);
11423
+ }
11424
+ }
11425
+ score = Math.max(0, Math.min(100, score));
11426
+ const checkResult = {
11427
+ score,
11428
+ passed: score >= 60,
11429
+ issues,
11430
+ preferredTermsUsed: prefUsed,
11431
+ bannedTermsFound: bannedFound
11432
+ };
11433
+ const envelope = asEnvelope19(checkResult);
11434
+ return {
11435
+ content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
9283
11436
  };
9284
11437
  }
9285
11438
  );
@@ -9320,12 +11473,46 @@ function applyScopeEnforcement(server, scopeResolver) {
9320
11473
  isError: true
9321
11474
  };
9322
11475
  }
9323
- return originalHandler(...handlerArgs);
11476
+ const result = await originalHandler(...handlerArgs);
11477
+ return truncateResponse(result);
9324
11478
  };
9325
11479
  }
9326
11480
  return originalTool(...args);
9327
11481
  };
9328
11482
  }
11483
+ var RESPONSE_CHAR_LIMIT = 1e5;
11484
+ function truncateResponse(result) {
11485
+ if (!result?.content || !Array.isArray(result.content)) return result;
11486
+ let totalChars = 0;
11487
+ for (const part of result.content) {
11488
+ if (part.type === "text" && typeof part.text === "string") {
11489
+ totalChars += part.text.length;
11490
+ }
11491
+ }
11492
+ if (totalChars <= RESPONSE_CHAR_LIMIT) return result;
11493
+ let remaining = RESPONSE_CHAR_LIMIT;
11494
+ const truncated = [];
11495
+ for (const part of result.content) {
11496
+ if (part.type === "text" && typeof part.text === "string") {
11497
+ if (remaining <= 0) continue;
11498
+ if (part.text.length <= remaining) {
11499
+ truncated.push(part);
11500
+ remaining -= part.text.length;
11501
+ } else {
11502
+ truncated.push({
11503
+ ...part,
11504
+ text: part.text.slice(0, remaining) + `
11505
+
11506
+ [Response truncated: ${totalChars.toLocaleString()} chars exceeded ${RESPONSE_CHAR_LIMIT.toLocaleString()} limit. Use filters to narrow your query.]`
11507
+ });
11508
+ remaining = 0;
11509
+ }
11510
+ } else {
11511
+ truncated.push(part);
11512
+ }
11513
+ }
11514
+ return { ...result, content: truncated };
11515
+ }
9329
11516
  function registerAllTools(server, options) {
9330
11517
  registerIdeationTools(server);
9331
11518
  registerContentTools(server);
@@ -9349,6 +11536,11 @@ function registerAllTools(server, options) {
9349
11536
  registerPlanningTools(server);
9350
11537
  registerPlanApprovalTools(server);
9351
11538
  registerDiscoveryTools(server);
11539
+ registerPipelineTools(server);
11540
+ registerSuggestTools(server);
11541
+ registerDigestTools(server);
11542
+ registerBrandRuntimeTools(server);
11543
+ applyAnnotations(server);
9352
11544
  }
9353
11545
 
9354
11546
  // src/http.ts
@@ -9449,7 +11641,7 @@ async function verifyApiKey(apiKey, supabaseUrl, supabaseAnonKey) {
9449
11641
  clientId: "api-key",
9450
11642
  scopes: data.scopes ?? ["mcp:read"],
9451
11643
  expiresAt,
9452
- extra: { userId: data.userId, email: data.email }
11644
+ extra: { userId: data.userId }
9453
11645
  };
9454
11646
  } catch (err) {
9455
11647
  clearTimeout(timer);
@@ -10521,7 +12713,7 @@ var cleanupInterval = setInterval(
10521
12713
  );
10522
12714
  var app = express();
10523
12715
  app.disable("x-powered-by");
10524
- app.use(express.json());
12716
+ app.use(express.json({ limit: "50kb" }));
10525
12717
  app.set("trust proxy", 1);
10526
12718
  var ipBuckets = /* @__PURE__ */ new Map();
10527
12719
  var IP_RATE_MAX = 60;
@@ -10534,7 +12726,8 @@ setInterval(() => {
10534
12726
  }
10535
12727
  }, IP_RATE_CLEANUP_INTERVAL).unref();
10536
12728
  app.use((req, res, next) => {
10537
- if (req.path === "/health") return next();
12729
+ if (req.path === "/health" || req.path === "/.well-known/mcp/server-card.json" || req.path === "/.well-known/oauth-protected-resource")
12730
+ return next();
10538
12731
  const ip = req.ip ?? req.socket.remoteAddress ?? "unknown";
10539
12732
  const now = Date.now();
10540
12733
  let bucket = ipBuckets.get(ip);
@@ -10626,13 +12819,217 @@ async function authenticateRequest(req, res, next) {
10626
12819
  };
10627
12820
  next();
10628
12821
  } catch (err) {
10629
- const message = err instanceof Error ? err.message : "Token verification failed";
12822
+ const message = sanitizeError(err);
10630
12823
  res.status(401).json({
10631
12824
  error: "invalid_token",
10632
12825
  error_description: message
10633
12826
  });
10634
12827
  }
10635
12828
  }
12829
+ app.get("/.well-known/mcp/server-card.json", (_req, res) => {
12830
+ res.setHeader("Cache-Control", "public, max-age=3600");
12831
+ res.json({
12832
+ serverInfo: {
12833
+ name: "socialneuron",
12834
+ version: MCP_VERSION
12835
+ },
12836
+ authentication: {
12837
+ required: true,
12838
+ schemes: ["oauth2"]
12839
+ },
12840
+ tools: [
12841
+ {
12842
+ name: "generate_content",
12843
+ description: "Create a script, caption, hook, or blog post tailored to a specific platform.",
12844
+ inputSchema: {
12845
+ type: "object",
12846
+ properties: {
12847
+ prompt: { type: "string" },
12848
+ platform: { type: "string" }
12849
+ },
12850
+ required: ["prompt"]
12851
+ }
12852
+ },
12853
+ {
12854
+ name: "schedule_post",
12855
+ description: "Publish or schedule a post to connected social platforms.",
12856
+ inputSchema: {
12857
+ type: "object",
12858
+ properties: {
12859
+ content: { type: "string" },
12860
+ platform: { type: "string" }
12861
+ },
12862
+ required: ["content", "platform"]
12863
+ }
12864
+ },
12865
+ {
12866
+ name: "fetch_analytics",
12867
+ description: "Get post performance metrics for connected platforms.",
12868
+ inputSchema: {
12869
+ type: "object",
12870
+ properties: { platform: { type: "string" } }
12871
+ }
12872
+ },
12873
+ {
12874
+ name: "extract_brand",
12875
+ description: "Analyze a website URL and extract brand identity data.",
12876
+ inputSchema: {
12877
+ type: "object",
12878
+ properties: { url: { type: "string" } },
12879
+ required: ["url"]
12880
+ }
12881
+ },
12882
+ {
12883
+ name: "plan_content_week",
12884
+ description: "Generate a full week content plan with platform-specific drafts.",
12885
+ inputSchema: {
12886
+ type: "object",
12887
+ properties: { niche: { type: "string" } },
12888
+ required: ["niche"]
12889
+ }
12890
+ },
12891
+ {
12892
+ name: "generate_video",
12893
+ description: "Start an async AI video generation job.",
12894
+ inputSchema: {
12895
+ type: "object",
12896
+ properties: { prompt: { type: "string" } },
12897
+ required: ["prompt"]
12898
+ }
12899
+ },
12900
+ {
12901
+ name: "generate_image",
12902
+ description: "Start an async AI image generation job.",
12903
+ inputSchema: {
12904
+ type: "object",
12905
+ properties: { prompt: { type: "string" } },
12906
+ required: ["prompt"]
12907
+ }
12908
+ },
12909
+ {
12910
+ name: "adapt_content",
12911
+ description: "Rewrite existing content for a different platform.",
12912
+ inputSchema: {
12913
+ type: "object",
12914
+ properties: {
12915
+ content: { type: "string" },
12916
+ target_platform: { type: "string" }
12917
+ },
12918
+ required: ["content", "target_platform"]
12919
+ }
12920
+ },
12921
+ {
12922
+ name: "quality_check",
12923
+ description: "Score post quality across 7 categories (0-100).",
12924
+ inputSchema: {
12925
+ type: "object",
12926
+ properties: { content: { type: "string" } },
12927
+ required: ["content"]
12928
+ }
12929
+ },
12930
+ {
12931
+ name: "run_content_pipeline",
12932
+ description: "Full pipeline: trends \u2192 plan \u2192 quality check \u2192 schedule.",
12933
+ inputSchema: {
12934
+ type: "object",
12935
+ properties: { niche: { type: "string" } },
12936
+ required: ["niche"]
12937
+ }
12938
+ },
12939
+ {
12940
+ name: "fetch_trends",
12941
+ description: "Get current trending topics for content inspiration.",
12942
+ inputSchema: {
12943
+ type: "object",
12944
+ properties: { platform: { type: "string" } }
12945
+ }
12946
+ },
12947
+ {
12948
+ name: "get_credit_balance",
12949
+ description: "Check remaining credits and plan tier.",
12950
+ inputSchema: { type: "object", properties: {} }
12951
+ },
12952
+ {
12953
+ name: "list_connected_accounts",
12954
+ description: "Check which social platforms have active OAuth connections.",
12955
+ inputSchema: { type: "object", properties: {} }
12956
+ },
12957
+ {
12958
+ name: "get_brand_profile",
12959
+ description: "Load the active brand voice profile.",
12960
+ inputSchema: { type: "object", properties: {} }
12961
+ },
12962
+ {
12963
+ name: "list_comments",
12964
+ description: "List YouTube comments for moderation.",
12965
+ inputSchema: { type: "object", properties: {} }
12966
+ },
12967
+ {
12968
+ name: "reply_to_comment",
12969
+ description: "Reply to a YouTube comment.",
12970
+ inputSchema: {
12971
+ type: "object",
12972
+ properties: {
12973
+ comment_id: { type: "string" },
12974
+ text: { type: "string" }
12975
+ },
12976
+ required: ["comment_id", "text"]
12977
+ }
12978
+ }
12979
+ ],
12980
+ prompts: [
12981
+ {
12982
+ name: "create_weekly_content_plan",
12983
+ description: "Generate a full week of social media content with structured plan."
12984
+ },
12985
+ {
12986
+ name: "analyze_top_content",
12987
+ description: "Analyze best-performing posts to identify patterns and replicate success."
12988
+ },
12989
+ {
12990
+ name: "repurpose_content",
12991
+ description: "Transform one piece of content into 8-10 pieces across platforms."
12992
+ },
12993
+ {
12994
+ name: "setup_brand_voice",
12995
+ description: "Define or refine brand voice profile for consistent content."
12996
+ },
12997
+ {
12998
+ name: "run_content_audit",
12999
+ description: "Audit recent content performance with prioritized action plan."
13000
+ }
13001
+ ],
13002
+ resources: [
13003
+ {
13004
+ uri: "socialneuron://brand/profile",
13005
+ name: "brand-profile",
13006
+ description: "Brand voice profile with personality traits, audience, tone, and content pillars."
13007
+ },
13008
+ {
13009
+ uri: "socialneuron://account/overview",
13010
+ name: "account-overview",
13011
+ description: "Account status including plan tier, credits, and feature access."
13012
+ },
13013
+ {
13014
+ uri: "socialneuron://docs/capabilities",
13015
+ name: "platform-capabilities",
13016
+ description: "Complete reference of all capabilities, platforms, AI models, and credit costs."
13017
+ },
13018
+ {
13019
+ uri: "socialneuron://docs/getting-started",
13020
+ name: "getting-started",
13021
+ description: "Quick start guide for using Social Neuron with AI agents."
13022
+ }
13023
+ ]
13024
+ });
13025
+ });
13026
+ app.get("/config", (_req, res) => {
13027
+ res.setHeader("Cache-Control", "public, max-age=3600");
13028
+ res.json({
13029
+ supabaseUrl: SUPABASE_URL2,
13030
+ anonKey: SUPABASE_ANON_KEY
13031
+ });
13032
+ });
10636
13033
  app.get("/health", (_req, res) => {
10637
13034
  res.json({ status: "ok", version: MCP_VERSION });
10638
13035
  });
@@ -10711,7 +13108,7 @@ app.post(
10711
13108
  applyScopeEnforcement(server, () => getRequestScopes() ?? auth.scopes);
10712
13109
  registerAllTools(server, { skipScreenshots: true });
10713
13110
  const transport = new StreamableHTTPServerTransport({
10714
- sessionIdGenerator: () => randomUUID3(),
13111
+ sessionIdGenerator: () => randomUUID4(),
10715
13112
  onsessioninitialized: (sessionId) => {
10716
13113
  sessions.set(sessionId, {
10717
13114
  transport,
@@ -10737,9 +13134,10 @@ app.post(
10737
13134
  () => transport.handleRequest(req, res, req.body)
10738
13135
  );
10739
13136
  } catch (err) {
10740
- const message = err instanceof Error ? err.message : "Internal server error";
10741
- console.error(`[MCP HTTP] POST /mcp error: ${message}`);
13137
+ const rawMessage = err instanceof Error ? err.message : "Internal server error";
13138
+ console.error(`[MCP HTTP] POST /mcp error: ${rawMessage}`);
10742
13139
  if (!res.headersSent) {
13140
+ const message = sanitizeError(err);
10743
13141
  res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message } });
10744
13142
  }
10745
13143
  }