@socialneuron/mcp-server 1.6.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/http.js +2537 -148
- package/dist/index.js +2843 -160
- package/package.json +2 -2
package/dist/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 =
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
542
|
-
|
|
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
|
|
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.
|
|
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
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
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: ${
|
|
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
|
|
7408
|
+
text: JSON.stringify(asEnvelope11(configs), null, 2)
|
|
7092
7409
|
}
|
|
7093
7410
|
]
|
|
7094
7411
|
};
|
|
7095
7412
|
}
|
|
7096
|
-
if (
|
|
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"])
|
|
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
|
-
|
|
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:
|
|
7187
|
-
|
|
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: ${
|
|
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
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
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:
|
|
7234
|
-
recentRuns:
|
|
7235
|
-
pendingApprovals:
|
|
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
|
-
|
|
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: "
|
|
9054
|
-
description: "
|
|
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
|
-
"
|
|
9242
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ({
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
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
|
-
|
|
9264
|
-
|
|
9265
|
-
|
|
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
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
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
|
-
|
|
9272
|
-
|
|
9273
|
-
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
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
|
-
|
|
9288
|
-
|
|
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
|
-
|
|
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"
|
|
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: () =>
|
|
13111
|
+
sessionIdGenerator: () => randomUUID4(),
|
|
10723
13112
|
onsessioninitialized: (sessionId) => {
|
|
10724
13113
|
sessions.set(sessionId, {
|
|
10725
13114
|
transport,
|