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