@socialneuron/mcp-server 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/http.js +2537 -148
  2. package/dist/index.js +2843 -160
  3. package/package.json +2 -2
package/dist/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,12 +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
345
  getAuthenticatedExpiresAt: () => getAuthenticatedExpiresAt,
347
346
  getAuthenticatedScopes: () => getAuthenticatedScopes,
347
+ getCloudAnonKey: () => getCloudAnonKey,
348
348
  getDefaultProjectId: () => getDefaultProjectId,
349
349
  getDefaultUserId: () => getDefaultUserId,
350
350
  getMcpRunId: () => getMcpRunId,
@@ -369,11 +369,47 @@ function getSupabaseClient() {
369
369
  }
370
370
  return client2;
371
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
+ }
372
397
  function getSupabaseUrl() {
373
398
  if (SUPABASE_URL) return SUPABASE_URL;
374
399
  const cloudOverride = process.env.SOCIALNEURON_CLOUD_SUPABASE_URL;
375
400
  if (cloudOverride) return cloudOverride;
376
- 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
+ );
377
413
  }
378
414
  function getServiceKey() {
379
415
  if (!SUPABASE_SERVICE_KEY) {
@@ -420,6 +456,12 @@ async function getDefaultProjectId() {
420
456
  return null;
421
457
  }
422
458
  async function initializeAuth() {
459
+ if (!SUPABASE_URL) {
460
+ try {
461
+ await fetchCloudConfig();
462
+ } catch {
463
+ }
464
+ }
423
465
  const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
424
466
  const apiKey = await loadApiKey2();
425
467
  if (apiKey) {
@@ -523,7 +565,7 @@ async function logMcpToolInvocation(args) {
523
565
  captureToolEvent(args).catch(() => {
524
566
  });
525
567
  }
526
- var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, 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;
527
569
  var init_supabase = __esm({
528
570
  "src/lib/supabase.ts"() {
529
571
  "use strict";
@@ -538,15 +580,15 @@ var init_supabase = __esm({
538
580
  authenticatedExpiresAt = null;
539
581
  authenticatedApiKey = null;
540
582
  MCP_RUN_ID = randomUUID();
541
- CLOUD_SUPABASE_URL = "https://rhukkjscgzauutioyeei.supabase.co";
542
- CLOUD_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJodWtranNjZ3phdXV0aW95ZWVpIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ4NjM4ODYsImV4cCI6MjA4MDQzOTg4Nn0.JVtrviGvN0HaSh0JFS5KNl5FAB5ffG5Y1IMZsQFUrNQ";
583
+ CLOUD_CONFIG_URL = process.env.SOCIALNEURON_CONFIG_URL || "https://mcp.socialneuron.com/config";
584
+ _cloudConfig = null;
543
585
  projectIdCache = /* @__PURE__ */ new Map();
544
586
  }
545
587
  });
546
588
 
547
589
  // src/http.ts
548
590
  import express from "express";
549
- import { randomUUID as randomUUID3 } from "node:crypto";
591
+ import { randomUUID as randomUUID4 } from "node:crypto";
550
592
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
551
593
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
552
594
 
@@ -576,6 +618,9 @@ var TOOL_SCOPES = {
576
618
  get_best_posting_times: "mcp:read",
577
619
  extract_brand: "mcp:read",
578
620
  get_brand_profile: "mcp:read",
621
+ get_brand_runtime: "mcp:read",
622
+ explain_brand_system: "mcp:read",
623
+ check_brand_consistency: "mcp:read",
579
624
  get_ideation_context: "mcp:read",
580
625
  get_credit_balance: "mcp:read",
581
626
  get_budget_status: "mcp:read",
@@ -591,6 +636,7 @@ var TOOL_SCOPES = {
591
636
  generate_image: "mcp:write",
592
637
  check_status: "mcp:read",
593
638
  render_demo_video: "mcp:write",
639
+ render_template_video: "mcp:write",
594
640
  save_brand_profile: "mcp:write",
595
641
  update_platform_voice: "mcp:write",
596
642
  create_storyboard: "mcp:write",
@@ -629,7 +675,19 @@ var TOOL_SCOPES = {
629
675
  // mcp:read (usage is read-only)
630
676
  get_mcp_usage: "mcp:read",
631
677
  list_plan_approvals: "mcp:read",
632
- 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"
633
691
  };
634
692
  function hasScope(userScopes, required) {
635
693
  if (userScopes.includes(required)) return true;
@@ -640,6 +698,147 @@ function hasScope(userScopes, required) {
640
698
  return false;
641
699
  }
642
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
+
643
842
  // src/tools/ideation.ts
644
843
  import { z } from "zod";
645
844
 
@@ -1376,7 +1575,7 @@ function sanitizeError(error) {
1376
1575
  init_request_context();
1377
1576
 
1378
1577
  // src/lib/version.ts
1379
- var MCP_VERSION = "1.6.0";
1578
+ var MCP_VERSION = "1.7.0";
1380
1579
 
1381
1580
  // src/tools/content.ts
1382
1581
  var MAX_CREDITS_PER_RUN = Math.max(
@@ -5447,6 +5646,22 @@ var COMPOSITIONS = [
5447
5646
  durationInFrames: 450,
5448
5647
  fps: 30,
5449
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)"
5450
5665
  }
5451
5666
  ];
5452
5667
  function registerRemotionTools(server) {
@@ -5454,13 +5669,6 @@ function registerRemotionTools(server) {
5454
5669
  "list_compositions",
5455
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.",
5456
5671
  {},
5457
- {
5458
- title: "List Compositions",
5459
- readOnlyHint: true,
5460
- destructiveHint: false,
5461
- idempotentHint: true,
5462
- openWorldHint: false
5463
- },
5464
5672
  async () => {
5465
5673
  const lines = [`${COMPOSITIONS.length} Remotion compositions available:`, ""];
5466
5674
  for (const comp of COMPOSITIONS) {
@@ -5489,13 +5697,6 @@ function registerRemotionTools(server) {
5489
5697
  "JSON string of input props to pass to the composition. Each composition accepts different props. Omit for defaults."
5490
5698
  )
5491
5699
  },
5492
- {
5493
- title: "Render Demo Video",
5494
- readOnlyHint: false,
5495
- destructiveHint: false,
5496
- idempotentHint: false,
5497
- openWorldHint: false
5498
- },
5499
5700
  async ({ composition_id, output_format, props }) => {
5500
5701
  const startedAt = Date.now();
5501
5702
  const userId = await getDefaultUserId();
@@ -5627,6 +5828,134 @@ function registerRemotionTools(server) {
5627
5828
  }
5628
5829
  }
5629
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
+ );
5630
5959
  }
5631
5960
 
5632
5961
  // src/tools/insights.ts
@@ -7035,7 +7364,6 @@ ${"=".repeat(40)}
7035
7364
  }
7036
7365
 
7037
7366
  // src/tools/autopilot.ts
7038
- init_supabase();
7039
7367
  import { z as z15 } from "zod";
7040
7368
  function asEnvelope11(data) {
7041
7369
  return {
@@ -7054,46 +7382,35 @@ function registerAutopilotTools(server) {
7054
7382
  active_only: z15.boolean().optional().describe("If true, only return active configs. Defaults to false (show all)."),
7055
7383
  response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7056
7384
  },
7057
- {
7058
- title: "List Autopilot Configs",
7059
- readOnlyHint: true,
7060
- destructiveHint: false,
7061
- idempotentHint: true,
7062
- openWorldHint: false
7063
- },
7064
7385
  async ({ active_only, response_format }) => {
7065
7386
  const format = response_format ?? "text";
7066
- const supabase = getSupabaseClient();
7067
- const userId = await getDefaultUserId();
7068
- let query = supabase.from("autopilot_configs").select(
7069
- "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"
7070
- ).eq("user_id", userId).order("created_at", { ascending: false });
7071
- if (active_only) {
7072
- query = query.eq("is_active", true);
7073
- }
7074
- const { data: configs, error } = await query;
7075
- 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) {
7076
7392
  return {
7077
7393
  content: [
7078
7394
  {
7079
7395
  type: "text",
7080
- text: `Error fetching autopilot configs: ${sanitizeDbError(error)}`
7396
+ text: `Error fetching autopilot configs: ${efError}`
7081
7397
  }
7082
7398
  ],
7083
7399
  isError: true
7084
7400
  };
7085
7401
  }
7402
+ const configs = result?.configs ?? [];
7086
7403
  if (format === "json") {
7087
7404
  return {
7088
7405
  content: [
7089
7406
  {
7090
7407
  type: "text",
7091
- text: JSON.stringify(asEnvelope11(configs || []), null, 2)
7408
+ text: JSON.stringify(asEnvelope11(configs), null, 2)
7092
7409
  }
7093
7410
  ]
7094
7411
  };
7095
7412
  }
7096
- if (!configs || configs.length === 0) {
7413
+ if (configs.length === 0) {
7097
7414
  return {
7098
7415
  content: [
7099
7416
  {
@@ -7138,18 +7455,11 @@ ${"=".repeat(40)}
7138
7455
  {
7139
7456
  config_id: z15.string().uuid().describe("The autopilot config ID to update."),
7140
7457
  is_active: z15.boolean().optional().describe("Enable or disable this autopilot config."),
7141
- 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"]).'),
7142
7459
  schedule_time: z15.string().optional().describe('Time to run in HH:MM format (24h, user timezone). E.g., "09:00".'),
7143
7460
  max_credits_per_run: z15.number().optional().describe("Maximum credits per execution."),
7144
7461
  max_credits_per_week: z15.number().optional().describe("Maximum credits per week.")
7145
7462
  },
7146
- {
7147
- title: "Update Autopilot Config",
7148
- readOnlyHint: false,
7149
- destructiveHint: false,
7150
- idempotentHint: true,
7151
- openWorldHint: false
7152
- },
7153
7463
  async ({
7154
7464
  config_id,
7155
7465
  is_active,
@@ -7158,22 +7468,7 @@ ${"=".repeat(40)}
7158
7468
  max_credits_per_run,
7159
7469
  max_credits_per_week
7160
7470
  }) => {
7161
- const supabase = getSupabaseClient();
7162
- const userId = await getDefaultUserId();
7163
- const updates = {};
7164
- if (is_active !== void 0) updates.is_active = is_active;
7165
- if (max_credits_per_run !== void 0) updates.max_credits_per_run = max_credits_per_run;
7166
- if (max_credits_per_week !== void 0) updates.max_credits_per_week = max_credits_per_week;
7167
- if (schedule_days || schedule_time) {
7168
- const { data: existing } = await supabase.from("autopilot_configs").select("schedule_config").eq("id", config_id).eq("user_id", userId).single();
7169
- const existingSchedule = existing?.schedule_config || {};
7170
- updates.schedule_config = {
7171
- ...existingSchedule,
7172
- ...schedule_days ? { days: schedule_days } : {},
7173
- ...schedule_time ? { time: schedule_time } : {}
7174
- };
7175
- }
7176
- 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) {
7177
7472
  return {
7178
7473
  content: [
7179
7474
  {
@@ -7183,18 +7478,37 @@ ${"=".repeat(40)}
7183
7478
  ]
7184
7479
  };
7185
7480
  }
7186
- 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();
7187
- 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) {
7188
7491
  return {
7189
7492
  content: [
7190
7493
  {
7191
7494
  type: "text",
7192
- text: `Error updating config: ${sanitizeDbError(error)}`
7495
+ text: `Error updating config: ${efError}`
7193
7496
  }
7194
7497
  ],
7195
7498
  isError: true
7196
7499
  };
7197
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
+ }
7198
7512
  return {
7199
7513
  content: [
7200
7514
  {
@@ -7213,26 +7527,19 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
7213
7527
  {
7214
7528
  response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7215
7529
  },
7216
- {
7217
- title: "Get Autopilot Status",
7218
- readOnlyHint: true,
7219
- destructiveHint: false,
7220
- idempotentHint: true,
7221
- openWorldHint: false
7222
- },
7223
7530
  async ({ response_format }) => {
7224
7531
  const format = response_format ?? "text";
7225
- const supabase = getSupabaseClient();
7226
- const userId = await getDefaultUserId();
7227
- const { data: configs } = await supabase.from("autopilot_configs").select(
7228
- "id, recipe_id, is_active, schedule_config, last_run_at, credits_used_this_week, max_credits_per_week"
7229
- ).eq("user_id", userId).eq("is_active", true);
7230
- 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);
7231
- 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
+ }
7232
7539
  const statusData = {
7233
- activeConfigs: configs?.length || 0,
7234
- recentRuns: recentRuns || [],
7235
- pendingApprovals: approvals?.length || 0
7540
+ activeConfigs: result?.activeConfigs ?? 0,
7541
+ recentRuns: [],
7542
+ pendingApprovals: result?.pendingApprovals ?? 0
7236
7543
  };
7237
7544
  if (format === "json") {
7238
7545
  return {
@@ -7253,22 +7560,93 @@ ${"=".repeat(40)}
7253
7560
  text += `Pending Approvals: ${statusData.pendingApprovals}
7254
7561
 
7255
7562
  `;
7256
- if (statusData.recentRuns.length > 0) {
7257
- text += `Recent Runs:
7563
+ text += `No recent runs.
7258
7564
  `;
7259
- for (const run of statusData.recentRuns) {
7260
- text += ` ${run.id.substring(0, 8)}... \u2014 ${run.status} (${run.started_at})
7261
- `;
7262
- }
7263
- } else {
7264
- text += `No recent runs.
7265
- `;
7266
- }
7267
7565
  return {
7268
7566
  content: [{ type: "text", text }]
7269
7567
  };
7270
7568
  }
7271
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
+ );
7272
7650
  }
7273
7651
 
7274
7652
  // src/tools/extraction.ts
@@ -9010,6 +9388,24 @@ var TOOL_CATALOG = [
9010
9388
  module: "brand",
9011
9389
  scope: "mcp:read"
9012
9390
  },
9391
+ {
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
+ },
9013
9409
  {
9014
9410
  name: "save_brand_profile",
9015
9411
  description: "Save or update brand profile",
@@ -9048,10 +9444,16 @@ var TOOL_CATALOG = [
9048
9444
  module: "remotion",
9049
9445
  scope: "mcp:read"
9050
9446
  },
9051
- // youtube-analytics
9052
9447
  {
9053
- name: "fetch_youtube_analytics",
9054
- description: "Fetch YouTube channel analytics data",
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
+ },
9453
+ // youtube-analytics
9454
+ {
9455
+ name: "fetch_youtube_analytics",
9456
+ description: "Fetch YouTube channel analytics data",
9055
9457
  module: "youtube-analytics",
9056
9458
  scope: "mcp:analytics"
9057
9459
  },
@@ -9220,6 +9622,58 @@ var TOOL_CATALOG = [
9220
9622
  description: "Search and discover available MCP tools",
9221
9623
  module: "discovery",
9222
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"
9223
9677
  }
9224
9678
  ];
9225
9679
  function getToolsByModule(module) {
@@ -9235,59 +9689,1750 @@ function searchTools(query) {
9235
9689
  );
9236
9690
  }
9237
9691
 
9238
- // src/tools/discovery.ts
9239
- 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) {
9240
11190
  server.tool(
9241
- "search_tools",
9242
- '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.",
9243
11193
  {
9244
- query: z20.string().optional().describe("Search query to filter tools by name or description"),
9245
- module: z20.string().optional().describe('Filter by module name (e.g. "planning", "content", "analytics")'),
9246
- scope: z20.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
9247
- detail: z20.enum(["name", "summary", "full"]).default("summary").describe(
9248
- 'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
9249
- )
11194
+ project_id: z24.string().optional().describe("Project ID. Defaults to current project.")
9250
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.",
9251
11263
  {
9252
- title: "Search Tools",
9253
- readOnlyHint: true,
9254
- destructiveHint: false,
9255
- idempotentHint: true,
9256
- openWorldHint: false
11264
+ project_id: z24.string().optional().describe("Project ID. Defaults to current project.")
9257
11265
  },
9258
- async ({ query, module, scope, detail }) => {
9259
- let results = [...TOOL_CATALOG];
9260
- if (query) {
9261
- 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
+ };
9262
11275
  }
9263
- if (module) {
9264
- const moduleTools = getToolsByModule(module);
9265
- 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
+ };
9266
11283
  }
9267
- if (scope) {
9268
- const scopeTools = getToolsByScope(scope);
9269
- 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}%)`);
9270
11347
  }
9271
- let output;
9272
- switch (detail) {
9273
- case "name":
9274
- output = results.map((t) => t.name);
9275
- break;
9276
- case "summary":
9277
- output = results.map((t) => ({ name: t.name, description: t.description }));
9278
- break;
9279
- case "full":
9280
- default:
9281
- output = results;
9282
- 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}`));
9283
11366
  }
9284
11367
  return {
9285
- content: [
9286
- {
9287
- type: "text",
9288
- text: JSON.stringify({ toolCount: results.length, tools: output }, null, 2)
9289
- }
9290
- ]
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) }]
9291
11436
  };
9292
11437
  }
9293
11438
  );
@@ -9328,12 +11473,46 @@ function applyScopeEnforcement(server, scopeResolver) {
9328
11473
  isError: true
9329
11474
  };
9330
11475
  }
9331
- return originalHandler(...handlerArgs);
11476
+ const result = await originalHandler(...handlerArgs);
11477
+ return truncateResponse(result);
9332
11478
  };
9333
11479
  }
9334
11480
  return originalTool(...args);
9335
11481
  };
9336
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
+ }
9337
11516
  function registerAllTools(server, options) {
9338
11517
  registerIdeationTools(server);
9339
11518
  registerContentTools(server);
@@ -9357,6 +11536,11 @@ function registerAllTools(server, options) {
9357
11536
  registerPlanningTools(server);
9358
11537
  registerPlanApprovalTools(server);
9359
11538
  registerDiscoveryTools(server);
11539
+ registerPipelineTools(server);
11540
+ registerSuggestTools(server);
11541
+ registerDigestTools(server);
11542
+ registerBrandRuntimeTools(server);
11543
+ applyAnnotations(server);
9360
11544
  }
9361
11545
 
9362
11546
  // src/http.ts
@@ -10542,7 +12726,8 @@ setInterval(() => {
10542
12726
  }
10543
12727
  }, IP_RATE_CLEANUP_INTERVAL).unref();
10544
12728
  app.use((req, res, next) => {
10545
- 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();
10546
12731
  const ip = req.ip ?? req.socket.remoteAddress ?? "unknown";
10547
12732
  const now = Date.now();
10548
12733
  let bucket = ipBuckets.get(ip);
@@ -10641,6 +12826,210 @@ async function authenticateRequest(req, res, next) {
10641
12826
  });
10642
12827
  }
10643
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
+ });
10644
13033
  app.get("/health", (_req, res) => {
10645
13034
  res.json({ status: "ok", version: MCP_VERSION });
10646
13035
  });
@@ -10719,7 +13108,7 @@ app.post(
10719
13108
  applyScopeEnforcement(server, () => getRequestScopes() ?? auth.scopes);
10720
13109
  registerAllTools(server, { skipScreenshots: true });
10721
13110
  const transport = new StreamableHTTPServerTransport({
10722
- sessionIdGenerator: () => randomUUID3(),
13111
+ sessionIdGenerator: () => randomUUID4(),
10723
13112
  onsessioninitialized: (sessionId) => {
10724
13113
  sessions.set(sessionId, {
10725
13114
  transport,