@socialneuron/mcp-server 1.7.2 → 1.7.4
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 +123 -17
- package/README.md +77 -19
- package/dist/http.js +1463 -280
- package/dist/index.js +1467 -230
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var MCP_VERSION;
|
|
|
14
14
|
var init_version = __esm({
|
|
15
15
|
"src/lib/version.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
MCP_VERSION = "1.7.
|
|
17
|
+
MCP_VERSION = "1.7.4";
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
20
|
|
|
@@ -895,6 +895,12 @@ var init_tool_catalog = __esm({
|
|
|
895
895
|
module: "content",
|
|
896
896
|
scope: "mcp:write"
|
|
897
897
|
},
|
|
898
|
+
{
|
|
899
|
+
name: "create_carousel",
|
|
900
|
+
description: "End-to-end carousel: generate text + kick off image jobs for each slide",
|
|
901
|
+
module: "carousel",
|
|
902
|
+
scope: "mcp:write"
|
|
903
|
+
},
|
|
898
904
|
// media
|
|
899
905
|
{
|
|
900
906
|
name: "upload_media",
|
|
@@ -1252,6 +1258,45 @@ var init_tool_catalog = __esm({
|
|
|
1252
1258
|
description: "Create a new autopilot configuration",
|
|
1253
1259
|
module: "autopilot",
|
|
1254
1260
|
scope: "mcp:autopilot"
|
|
1261
|
+
},
|
|
1262
|
+
// brand runtime (additions)
|
|
1263
|
+
{
|
|
1264
|
+
name: "audit_brand_colors",
|
|
1265
|
+
description: "Audit brand color palette for accessibility, contrast, and harmony",
|
|
1266
|
+
module: "brandRuntime",
|
|
1267
|
+
scope: "mcp:read"
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
name: "export_design_tokens",
|
|
1271
|
+
description: "Export brand design tokens in CSS/Tailwind/JSON formats",
|
|
1272
|
+
module: "brandRuntime",
|
|
1273
|
+
scope: "mcp:read"
|
|
1274
|
+
},
|
|
1275
|
+
// carousel (already listed in content section above)
|
|
1276
|
+
// recipes
|
|
1277
|
+
{
|
|
1278
|
+
name: "list_recipes",
|
|
1279
|
+
description: "List available recipe templates for automated content workflows",
|
|
1280
|
+
module: "recipes",
|
|
1281
|
+
scope: "mcp:read"
|
|
1282
|
+
},
|
|
1283
|
+
{
|
|
1284
|
+
name: "get_recipe_details",
|
|
1285
|
+
description: "Get full details of a recipe template including steps and required inputs",
|
|
1286
|
+
module: "recipes",
|
|
1287
|
+
scope: "mcp:read"
|
|
1288
|
+
},
|
|
1289
|
+
{
|
|
1290
|
+
name: "execute_recipe",
|
|
1291
|
+
description: "Execute a recipe template with provided inputs to run a multi-step workflow",
|
|
1292
|
+
module: "recipes",
|
|
1293
|
+
scope: "mcp:write"
|
|
1294
|
+
},
|
|
1295
|
+
{
|
|
1296
|
+
name: "get_recipe_run_status",
|
|
1297
|
+
description: "Check the status and progress of a running recipe execution",
|
|
1298
|
+
module: "recipes",
|
|
1299
|
+
scope: "mcp:read"
|
|
1255
1300
|
}
|
|
1256
1301
|
];
|
|
1257
1302
|
}
|
|
@@ -3856,6 +3901,8 @@ var TOOL_SCOPES = {
|
|
|
3856
3901
|
get_brand_runtime: "mcp:read",
|
|
3857
3902
|
explain_brand_system: "mcp:read",
|
|
3858
3903
|
check_brand_consistency: "mcp:read",
|
|
3904
|
+
audit_brand_colors: "mcp:read",
|
|
3905
|
+
export_design_tokens: "mcp:read",
|
|
3859
3906
|
get_ideation_context: "mcp:read",
|
|
3860
3907
|
get_credit_balance: "mcp:read",
|
|
3861
3908
|
get_budget_status: "mcp:read",
|
|
@@ -3877,6 +3924,7 @@ var TOOL_SCOPES = {
|
|
|
3877
3924
|
create_storyboard: "mcp:write",
|
|
3878
3925
|
generate_voiceover: "mcp:write",
|
|
3879
3926
|
generate_carousel: "mcp:write",
|
|
3927
|
+
create_carousel: "mcp:write",
|
|
3880
3928
|
upload_media: "mcp:write",
|
|
3881
3929
|
// mcp:read (media)
|
|
3882
3930
|
get_media_url: "mcp:read",
|
|
@@ -3895,6 +3943,11 @@ var TOOL_SCOPES = {
|
|
|
3895
3943
|
list_autopilot_configs: "mcp:autopilot",
|
|
3896
3944
|
update_autopilot_config: "mcp:autopilot",
|
|
3897
3945
|
get_autopilot_status: "mcp:autopilot",
|
|
3946
|
+
// Recipes
|
|
3947
|
+
list_recipes: "mcp:read",
|
|
3948
|
+
get_recipe_details: "mcp:read",
|
|
3949
|
+
execute_recipe: "mcp:write",
|
|
3950
|
+
get_recipe_run_status: "mcp:read",
|
|
3898
3951
|
// mcp:read (content lifecycle — read-only tools)
|
|
3899
3952
|
extract_url_content: "mcp:read",
|
|
3900
3953
|
quality_check: "mcp:read",
|
|
@@ -5658,6 +5711,56 @@ Return ONLY valid JSON in this exact format:
|
|
|
5658
5711
|
init_edge_function();
|
|
5659
5712
|
import { z as z3 } from "zod";
|
|
5660
5713
|
import { createHash as createHash2 } from "node:crypto";
|
|
5714
|
+
|
|
5715
|
+
// src/lib/sanitize-error.ts
|
|
5716
|
+
var ERROR_PATTERNS = [
|
|
5717
|
+
// Postgres / PostgREST
|
|
5718
|
+
[/PGRST301|permission denied/i, "Access denied. Check your account permissions."],
|
|
5719
|
+
[/42P01|does not exist/i, "Service temporarily unavailable. Please try again."],
|
|
5720
|
+
[/23505|unique.*constraint|duplicate key/i, "A duplicate record already exists."],
|
|
5721
|
+
[/23503|foreign key/i, "Referenced record not found."],
|
|
5722
|
+
// Gemini / Google AI
|
|
5723
|
+
[/google.*api.*key|googleapis\.com.*40[13]/i, "Content generation failed. Please try again."],
|
|
5724
|
+
[
|
|
5725
|
+
/RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
|
|
5726
|
+
"AI service rate limit reached. Please wait and retry."
|
|
5727
|
+
],
|
|
5728
|
+
[
|
|
5729
|
+
/SAFETY|prompt.*blocked|content.*filter/i,
|
|
5730
|
+
"Content was blocked by the AI safety filter. Try rephrasing."
|
|
5731
|
+
],
|
|
5732
|
+
[/gemini.*error|generativelanguage/i, "Content generation failed. Please try again."],
|
|
5733
|
+
// Kie.ai
|
|
5734
|
+
[/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
|
|
5735
|
+
// Stripe
|
|
5736
|
+
[/stripe.*api|sk_live_|sk_test_/i, "Payment processing error. Please try again."],
|
|
5737
|
+
// Network / fetch
|
|
5738
|
+
[
|
|
5739
|
+
/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
|
|
5740
|
+
"External service unavailable. Please try again."
|
|
5741
|
+
],
|
|
5742
|
+
[/fetch failed|network error|abort.*timeout/i, "Network request failed. Please try again."],
|
|
5743
|
+
[/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
|
|
5744
|
+
// Supabase Edge Function internals
|
|
5745
|
+
[/FunctionsHttpError|non-2xx status/i, "Backend service error. Please try again."],
|
|
5746
|
+
[/JWT|token.*expired|token.*invalid/i, "Authentication expired. Please re-authenticate."],
|
|
5747
|
+
// Generic sensitive patterns (API keys, URLs with secrets)
|
|
5748
|
+
[/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i, "An internal error occurred. Please try again."]
|
|
5749
|
+
];
|
|
5750
|
+
function sanitizeError2(error) {
|
|
5751
|
+
const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
5752
|
+
if (process.env.NODE_ENV !== "production") {
|
|
5753
|
+
console.error("[Error]", msg);
|
|
5754
|
+
}
|
|
5755
|
+
for (const [pattern, userMessage] of ERROR_PATTERNS) {
|
|
5756
|
+
if (pattern.test(msg)) {
|
|
5757
|
+
return userMessage;
|
|
5758
|
+
}
|
|
5759
|
+
}
|
|
5760
|
+
return "An unexpected error occurred. Please try again.";
|
|
5761
|
+
}
|
|
5762
|
+
|
|
5763
|
+
// src/tools/distribution.ts
|
|
5661
5764
|
init_supabase();
|
|
5662
5765
|
init_quality();
|
|
5663
5766
|
init_version();
|
|
@@ -5863,7 +5966,12 @@ function registerDistributionTools(server2) {
|
|
|
5863
5966
|
const signed = await signR2Key(r2_key);
|
|
5864
5967
|
if (!signed) {
|
|
5865
5968
|
return {
|
|
5866
|
-
content: [
|
|
5969
|
+
content: [
|
|
5970
|
+
{
|
|
5971
|
+
type: "text",
|
|
5972
|
+
text: `Failed to sign media key. Verify the key exists and you have access.`
|
|
5973
|
+
}
|
|
5974
|
+
],
|
|
5867
5975
|
isError: true
|
|
5868
5976
|
};
|
|
5869
5977
|
}
|
|
@@ -5891,7 +5999,7 @@ function registerDistributionTools(server2) {
|
|
|
5891
5999
|
content: [
|
|
5892
6000
|
{
|
|
5893
6001
|
type: "text",
|
|
5894
|
-
text: `Failed to sign
|
|
6002
|
+
text: `Failed to sign media key at index ${failIdx}. Verify the key exists and you have access.`
|
|
5895
6003
|
}
|
|
5896
6004
|
],
|
|
5897
6005
|
isError: true
|
|
@@ -5919,7 +6027,7 @@ function registerDistributionTools(server2) {
|
|
|
5919
6027
|
content: [
|
|
5920
6028
|
{
|
|
5921
6029
|
type: "text",
|
|
5922
|
-
text: `Failed to resolve media: ${
|
|
6030
|
+
text: `Failed to resolve media: ${sanitizeError2(resolveErr)}`
|
|
5923
6031
|
}
|
|
5924
6032
|
],
|
|
5925
6033
|
isError: true
|
|
@@ -6284,7 +6392,7 @@ Created with Social Neuron`;
|
|
|
6284
6392
|
return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
|
|
6285
6393
|
} catch (err) {
|
|
6286
6394
|
const durationMs = Date.now() - startedAt;
|
|
6287
|
-
const message =
|
|
6395
|
+
const message = sanitizeError2(err);
|
|
6288
6396
|
logMcpToolInvocation({
|
|
6289
6397
|
toolName: "find_next_slots",
|
|
6290
6398
|
status: "error",
|
|
@@ -6804,7 +6912,7 @@ Created with Social Neuron`;
|
|
|
6804
6912
|
};
|
|
6805
6913
|
} catch (err) {
|
|
6806
6914
|
const durationMs = Date.now() - startedAt;
|
|
6807
|
-
const message =
|
|
6915
|
+
const message = sanitizeError2(err);
|
|
6808
6916
|
logMcpToolInvocation({
|
|
6809
6917
|
toolName: "schedule_content_plan",
|
|
6810
6918
|
status: "error",
|
|
@@ -6960,7 +7068,7 @@ function registerMediaTools(server2) {
|
|
|
6960
7068
|
content: [
|
|
6961
7069
|
{
|
|
6962
7070
|
type: "text",
|
|
6963
|
-
text: `R2 upload failed: ${
|
|
7071
|
+
text: `R2 upload failed: ${sanitizeError(uploadErr)}`
|
|
6964
7072
|
}
|
|
6965
7073
|
],
|
|
6966
7074
|
isError: true
|
|
@@ -8088,7 +8196,7 @@ function registerScreenshotTools(server2) {
|
|
|
8088
8196
|
};
|
|
8089
8197
|
} catch (err) {
|
|
8090
8198
|
await closeBrowser();
|
|
8091
|
-
const message =
|
|
8199
|
+
const message = sanitizeError2(err);
|
|
8092
8200
|
await logMcpToolInvocation({
|
|
8093
8201
|
toolName: "capture_app_page",
|
|
8094
8202
|
status: "error",
|
|
@@ -8244,7 +8352,7 @@ function registerScreenshotTools(server2) {
|
|
|
8244
8352
|
};
|
|
8245
8353
|
} catch (err) {
|
|
8246
8354
|
await closeBrowser();
|
|
8247
|
-
const message =
|
|
8355
|
+
const message = sanitizeError2(err);
|
|
8248
8356
|
await logMcpToolInvocation({
|
|
8249
8357
|
toolName: "capture_screenshot",
|
|
8250
8358
|
status: "error",
|
|
@@ -8538,7 +8646,7 @@ function registerRemotionTools(server2) {
|
|
|
8538
8646
|
]
|
|
8539
8647
|
};
|
|
8540
8648
|
} catch (err) {
|
|
8541
|
-
const message =
|
|
8649
|
+
const message = sanitizeError2(err);
|
|
8542
8650
|
await logMcpToolInvocation({
|
|
8543
8651
|
toolName: "render_demo_video",
|
|
8544
8652
|
status: "error",
|
|
@@ -8666,7 +8774,7 @@ function registerRemotionTools(server2) {
|
|
|
8666
8774
|
]
|
|
8667
8775
|
};
|
|
8668
8776
|
} catch (err) {
|
|
8669
|
-
const message =
|
|
8777
|
+
const message = sanitizeError2(err);
|
|
8670
8778
|
await logMcpToolInvocation({
|
|
8671
8779
|
toolName: "render_template_video",
|
|
8672
8780
|
status: "error",
|
|
@@ -10126,12 +10234,280 @@ Active: ${is_active}`
|
|
|
10126
10234
|
);
|
|
10127
10235
|
}
|
|
10128
10236
|
|
|
10237
|
+
// src/tools/recipes.ts
|
|
10238
|
+
init_edge_function();
|
|
10239
|
+
init_version();
|
|
10240
|
+
import { z as z17 } from "zod";
|
|
10241
|
+
function asEnvelope13(data) {
|
|
10242
|
+
return {
|
|
10243
|
+
_meta: {
|
|
10244
|
+
version: MCP_VERSION,
|
|
10245
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10246
|
+
},
|
|
10247
|
+
data
|
|
10248
|
+
};
|
|
10249
|
+
}
|
|
10250
|
+
function registerRecipeTools(server2) {
|
|
10251
|
+
server2.tool(
|
|
10252
|
+
"list_recipes",
|
|
10253
|
+
'List available recipe templates. Recipes are pre-built multi-step workflows like "Weekly Instagram Calendar" or "Product Launch Sequence" that automate common content operations. Use this to discover what recipes are available before running one.',
|
|
10254
|
+
{
|
|
10255
|
+
category: z17.enum([
|
|
10256
|
+
"content_creation",
|
|
10257
|
+
"distribution",
|
|
10258
|
+
"repurposing",
|
|
10259
|
+
"analytics",
|
|
10260
|
+
"engagement",
|
|
10261
|
+
"general"
|
|
10262
|
+
]).optional().describe("Filter by category. Omit to list all."),
|
|
10263
|
+
featured_only: z17.boolean().optional().describe("If true, only return featured recipes. Defaults to false."),
|
|
10264
|
+
response_format: z17.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
10265
|
+
},
|
|
10266
|
+
async ({ category, featured_only, response_format }) => {
|
|
10267
|
+
const format = response_format ?? "text";
|
|
10268
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
10269
|
+
action: "list-recipes",
|
|
10270
|
+
category: category ?? null,
|
|
10271
|
+
featured_only: featured_only ?? false
|
|
10272
|
+
});
|
|
10273
|
+
if (efError) {
|
|
10274
|
+
return {
|
|
10275
|
+
content: [
|
|
10276
|
+
{
|
|
10277
|
+
type: "text",
|
|
10278
|
+
text: `Error fetching recipes: ${efError}`
|
|
10279
|
+
}
|
|
10280
|
+
],
|
|
10281
|
+
isError: true
|
|
10282
|
+
};
|
|
10283
|
+
}
|
|
10284
|
+
const recipes = result?.recipes ?? [];
|
|
10285
|
+
if (format === "json") {
|
|
10286
|
+
return {
|
|
10287
|
+
content: [
|
|
10288
|
+
{
|
|
10289
|
+
type: "text",
|
|
10290
|
+
text: JSON.stringify(asEnvelope13(recipes))
|
|
10291
|
+
}
|
|
10292
|
+
]
|
|
10293
|
+
};
|
|
10294
|
+
}
|
|
10295
|
+
if (recipes.length === 0) {
|
|
10296
|
+
return {
|
|
10297
|
+
content: [
|
|
10298
|
+
{
|
|
10299
|
+
type: "text",
|
|
10300
|
+
text: "No recipes found. Recipes are pre-built automation templates \u2014 check back after setup."
|
|
10301
|
+
}
|
|
10302
|
+
]
|
|
10303
|
+
};
|
|
10304
|
+
}
|
|
10305
|
+
const lines = recipes.map(
|
|
10306
|
+
(r) => `**${r.name}** (${r.slug})
|
|
10307
|
+
${r.description}
|
|
10308
|
+
Category: ${r.category} | Credits: ~${r.estimated_credits} | Steps: ${r.steps.length}${r.is_featured ? " | \u2B50 Featured" : ""}
|
|
10309
|
+
Inputs: ${r.inputs_schema.map((i) => `${i.label}${i.required ? "*" : ""}`).join(", ")}`
|
|
10310
|
+
);
|
|
10311
|
+
return {
|
|
10312
|
+
content: [
|
|
10313
|
+
{
|
|
10314
|
+
type: "text",
|
|
10315
|
+
text: `## Available Recipes (${recipes.length})
|
|
10316
|
+
|
|
10317
|
+
${lines.join("\n\n")}`
|
|
10318
|
+
}
|
|
10319
|
+
]
|
|
10320
|
+
};
|
|
10321
|
+
}
|
|
10322
|
+
);
|
|
10323
|
+
server2.tool(
|
|
10324
|
+
"get_recipe_details",
|
|
10325
|
+
"Get full details of a recipe template including all steps, input schema, and estimated costs. Use this before execute_recipe to understand what inputs are required.",
|
|
10326
|
+
{
|
|
10327
|
+
slug: z17.string().describe('Recipe slug (e.g., "weekly-instagram-calendar")'),
|
|
10328
|
+
response_format: z17.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
10329
|
+
},
|
|
10330
|
+
async ({ slug, response_format }) => {
|
|
10331
|
+
const format = response_format ?? "text";
|
|
10332
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
10333
|
+
action: "get-recipe-details",
|
|
10334
|
+
slug
|
|
10335
|
+
});
|
|
10336
|
+
if (efError) {
|
|
10337
|
+
return {
|
|
10338
|
+
content: [{ type: "text", text: `Error: ${efError}` }],
|
|
10339
|
+
isError: true
|
|
10340
|
+
};
|
|
10341
|
+
}
|
|
10342
|
+
const recipe = result?.recipe;
|
|
10343
|
+
if (!recipe) {
|
|
10344
|
+
return {
|
|
10345
|
+
content: [
|
|
10346
|
+
{
|
|
10347
|
+
type: "text",
|
|
10348
|
+
text: `Recipe "${slug}" not found. Use list_recipes to see available recipes.`
|
|
10349
|
+
}
|
|
10350
|
+
],
|
|
10351
|
+
isError: true
|
|
10352
|
+
};
|
|
10353
|
+
}
|
|
10354
|
+
if (format === "json") {
|
|
10355
|
+
return {
|
|
10356
|
+
content: [
|
|
10357
|
+
{
|
|
10358
|
+
type: "text",
|
|
10359
|
+
text: JSON.stringify(asEnvelope13(recipe))
|
|
10360
|
+
}
|
|
10361
|
+
]
|
|
10362
|
+
};
|
|
10363
|
+
}
|
|
10364
|
+
const stepsText = recipe.steps.map((s, i) => ` ${i + 1}. **${s.name}** (${s.type})`).join("\n");
|
|
10365
|
+
const inputsText = recipe.inputs_schema.map(
|
|
10366
|
+
(i) => ` - **${i.label}**${i.required ? " (required)" : ""}: ${i.type}${i.placeholder ? ` \u2014 e.g., "${i.placeholder}"` : ""}`
|
|
10367
|
+
).join("\n");
|
|
10368
|
+
return {
|
|
10369
|
+
content: [
|
|
10370
|
+
{
|
|
10371
|
+
type: "text",
|
|
10372
|
+
text: [
|
|
10373
|
+
`## ${recipe.name}`,
|
|
10374
|
+
recipe.description,
|
|
10375
|
+
"",
|
|
10376
|
+
`**Category:** ${recipe.category}`,
|
|
10377
|
+
`**Estimated credits:** ~${recipe.estimated_credits}`,
|
|
10378
|
+
`**Estimated time:** ~${Math.round(recipe.estimated_duration_seconds / 60)} minutes`,
|
|
10379
|
+
"",
|
|
10380
|
+
"### Steps",
|
|
10381
|
+
stepsText,
|
|
10382
|
+
"",
|
|
10383
|
+
"### Required Inputs",
|
|
10384
|
+
inputsText
|
|
10385
|
+
].join("\n")
|
|
10386
|
+
}
|
|
10387
|
+
]
|
|
10388
|
+
};
|
|
10389
|
+
}
|
|
10390
|
+
);
|
|
10391
|
+
server2.tool(
|
|
10392
|
+
"execute_recipe",
|
|
10393
|
+
"Execute a recipe template with the provided inputs. This creates a recipe run that processes each step sequentially. Long-running recipes will return a run_id you can check with get_recipe_run_status.",
|
|
10394
|
+
{
|
|
10395
|
+
slug: z17.string().describe('Recipe slug (e.g., "weekly-instagram-calendar")'),
|
|
10396
|
+
inputs: z17.record(z17.unknown()).describe(
|
|
10397
|
+
"Input values matching the recipe input schema. Use get_recipe_details to see required inputs."
|
|
10398
|
+
),
|
|
10399
|
+
response_format: z17.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
10400
|
+
},
|
|
10401
|
+
async ({ slug, inputs, response_format }) => {
|
|
10402
|
+
const format = response_format ?? "text";
|
|
10403
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
10404
|
+
action: "execute-recipe",
|
|
10405
|
+
slug,
|
|
10406
|
+
inputs
|
|
10407
|
+
});
|
|
10408
|
+
if (efError) {
|
|
10409
|
+
return {
|
|
10410
|
+
content: [{ type: "text", text: `Error: ${efError}` }],
|
|
10411
|
+
isError: true
|
|
10412
|
+
};
|
|
10413
|
+
}
|
|
10414
|
+
if (format === "json") {
|
|
10415
|
+
return {
|
|
10416
|
+
content: [
|
|
10417
|
+
{
|
|
10418
|
+
type: "text",
|
|
10419
|
+
text: JSON.stringify(asEnvelope13(result))
|
|
10420
|
+
}
|
|
10421
|
+
]
|
|
10422
|
+
};
|
|
10423
|
+
}
|
|
10424
|
+
return {
|
|
10425
|
+
content: [
|
|
10426
|
+
{
|
|
10427
|
+
type: "text",
|
|
10428
|
+
text: `Recipe "${slug}" started.
|
|
10429
|
+
|
|
10430
|
+
**Run ID:** ${result?.run_id}
|
|
10431
|
+
**Status:** ${result?.status}
|
|
10432
|
+
|
|
10433
|
+
${result?.message || "Use get_recipe_run_status to check progress."}`
|
|
10434
|
+
}
|
|
10435
|
+
]
|
|
10436
|
+
};
|
|
10437
|
+
}
|
|
10438
|
+
);
|
|
10439
|
+
server2.tool(
|
|
10440
|
+
"get_recipe_run_status",
|
|
10441
|
+
"Check the status of a running recipe execution. Shows progress, current step, credits used, and outputs when complete.",
|
|
10442
|
+
{
|
|
10443
|
+
run_id: z17.string().describe("The recipe run ID returned by execute_recipe"),
|
|
10444
|
+
response_format: z17.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
10445
|
+
},
|
|
10446
|
+
async ({ run_id, response_format }) => {
|
|
10447
|
+
const format = response_format ?? "text";
|
|
10448
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
10449
|
+
action: "get-recipe-run-status",
|
|
10450
|
+
run_id
|
|
10451
|
+
});
|
|
10452
|
+
if (efError) {
|
|
10453
|
+
return {
|
|
10454
|
+
content: [{ type: "text", text: `Error: ${efError}` }],
|
|
10455
|
+
isError: true
|
|
10456
|
+
};
|
|
10457
|
+
}
|
|
10458
|
+
const run = result?.run;
|
|
10459
|
+
if (!run) {
|
|
10460
|
+
return {
|
|
10461
|
+
content: [
|
|
10462
|
+
{
|
|
10463
|
+
type: "text",
|
|
10464
|
+
text: `Run "${run_id}" not found.`
|
|
10465
|
+
}
|
|
10466
|
+
],
|
|
10467
|
+
isError: true
|
|
10468
|
+
};
|
|
10469
|
+
}
|
|
10470
|
+
if (format === "json") {
|
|
10471
|
+
return {
|
|
10472
|
+
content: [
|
|
10473
|
+
{
|
|
10474
|
+
type: "text",
|
|
10475
|
+
text: JSON.stringify(asEnvelope13(run))
|
|
10476
|
+
}
|
|
10477
|
+
]
|
|
10478
|
+
};
|
|
10479
|
+
}
|
|
10480
|
+
const statusEmoji = run.status === "completed" ? "Done" : run.status === "failed" ? "Failed" : run.status === "running" ? "Running" : run.status;
|
|
10481
|
+
return {
|
|
10482
|
+
content: [
|
|
10483
|
+
{
|
|
10484
|
+
type: "text",
|
|
10485
|
+
text: [
|
|
10486
|
+
`**Recipe Run:** ${run.id}`,
|
|
10487
|
+
`**Status:** ${statusEmoji}`,
|
|
10488
|
+
`**Progress:** ${run.progress}%`,
|
|
10489
|
+
run.current_step ? `**Current step:** ${run.current_step}` : "",
|
|
10490
|
+
`**Credits used:** ${run.credits_used}`,
|
|
10491
|
+
run.completed_at ? `**Completed:** ${run.completed_at}` : "",
|
|
10492
|
+
run.outputs ? `
|
|
10493
|
+
**Outputs:**
|
|
10494
|
+
\`\`\`json
|
|
10495
|
+
${JSON.stringify(run.outputs, null, 2)}
|
|
10496
|
+
\`\`\`` : ""
|
|
10497
|
+
].filter(Boolean).join("\n")
|
|
10498
|
+
}
|
|
10499
|
+
]
|
|
10500
|
+
};
|
|
10501
|
+
}
|
|
10502
|
+
);
|
|
10503
|
+
}
|
|
10504
|
+
|
|
10129
10505
|
// src/tools/extraction.ts
|
|
10130
10506
|
init_edge_function();
|
|
10507
|
+
import { z as z18 } from "zod";
|
|
10131
10508
|
init_supabase();
|
|
10132
|
-
import { z as z17 } from "zod";
|
|
10133
10509
|
init_version();
|
|
10134
|
-
function
|
|
10510
|
+
function asEnvelope14(data) {
|
|
10135
10511
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
10136
10512
|
}
|
|
10137
10513
|
function isYouTubeUrl(url) {
|
|
@@ -10185,11 +10561,11 @@ function registerExtractionTools(server2) {
|
|
|
10185
10561
|
"extract_url_content",
|
|
10186
10562
|
"Extract text content from any URL \u2014 YouTube video transcripts, article text, or product page features/benefits/USP. YouTube URLs auto-route to transcript extraction with optional comments. Use before generate_content to repurpose existing content, or before plan_content_week to base a content plan on a source URL.",
|
|
10187
10563
|
{
|
|
10188
|
-
url:
|
|
10189
|
-
extract_type:
|
|
10190
|
-
include_comments:
|
|
10191
|
-
max_results:
|
|
10192
|
-
response_format:
|
|
10564
|
+
url: z18.string().url().describe("URL to extract content from"),
|
|
10565
|
+
extract_type: z18.enum(["auto", "transcript", "article", "product"]).default("auto").describe("Type of extraction"),
|
|
10566
|
+
include_comments: z18.boolean().default(false).describe("Include top comments (YouTube only)"),
|
|
10567
|
+
max_results: z18.number().min(1).max(100).default(10).describe("Max comments to include"),
|
|
10568
|
+
response_format: z18.enum(["text", "json"]).default("text")
|
|
10193
10569
|
},
|
|
10194
10570
|
async ({ url, extract_type, include_comments, max_results, response_format }) => {
|
|
10195
10571
|
const startedAt = Date.now();
|
|
@@ -10324,7 +10700,7 @@ function registerExtractionTools(server2) {
|
|
|
10324
10700
|
if (response_format === "json") {
|
|
10325
10701
|
return {
|
|
10326
10702
|
content: [
|
|
10327
|
-
{ type: "text", text: JSON.stringify(
|
|
10703
|
+
{ type: "text", text: JSON.stringify(asEnvelope14(extracted), null, 2) }
|
|
10328
10704
|
],
|
|
10329
10705
|
isError: false
|
|
10330
10706
|
};
|
|
@@ -10335,7 +10711,7 @@ function registerExtractionTools(server2) {
|
|
|
10335
10711
|
};
|
|
10336
10712
|
} catch (err) {
|
|
10337
10713
|
const durationMs = Date.now() - startedAt;
|
|
10338
|
-
const message =
|
|
10714
|
+
const message = sanitizeError2(err);
|
|
10339
10715
|
logMcpToolInvocation({
|
|
10340
10716
|
toolName: "extract_url_content",
|
|
10341
10717
|
status: "error",
|
|
@@ -10355,8 +10731,8 @@ function registerExtractionTools(server2) {
|
|
|
10355
10731
|
init_quality();
|
|
10356
10732
|
init_supabase();
|
|
10357
10733
|
init_version();
|
|
10358
|
-
import { z as
|
|
10359
|
-
function
|
|
10734
|
+
import { z as z19 } from "zod";
|
|
10735
|
+
function asEnvelope15(data) {
|
|
10360
10736
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
10361
10737
|
}
|
|
10362
10738
|
function registerQualityTools(server2) {
|
|
@@ -10364,12 +10740,12 @@ function registerQualityTools(server2) {
|
|
|
10364
10740
|
"quality_check",
|
|
10365
10741
|
"Score post quality across 7 categories: Hook Strength, Message Clarity, Platform Fit, Brand Alignment, Novelty, CTA Strength, and Safety/Claims. Each scored 0-5, total 35. Default pass threshold is 26 (~75%). Run after generate_content and before schedule_post. Include hashtags in caption if they will be published \u2014 they affect Platform Fit and Safety scores.",
|
|
10366
10742
|
{
|
|
10367
|
-
caption:
|
|
10743
|
+
caption: z19.string().describe(
|
|
10368
10744
|
"The post text to score. Include hashtags if they will be published \u2014 they affect Platform Fit and Safety/Claims scores."
|
|
10369
10745
|
),
|
|
10370
|
-
title:
|
|
10371
|
-
platforms:
|
|
10372
|
-
|
|
10746
|
+
title: z19.string().optional().describe("Post title (important for YouTube)"),
|
|
10747
|
+
platforms: z19.array(
|
|
10748
|
+
z19.enum([
|
|
10373
10749
|
"youtube",
|
|
10374
10750
|
"tiktok",
|
|
10375
10751
|
"instagram",
|
|
@@ -10380,13 +10756,13 @@ function registerQualityTools(server2) {
|
|
|
10380
10756
|
"bluesky"
|
|
10381
10757
|
])
|
|
10382
10758
|
).min(1).describe("Target platforms"),
|
|
10383
|
-
threshold:
|
|
10759
|
+
threshold: z19.number().min(0).max(35).default(26).describe(
|
|
10384
10760
|
"Minimum total score to pass (max 35, scored across 7 categories at 0-5 each). Default 26 (~75%). Use 20 for rough drafts, 28+ for final posts going to large audiences."
|
|
10385
10761
|
),
|
|
10386
|
-
brand_keyword:
|
|
10387
|
-
brand_avoid_patterns:
|
|
10388
|
-
custom_banned_terms:
|
|
10389
|
-
response_format:
|
|
10762
|
+
brand_keyword: z19.string().optional().describe("Brand keyword for alignment check"),
|
|
10763
|
+
brand_avoid_patterns: z19.array(z19.string()).optional(),
|
|
10764
|
+
custom_banned_terms: z19.array(z19.string()).optional(),
|
|
10765
|
+
response_format: z19.enum(["text", "json"]).default("text")
|
|
10390
10766
|
},
|
|
10391
10767
|
async ({
|
|
10392
10768
|
caption,
|
|
@@ -10417,7 +10793,7 @@ function registerQualityTools(server2) {
|
|
|
10417
10793
|
});
|
|
10418
10794
|
if (response_format === "json") {
|
|
10419
10795
|
return {
|
|
10420
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
10796
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope15(result), null, 2) }],
|
|
10421
10797
|
isError: false
|
|
10422
10798
|
};
|
|
10423
10799
|
}
|
|
@@ -10445,20 +10821,20 @@ function registerQualityTools(server2) {
|
|
|
10445
10821
|
"quality_check_plan",
|
|
10446
10822
|
"Batch quality check all posts in a content plan. Returns per-post scores and aggregate pass/fail summary. Use after plan_content_week and before schedule_content_plan to catch low-quality posts before publishing.",
|
|
10447
10823
|
{
|
|
10448
|
-
plan:
|
|
10449
|
-
posts:
|
|
10450
|
-
|
|
10451
|
-
id:
|
|
10452
|
-
caption:
|
|
10453
|
-
title:
|
|
10454
|
-
platform:
|
|
10824
|
+
plan: z19.object({
|
|
10825
|
+
posts: z19.array(
|
|
10826
|
+
z19.object({
|
|
10827
|
+
id: z19.string(),
|
|
10828
|
+
caption: z19.string(),
|
|
10829
|
+
title: z19.string().optional(),
|
|
10830
|
+
platform: z19.string()
|
|
10455
10831
|
})
|
|
10456
10832
|
)
|
|
10457
10833
|
}).passthrough().describe("Content plan with posts array"),
|
|
10458
|
-
threshold:
|
|
10834
|
+
threshold: z19.number().min(0).max(35).default(26).describe(
|
|
10459
10835
|
"Minimum total score to pass (max 35, scored across 7 categories at 0-5 each). Default 26 (~75%). Use 20 for rough drafts, 28+ for final posts going to large audiences."
|
|
10460
10836
|
),
|
|
10461
|
-
response_format:
|
|
10837
|
+
response_format: z19.enum(["text", "json"]).default("text")
|
|
10462
10838
|
},
|
|
10463
10839
|
async ({ plan, threshold, response_format }) => {
|
|
10464
10840
|
const startedAt = Date.now();
|
|
@@ -10500,7 +10876,7 @@ function registerQualityTools(server2) {
|
|
|
10500
10876
|
content: [
|
|
10501
10877
|
{
|
|
10502
10878
|
type: "text",
|
|
10503
|
-
text: JSON.stringify(
|
|
10879
|
+
text: JSON.stringify(asEnvelope15({ posts: postsWithQuality, summary }), null, 2)
|
|
10504
10880
|
}
|
|
10505
10881
|
],
|
|
10506
10882
|
isError: false
|
|
@@ -10525,10 +10901,10 @@ function registerQualityTools(server2) {
|
|
|
10525
10901
|
|
|
10526
10902
|
// src/tools/planning.ts
|
|
10527
10903
|
init_edge_function();
|
|
10904
|
+
import { z as z20 } from "zod";
|
|
10905
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10528
10906
|
init_supabase();
|
|
10529
10907
|
init_version();
|
|
10530
|
-
import { z as z19 } from "zod";
|
|
10531
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10532
10908
|
|
|
10533
10909
|
// src/lib/parse-utils.ts
|
|
10534
10910
|
function extractJsonArray(text) {
|
|
@@ -10554,7 +10930,7 @@ function extractJsonArray(text) {
|
|
|
10554
10930
|
function toRecord(value) {
|
|
10555
10931
|
return value && typeof value === "object" ? value : void 0;
|
|
10556
10932
|
}
|
|
10557
|
-
function
|
|
10933
|
+
function asEnvelope16(data) {
|
|
10558
10934
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
10559
10935
|
}
|
|
10560
10936
|
function tomorrowIsoDate() {
|
|
@@ -10624,10 +11000,10 @@ function registerPlanningTools(server2) {
|
|
|
10624
11000
|
"plan_content_week",
|
|
10625
11001
|
"Generate a full content plan with platform-specific drafts, hooks, angles, and optimal schedule times. Pass a topic or source_url \u2014 brand context and performance insights auto-load via project_id. Output feeds directly into quality_check_plan then schedule_content_plan. Costs ~5-15 credits depending on post count.",
|
|
10626
11002
|
{
|
|
10627
|
-
topic:
|
|
10628
|
-
source_url:
|
|
10629
|
-
platforms:
|
|
10630
|
-
|
|
11003
|
+
topic: z20.string().describe("Main topic or content theme"),
|
|
11004
|
+
source_url: z20.string().optional().describe("URL to extract content from (YouTube, article)"),
|
|
11005
|
+
platforms: z20.array(
|
|
11006
|
+
z20.enum([
|
|
10631
11007
|
"youtube",
|
|
10632
11008
|
"tiktok",
|
|
10633
11009
|
"instagram",
|
|
@@ -10638,12 +11014,12 @@ function registerPlanningTools(server2) {
|
|
|
10638
11014
|
"bluesky"
|
|
10639
11015
|
])
|
|
10640
11016
|
).min(1).describe("Target platforms"),
|
|
10641
|
-
posts_per_day:
|
|
10642
|
-
days:
|
|
10643
|
-
start_date:
|
|
10644
|
-
brand_voice:
|
|
10645
|
-
project_id:
|
|
10646
|
-
response_format:
|
|
11017
|
+
posts_per_day: z20.number().min(1).max(5).default(1).describe("Posts per platform per day"),
|
|
11018
|
+
days: z20.number().min(1).max(7).default(5).describe("Number of days to plan"),
|
|
11019
|
+
start_date: z20.string().optional().describe("ISO date, defaults to tomorrow"),
|
|
11020
|
+
brand_voice: z20.string().optional().describe("Override brand voice description"),
|
|
11021
|
+
project_id: z20.string().optional().describe("Project ID for brand/insights context"),
|
|
11022
|
+
response_format: z20.enum(["text", "json"]).default("json")
|
|
10647
11023
|
},
|
|
10648
11024
|
async ({
|
|
10649
11025
|
topic,
|
|
@@ -10895,7 +11271,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10895
11271
|
}
|
|
10896
11272
|
} catch (persistErr) {
|
|
10897
11273
|
const durationMs2 = Date.now() - startedAt;
|
|
10898
|
-
const message =
|
|
11274
|
+
const message = sanitizeError2(persistErr);
|
|
10899
11275
|
logMcpToolInvocation({
|
|
10900
11276
|
toolName: "plan_content_week",
|
|
10901
11277
|
status: "error",
|
|
@@ -10917,7 +11293,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10917
11293
|
});
|
|
10918
11294
|
if (response_format === "json") {
|
|
10919
11295
|
return {
|
|
10920
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
11296
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope16(plan), null, 2) }],
|
|
10921
11297
|
isError: false
|
|
10922
11298
|
};
|
|
10923
11299
|
}
|
|
@@ -10927,7 +11303,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10927
11303
|
};
|
|
10928
11304
|
} catch (err) {
|
|
10929
11305
|
const durationMs = Date.now() - startedAt;
|
|
10930
|
-
const message =
|
|
11306
|
+
const message = sanitizeError2(err);
|
|
10931
11307
|
logMcpToolInvocation({
|
|
10932
11308
|
toolName: "plan_content_week",
|
|
10933
11309
|
status: "error",
|
|
@@ -10945,13 +11321,13 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10945
11321
|
"save_content_plan",
|
|
10946
11322
|
"Save a content plan to the database for team review, approval workflows, and scheduled publishing. Creates a plan_id you can reference in get_content_plan, update_content_plan, and schedule_content_plan.",
|
|
10947
11323
|
{
|
|
10948
|
-
plan:
|
|
10949
|
-
topic:
|
|
10950
|
-
posts:
|
|
11324
|
+
plan: z20.object({
|
|
11325
|
+
topic: z20.string(),
|
|
11326
|
+
posts: z20.array(z20.record(z20.string(), z20.unknown()))
|
|
10951
11327
|
}).passthrough(),
|
|
10952
|
-
project_id:
|
|
10953
|
-
status:
|
|
10954
|
-
response_format:
|
|
11328
|
+
project_id: z20.string().uuid().optional(),
|
|
11329
|
+
status: z20.enum(["draft", "in_review", "approved", "scheduled", "completed"]).default("draft"),
|
|
11330
|
+
response_format: z20.enum(["text", "json"]).default("json")
|
|
10955
11331
|
},
|
|
10956
11332
|
async ({ plan, project_id, status, response_format }) => {
|
|
10957
11333
|
const startedAt = Date.now();
|
|
@@ -11007,7 +11383,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
11007
11383
|
};
|
|
11008
11384
|
if (response_format === "json") {
|
|
11009
11385
|
return {
|
|
11010
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
11386
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope16(result), null, 2) }],
|
|
11011
11387
|
isError: false
|
|
11012
11388
|
};
|
|
11013
11389
|
}
|
|
@@ -11019,7 +11395,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
11019
11395
|
};
|
|
11020
11396
|
} catch (err) {
|
|
11021
11397
|
const durationMs = Date.now() - startedAt;
|
|
11022
|
-
const message =
|
|
11398
|
+
const message = sanitizeError2(err);
|
|
11023
11399
|
logMcpToolInvocation({
|
|
11024
11400
|
toolName: "save_content_plan",
|
|
11025
11401
|
status: "error",
|
|
@@ -11037,8 +11413,8 @@ ${rawText.slice(0, 1e3)}`
|
|
|
11037
11413
|
"get_content_plan",
|
|
11038
11414
|
"Retrieve a persisted content plan by ID.",
|
|
11039
11415
|
{
|
|
11040
|
-
plan_id:
|
|
11041
|
-
response_format:
|
|
11416
|
+
plan_id: z20.string().uuid().describe("Persisted content plan ID"),
|
|
11417
|
+
response_format: z20.enum(["text", "json"]).default("json")
|
|
11042
11418
|
},
|
|
11043
11419
|
async ({ plan_id, response_format }) => {
|
|
11044
11420
|
const { data: result, error } = await callEdgeFunction("mcp-data", { action: "get-content-plan", plan_id }, { timeoutMs: 1e4 });
|
|
@@ -11073,7 +11449,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
11073
11449
|
};
|
|
11074
11450
|
if (response_format === "json") {
|
|
11075
11451
|
return {
|
|
11076
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
11452
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope16(payload), null, 2) }],
|
|
11077
11453
|
isError: false
|
|
11078
11454
|
};
|
|
11079
11455
|
}
|
|
@@ -11091,23 +11467,23 @@ ${rawText.slice(0, 1e3)}`
|
|
|
11091
11467
|
"update_content_plan",
|
|
11092
11468
|
"Update individual posts in a persisted content plan.",
|
|
11093
11469
|
{
|
|
11094
|
-
plan_id:
|
|
11095
|
-
post_updates:
|
|
11096
|
-
|
|
11097
|
-
post_id:
|
|
11098
|
-
caption:
|
|
11099
|
-
title:
|
|
11100
|
-
hashtags:
|
|
11101
|
-
hook:
|
|
11102
|
-
angle:
|
|
11103
|
-
visual_direction:
|
|
11104
|
-
media_url:
|
|
11105
|
-
schedule_at:
|
|
11106
|
-
platform:
|
|
11107
|
-
status:
|
|
11470
|
+
plan_id: z20.string().uuid(),
|
|
11471
|
+
post_updates: z20.array(
|
|
11472
|
+
z20.object({
|
|
11473
|
+
post_id: z20.string(),
|
|
11474
|
+
caption: z20.string().optional(),
|
|
11475
|
+
title: z20.string().optional(),
|
|
11476
|
+
hashtags: z20.array(z20.string()).optional(),
|
|
11477
|
+
hook: z20.string().optional(),
|
|
11478
|
+
angle: z20.string().optional(),
|
|
11479
|
+
visual_direction: z20.string().optional(),
|
|
11480
|
+
media_url: z20.string().optional(),
|
|
11481
|
+
schedule_at: z20.string().optional(),
|
|
11482
|
+
platform: z20.string().optional(),
|
|
11483
|
+
status: z20.enum(["approved", "rejected", "needs_edit"]).optional()
|
|
11108
11484
|
})
|
|
11109
11485
|
).min(1),
|
|
11110
|
-
response_format:
|
|
11486
|
+
response_format: z20.enum(["text", "json"]).default("json")
|
|
11111
11487
|
},
|
|
11112
11488
|
async ({ plan_id, post_updates, response_format }) => {
|
|
11113
11489
|
const { data: result, error } = await callEdgeFunction(
|
|
@@ -11145,7 +11521,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
11145
11521
|
};
|
|
11146
11522
|
if (response_format === "json") {
|
|
11147
11523
|
return {
|
|
11148
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
11524
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope16(payload), null, 2) }],
|
|
11149
11525
|
isError: false
|
|
11150
11526
|
};
|
|
11151
11527
|
}
|
|
@@ -11164,8 +11540,8 @@ ${rawText.slice(0, 1e3)}`
|
|
|
11164
11540
|
"submit_content_plan_for_approval",
|
|
11165
11541
|
"Create pending approval items for each post in a plan and mark plan status as in_review.",
|
|
11166
11542
|
{
|
|
11167
|
-
plan_id:
|
|
11168
|
-
response_format:
|
|
11543
|
+
plan_id: z20.string().uuid(),
|
|
11544
|
+
response_format: z20.enum(["text", "json"]).default("json")
|
|
11169
11545
|
},
|
|
11170
11546
|
async ({ plan_id, response_format }) => {
|
|
11171
11547
|
const { data: result, error } = await callEdgeFunction("mcp-data", { action: "submit-plan-approval", plan_id }, { timeoutMs: 15e3 });
|
|
@@ -11198,7 +11574,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
11198
11574
|
};
|
|
11199
11575
|
if (response_format === "json") {
|
|
11200
11576
|
return {
|
|
11201
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
11577
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope16(payload), null, 2) }],
|
|
11202
11578
|
isError: false
|
|
11203
11579
|
};
|
|
11204
11580
|
}
|
|
@@ -11219,8 +11595,8 @@ ${rawText.slice(0, 1e3)}`
|
|
|
11219
11595
|
init_edge_function();
|
|
11220
11596
|
init_supabase();
|
|
11221
11597
|
init_version();
|
|
11222
|
-
import { z as
|
|
11223
|
-
function
|
|
11598
|
+
import { z as z21 } from "zod";
|
|
11599
|
+
function asEnvelope17(data) {
|
|
11224
11600
|
return {
|
|
11225
11601
|
_meta: {
|
|
11226
11602
|
version: MCP_VERSION,
|
|
@@ -11234,19 +11610,19 @@ function registerPlanApprovalTools(server2) {
|
|
|
11234
11610
|
"create_plan_approvals",
|
|
11235
11611
|
"Create pending approval rows for each post in a content plan.",
|
|
11236
11612
|
{
|
|
11237
|
-
plan_id:
|
|
11238
|
-
posts:
|
|
11239
|
-
|
|
11240
|
-
id:
|
|
11241
|
-
platform:
|
|
11242
|
-
caption:
|
|
11243
|
-
title:
|
|
11244
|
-
media_url:
|
|
11245
|
-
schedule_at:
|
|
11613
|
+
plan_id: z21.string().uuid().describe("Content plan ID"),
|
|
11614
|
+
posts: z21.array(
|
|
11615
|
+
z21.object({
|
|
11616
|
+
id: z21.string(),
|
|
11617
|
+
platform: z21.string().optional(),
|
|
11618
|
+
caption: z21.string().optional(),
|
|
11619
|
+
title: z21.string().optional(),
|
|
11620
|
+
media_url: z21.string().optional(),
|
|
11621
|
+
schedule_at: z21.string().optional()
|
|
11246
11622
|
}).passthrough()
|
|
11247
11623
|
).min(1).describe("Posts to create approval entries for."),
|
|
11248
|
-
project_id:
|
|
11249
|
-
response_format:
|
|
11624
|
+
project_id: z21.string().uuid().optional().describe("Project ID. Defaults to active project context."),
|
|
11625
|
+
response_format: z21.enum(["text", "json"]).optional()
|
|
11250
11626
|
},
|
|
11251
11627
|
async ({ plan_id, posts, project_id, response_format }) => {
|
|
11252
11628
|
const projectId = project_id || await getDefaultProjectId();
|
|
@@ -11295,7 +11671,7 @@ function registerPlanApprovalTools(server2) {
|
|
|
11295
11671
|
};
|
|
11296
11672
|
if ((response_format || "text") === "json") {
|
|
11297
11673
|
return {
|
|
11298
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
11674
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope17(payload), null, 2) }],
|
|
11299
11675
|
isError: false
|
|
11300
11676
|
};
|
|
11301
11677
|
}
|
|
@@ -11314,9 +11690,9 @@ function registerPlanApprovalTools(server2) {
|
|
|
11314
11690
|
"list_plan_approvals",
|
|
11315
11691
|
"List MCP-native approval items for a specific content plan.",
|
|
11316
11692
|
{
|
|
11317
|
-
plan_id:
|
|
11318
|
-
status:
|
|
11319
|
-
response_format:
|
|
11693
|
+
plan_id: z21.string().uuid().describe("Content plan ID"),
|
|
11694
|
+
status: z21.enum(["pending", "approved", "rejected", "edited"]).optional(),
|
|
11695
|
+
response_format: z21.enum(["text", "json"]).optional()
|
|
11320
11696
|
},
|
|
11321
11697
|
async ({ plan_id, status, response_format }) => {
|
|
11322
11698
|
const { data: result, error } = await callEdgeFunction(
|
|
@@ -11347,7 +11723,7 @@ function registerPlanApprovalTools(server2) {
|
|
|
11347
11723
|
};
|
|
11348
11724
|
if ((response_format || "text") === "json") {
|
|
11349
11725
|
return {
|
|
11350
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
11726
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope17(payload), null, 2) }],
|
|
11351
11727
|
isError: false
|
|
11352
11728
|
};
|
|
11353
11729
|
}
|
|
@@ -11374,11 +11750,11 @@ function registerPlanApprovalTools(server2) {
|
|
|
11374
11750
|
"respond_plan_approval",
|
|
11375
11751
|
"Approve, reject, or edit a pending plan approval item.",
|
|
11376
11752
|
{
|
|
11377
|
-
approval_id:
|
|
11378
|
-
decision:
|
|
11379
|
-
edited_post:
|
|
11380
|
-
reason:
|
|
11381
|
-
response_format:
|
|
11753
|
+
approval_id: z21.string().uuid().describe("Approval item ID"),
|
|
11754
|
+
decision: z21.enum(["approved", "rejected", "edited"]),
|
|
11755
|
+
edited_post: z21.record(z21.string(), z21.unknown()).optional(),
|
|
11756
|
+
reason: z21.string().max(1e3).optional(),
|
|
11757
|
+
response_format: z21.enum(["text", "json"]).optional()
|
|
11382
11758
|
},
|
|
11383
11759
|
async ({ approval_id, decision, edited_post, reason, response_format }) => {
|
|
11384
11760
|
if (decision === "edited" && !edited_post) {
|
|
@@ -11425,7 +11801,7 @@ function registerPlanApprovalTools(server2) {
|
|
|
11425
11801
|
}
|
|
11426
11802
|
if ((response_format || "text") === "json") {
|
|
11427
11803
|
return {
|
|
11428
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
11804
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope17(data), null, 2) }],
|
|
11429
11805
|
isError: false
|
|
11430
11806
|
};
|
|
11431
11807
|
}
|
|
@@ -11444,16 +11820,16 @@ function registerPlanApprovalTools(server2) {
|
|
|
11444
11820
|
|
|
11445
11821
|
// src/tools/discovery.ts
|
|
11446
11822
|
init_tool_catalog();
|
|
11447
|
-
import { z as
|
|
11823
|
+
import { z as z22 } from "zod";
|
|
11448
11824
|
function registerDiscoveryTools(server2) {
|
|
11449
11825
|
server2.tool(
|
|
11450
11826
|
"search_tools",
|
|
11451
11827
|
'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.',
|
|
11452
11828
|
{
|
|
11453
|
-
query:
|
|
11454
|
-
module:
|
|
11455
|
-
scope:
|
|
11456
|
-
detail:
|
|
11829
|
+
query: z22.string().optional().describe("Search query to filter tools by name or description"),
|
|
11830
|
+
module: z22.string().optional().describe('Filter by module name (e.g. "planning", "content", "analytics")'),
|
|
11831
|
+
scope: z22.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
|
|
11832
|
+
detail: z22.enum(["name", "summary", "full"]).default("summary").describe(
|
|
11457
11833
|
'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
|
|
11458
11834
|
)
|
|
11459
11835
|
},
|
|
@@ -11497,15 +11873,15 @@ function registerDiscoveryTools(server2) {
|
|
|
11497
11873
|
|
|
11498
11874
|
// src/tools/pipeline.ts
|
|
11499
11875
|
init_edge_function();
|
|
11876
|
+
import { z as z23 } from "zod";
|
|
11877
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
11500
11878
|
init_supabase();
|
|
11501
11879
|
init_quality();
|
|
11502
11880
|
init_version();
|
|
11503
|
-
|
|
11504
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
11505
|
-
function asEnvelope17(data) {
|
|
11881
|
+
function asEnvelope18(data) {
|
|
11506
11882
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
11507
11883
|
}
|
|
11508
|
-
var PLATFORM_ENUM2 =
|
|
11884
|
+
var PLATFORM_ENUM2 = z23.enum([
|
|
11509
11885
|
"youtube",
|
|
11510
11886
|
"tiktok",
|
|
11511
11887
|
"instagram",
|
|
@@ -11522,10 +11898,10 @@ function registerPipelineTools(server2) {
|
|
|
11522
11898
|
"check_pipeline_readiness",
|
|
11523
11899
|
"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.",
|
|
11524
11900
|
{
|
|
11525
|
-
project_id:
|
|
11526
|
-
platforms:
|
|
11527
|
-
estimated_posts:
|
|
11528
|
-
response_format:
|
|
11901
|
+
project_id: z23.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
|
|
11902
|
+
platforms: z23.array(PLATFORM_ENUM2).min(1).describe("Target platforms to check"),
|
|
11903
|
+
estimated_posts: z23.number().min(1).max(50).default(5).describe("Estimated posts to generate"),
|
|
11904
|
+
response_format: z23.enum(["text", "json"]).optional().describe("Response format")
|
|
11529
11905
|
},
|
|
11530
11906
|
async ({ project_id, platforms, estimated_posts, response_format }) => {
|
|
11531
11907
|
const format = response_format ?? "text";
|
|
@@ -11601,7 +11977,7 @@ function registerPipelineTools(server2) {
|
|
|
11601
11977
|
});
|
|
11602
11978
|
if (format === "json") {
|
|
11603
11979
|
return {
|
|
11604
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
11980
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope18(result), null, 2) }]
|
|
11605
11981
|
};
|
|
11606
11982
|
}
|
|
11607
11983
|
const lines = [];
|
|
@@ -11631,7 +12007,7 @@ function registerPipelineTools(server2) {
|
|
|
11631
12007
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
11632
12008
|
} catch (err) {
|
|
11633
12009
|
const durationMs = Date.now() - startedAt;
|
|
11634
|
-
const message =
|
|
12010
|
+
const message = sanitizeError2(err);
|
|
11635
12011
|
logMcpToolInvocation({
|
|
11636
12012
|
toolName: "check_pipeline_readiness",
|
|
11637
12013
|
status: "error",
|
|
@@ -11649,22 +12025,22 @@ function registerPipelineTools(server2) {
|
|
|
11649
12025
|
"run_content_pipeline",
|
|
11650
12026
|
"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.",
|
|
11651
12027
|
{
|
|
11652
|
-
project_id:
|
|
11653
|
-
topic:
|
|
11654
|
-
source_url:
|
|
11655
|
-
platforms:
|
|
11656
|
-
days:
|
|
11657
|
-
posts_per_day:
|
|
11658
|
-
approval_mode:
|
|
12028
|
+
project_id: z23.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
|
|
12029
|
+
topic: z23.string().optional().describe("Content topic (required if no source_url)"),
|
|
12030
|
+
source_url: z23.string().optional().describe("URL to extract content from"),
|
|
12031
|
+
platforms: z23.array(PLATFORM_ENUM2).min(1).describe("Target platforms"),
|
|
12032
|
+
days: z23.number().min(1).max(7).default(5).describe("Days to plan"),
|
|
12033
|
+
posts_per_day: z23.number().min(1).max(3).default(1).describe("Posts per platform per day"),
|
|
12034
|
+
approval_mode: z23.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe(
|
|
11659
12035
|
"auto: approve all passing quality. review_all: flag everything. review_low_confidence: auto-approve high scorers."
|
|
11660
12036
|
),
|
|
11661
|
-
auto_approve_threshold:
|
|
12037
|
+
auto_approve_threshold: z23.number().min(0).max(35).default(28).describe(
|
|
11662
12038
|
"Quality score threshold for auto-approval (used in auto/review_low_confidence modes)"
|
|
11663
12039
|
),
|
|
11664
|
-
max_credits:
|
|
11665
|
-
dry_run:
|
|
11666
|
-
skip_stages:
|
|
11667
|
-
response_format:
|
|
12040
|
+
max_credits: z23.number().optional().describe("Credit budget cap"),
|
|
12041
|
+
dry_run: z23.boolean().default(false).describe("If true, skip scheduling and return plan only"),
|
|
12042
|
+
skip_stages: z23.array(z23.enum(["research", "quality", "schedule"])).optional().describe("Stages to skip"),
|
|
12043
|
+
response_format: z23.enum(["text", "json"]).default("json")
|
|
11668
12044
|
},
|
|
11669
12045
|
async ({
|
|
11670
12046
|
project_id,
|
|
@@ -11792,7 +12168,7 @@ function registerPipelineTools(server2) {
|
|
|
11792
12168
|
} catch (deductErr) {
|
|
11793
12169
|
errors.push({
|
|
11794
12170
|
stage: "planning",
|
|
11795
|
-
message: `Credit deduction failed: ${
|
|
12171
|
+
message: `Credit deduction failed: ${sanitizeError2(deductErr)}`
|
|
11796
12172
|
});
|
|
11797
12173
|
}
|
|
11798
12174
|
}
|
|
@@ -11963,7 +12339,7 @@ function registerPipelineTools(server2) {
|
|
|
11963
12339
|
} catch (schedErr) {
|
|
11964
12340
|
errors.push({
|
|
11965
12341
|
stage: "schedule",
|
|
11966
|
-
message: `Failed to schedule ${post.id}: ${
|
|
12342
|
+
message: `Failed to schedule ${post.id}: ${sanitizeError2(schedErr)}`
|
|
11967
12343
|
});
|
|
11968
12344
|
}
|
|
11969
12345
|
}
|
|
@@ -12026,7 +12402,7 @@ function registerPipelineTools(server2) {
|
|
|
12026
12402
|
if (response_format === "json") {
|
|
12027
12403
|
return {
|
|
12028
12404
|
content: [
|
|
12029
|
-
{ type: "text", text: JSON.stringify(
|
|
12405
|
+
{ type: "text", text: JSON.stringify(asEnvelope18(resultPayload), null, 2) }
|
|
12030
12406
|
]
|
|
12031
12407
|
};
|
|
12032
12408
|
}
|
|
@@ -12052,7 +12428,7 @@ function registerPipelineTools(server2) {
|
|
|
12052
12428
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
12053
12429
|
} catch (err) {
|
|
12054
12430
|
const durationMs = Date.now() - startedAt;
|
|
12055
|
-
const message =
|
|
12431
|
+
const message = sanitizeError2(err);
|
|
12056
12432
|
logMcpToolInvocation({
|
|
12057
12433
|
toolName: "run_content_pipeline",
|
|
12058
12434
|
status: "error",
|
|
@@ -12087,8 +12463,8 @@ function registerPipelineTools(server2) {
|
|
|
12087
12463
|
"get_pipeline_status",
|
|
12088
12464
|
"Check status of a pipeline run, including stages completed, pending approvals, and scheduled posts.",
|
|
12089
12465
|
{
|
|
12090
|
-
pipeline_id:
|
|
12091
|
-
response_format:
|
|
12466
|
+
pipeline_id: z23.string().uuid().optional().describe("Pipeline run ID (omit for latest)"),
|
|
12467
|
+
response_format: z23.enum(["text", "json"]).optional()
|
|
12092
12468
|
},
|
|
12093
12469
|
async ({ pipeline_id, response_format }) => {
|
|
12094
12470
|
const format = response_format ?? "text";
|
|
@@ -12114,7 +12490,7 @@ function registerPipelineTools(server2) {
|
|
|
12114
12490
|
}
|
|
12115
12491
|
if (format === "json") {
|
|
12116
12492
|
return {
|
|
12117
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
12493
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope18(data), null, 2) }]
|
|
12118
12494
|
};
|
|
12119
12495
|
}
|
|
12120
12496
|
const lines = [];
|
|
@@ -12148,9 +12524,9 @@ function registerPipelineTools(server2) {
|
|
|
12148
12524
|
"auto_approve_plan",
|
|
12149
12525
|
"Batch auto-approve posts in a content plan that meet quality thresholds. Posts below the threshold are flagged for manual review.",
|
|
12150
12526
|
{
|
|
12151
|
-
plan_id:
|
|
12152
|
-
quality_threshold:
|
|
12153
|
-
response_format:
|
|
12527
|
+
plan_id: z23.string().uuid().describe("Content plan ID"),
|
|
12528
|
+
quality_threshold: z23.number().min(0).max(35).default(26).describe("Minimum quality score to auto-approve"),
|
|
12529
|
+
response_format: z23.enum(["text", "json"]).default("json")
|
|
12154
12530
|
},
|
|
12155
12531
|
async ({ plan_id, quality_threshold, response_format }) => {
|
|
12156
12532
|
const startedAt = Date.now();
|
|
@@ -12256,7 +12632,7 @@ function registerPipelineTools(server2) {
|
|
|
12256
12632
|
if (response_format === "json") {
|
|
12257
12633
|
return {
|
|
12258
12634
|
content: [
|
|
12259
|
-
{ type: "text", text: JSON.stringify(
|
|
12635
|
+
{ type: "text", text: JSON.stringify(asEnvelope18(resultPayload), null, 2) }
|
|
12260
12636
|
]
|
|
12261
12637
|
};
|
|
12262
12638
|
}
|
|
@@ -12276,7 +12652,7 @@ function registerPipelineTools(server2) {
|
|
|
12276
12652
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
12277
12653
|
} catch (err) {
|
|
12278
12654
|
const durationMs = Date.now() - startedAt;
|
|
12279
|
-
const message =
|
|
12655
|
+
const message = sanitizeError2(err);
|
|
12280
12656
|
logMcpToolInvocation({
|
|
12281
12657
|
toolName: "auto_approve_plan",
|
|
12282
12658
|
status: "error",
|
|
@@ -12311,10 +12687,10 @@ function buildPlanPrompt(topic, platforms, days, postsPerDay, sourceUrl) {
|
|
|
12311
12687
|
|
|
12312
12688
|
// src/tools/suggest.ts
|
|
12313
12689
|
init_edge_function();
|
|
12690
|
+
import { z as z24 } from "zod";
|
|
12314
12691
|
init_supabase();
|
|
12315
12692
|
init_version();
|
|
12316
|
-
|
|
12317
|
-
function asEnvelope18(data) {
|
|
12693
|
+
function asEnvelope19(data) {
|
|
12318
12694
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
12319
12695
|
}
|
|
12320
12696
|
function registerSuggestTools(server2) {
|
|
@@ -12322,9 +12698,9 @@ function registerSuggestTools(server2) {
|
|
|
12322
12698
|
"suggest_next_content",
|
|
12323
12699
|
"Suggest next content topics based on performance insights, past content, and competitor patterns. No AI call, no credit cost \u2014 purely data-driven recommendations.",
|
|
12324
12700
|
{
|
|
12325
|
-
project_id:
|
|
12326
|
-
count:
|
|
12327
|
-
response_format:
|
|
12701
|
+
project_id: z24.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
|
|
12702
|
+
count: z24.number().min(1).max(10).default(3).describe("Number of suggestions to return"),
|
|
12703
|
+
response_format: z24.enum(["text", "json"]).optional()
|
|
12328
12704
|
},
|
|
12329
12705
|
async ({ project_id, count, response_format }) => {
|
|
12330
12706
|
const format = response_format ?? "text";
|
|
@@ -12434,7 +12810,7 @@ function registerSuggestTools(server2) {
|
|
|
12434
12810
|
if (format === "json") {
|
|
12435
12811
|
return {
|
|
12436
12812
|
content: [
|
|
12437
|
-
{ type: "text", text: JSON.stringify(
|
|
12813
|
+
{ type: "text", text: JSON.stringify(asEnvelope19(resultPayload), null, 2) }
|
|
12438
12814
|
]
|
|
12439
12815
|
};
|
|
12440
12816
|
}
|
|
@@ -12456,7 +12832,7 @@ ${i + 1}. ${s.topic}`);
|
|
|
12456
12832
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
12457
12833
|
} catch (err) {
|
|
12458
12834
|
const durationMs = Date.now() - startedAt;
|
|
12459
|
-
const message =
|
|
12835
|
+
const message = sanitizeError2(err);
|
|
12460
12836
|
logMcpToolInvocation({
|
|
12461
12837
|
toolName: "suggest_next_content",
|
|
12462
12838
|
status: "error",
|
|
@@ -12474,8 +12850,8 @@ ${i + 1}. ${s.topic}`);
|
|
|
12474
12850
|
|
|
12475
12851
|
// src/tools/digest.ts
|
|
12476
12852
|
init_edge_function();
|
|
12853
|
+
import { z as z25 } from "zod";
|
|
12477
12854
|
init_supabase();
|
|
12478
|
-
import { z as z24 } from "zod";
|
|
12479
12855
|
|
|
12480
12856
|
// src/lib/anomaly-detector.ts
|
|
12481
12857
|
var SENSITIVITY_THRESHOLDS = {
|
|
@@ -12579,10 +12955,10 @@ function detectAnomalies(currentData, previousData, sensitivity = "medium", aver
|
|
|
12579
12955
|
|
|
12580
12956
|
// src/tools/digest.ts
|
|
12581
12957
|
init_version();
|
|
12582
|
-
function
|
|
12958
|
+
function asEnvelope20(data) {
|
|
12583
12959
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
12584
12960
|
}
|
|
12585
|
-
var PLATFORM_ENUM3 =
|
|
12961
|
+
var PLATFORM_ENUM3 = z25.enum([
|
|
12586
12962
|
"youtube",
|
|
12587
12963
|
"tiktok",
|
|
12588
12964
|
"instagram",
|
|
@@ -12597,10 +12973,10 @@ function registerDigestTools(server2) {
|
|
|
12597
12973
|
"generate_performance_digest",
|
|
12598
12974
|
"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.",
|
|
12599
12975
|
{
|
|
12600
|
-
project_id:
|
|
12601
|
-
period:
|
|
12602
|
-
include_recommendations:
|
|
12603
|
-
response_format:
|
|
12976
|
+
project_id: z25.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
|
|
12977
|
+
period: z25.enum(["7d", "14d", "30d"]).default("7d").describe("Time period to analyze"),
|
|
12978
|
+
include_recommendations: z25.boolean().default(true),
|
|
12979
|
+
response_format: z25.enum(["text", "json"]).optional()
|
|
12604
12980
|
},
|
|
12605
12981
|
async ({ project_id, period, include_recommendations, response_format }) => {
|
|
12606
12982
|
const format = response_format ?? "text";
|
|
@@ -12756,7 +13132,7 @@ function registerDigestTools(server2) {
|
|
|
12756
13132
|
});
|
|
12757
13133
|
if (format === "json") {
|
|
12758
13134
|
return {
|
|
12759
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
13135
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope20(digest), null, 2) }]
|
|
12760
13136
|
};
|
|
12761
13137
|
}
|
|
12762
13138
|
const lines = [];
|
|
@@ -12797,7 +13173,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
|
|
|
12797
13173
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
12798
13174
|
} catch (err) {
|
|
12799
13175
|
const durationMs = Date.now() - startedAt;
|
|
12800
|
-
const message =
|
|
13176
|
+
const message = sanitizeError2(err);
|
|
12801
13177
|
logMcpToolInvocation({
|
|
12802
13178
|
toolName: "generate_performance_digest",
|
|
12803
13179
|
status: "error",
|
|
@@ -12815,11 +13191,11 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
|
|
|
12815
13191
|
"detect_anomalies",
|
|
12816
13192
|
"Detect significant performance changes: spikes, drops, viral content, trend shifts. Compares current period against previous equal-length period. No AI call, no credit cost.",
|
|
12817
13193
|
{
|
|
12818
|
-
project_id:
|
|
12819
|
-
days:
|
|
12820
|
-
sensitivity:
|
|
12821
|
-
platforms:
|
|
12822
|
-
response_format:
|
|
13194
|
+
project_id: z25.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
|
|
13195
|
+
days: z25.number().min(7).max(90).default(14).describe("Days to analyze"),
|
|
13196
|
+
sensitivity: z25.enum(["low", "medium", "high"]).default("medium").describe("Detection sensitivity: low=50%+, medium=30%+, high=15%+ changes"),
|
|
13197
|
+
platforms: z25.array(PLATFORM_ENUM3).optional().describe("Filter to specific platforms"),
|
|
13198
|
+
response_format: z25.enum(["text", "json"]).optional()
|
|
12823
13199
|
},
|
|
12824
13200
|
async ({ project_id, days, sensitivity, platforms, response_format }) => {
|
|
12825
13201
|
const format = response_format ?? "text";
|
|
@@ -12870,7 +13246,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
|
|
|
12870
13246
|
if (format === "json") {
|
|
12871
13247
|
return {
|
|
12872
13248
|
content: [
|
|
12873
|
-
{ type: "text", text: JSON.stringify(
|
|
13249
|
+
{ type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
|
|
12874
13250
|
]
|
|
12875
13251
|
};
|
|
12876
13252
|
}
|
|
@@ -12894,7 +13270,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
|
|
|
12894
13270
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
12895
13271
|
} catch (err) {
|
|
12896
13272
|
const durationMs = Date.now() - startedAt;
|
|
12897
|
-
const message =
|
|
13273
|
+
const message = sanitizeError2(err);
|
|
12898
13274
|
logMcpToolInvocation({
|
|
12899
13275
|
toolName: "detect_anomalies",
|
|
12900
13276
|
status: "error",
|
|
@@ -12914,8 +13290,450 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
|
|
|
12914
13290
|
init_edge_function();
|
|
12915
13291
|
init_supabase();
|
|
12916
13292
|
init_version();
|
|
12917
|
-
import { z as
|
|
12918
|
-
|
|
13293
|
+
import { z as z26 } from "zod";
|
|
13294
|
+
|
|
13295
|
+
// src/lib/brandScoring.ts
|
|
13296
|
+
var WEIGHTS = {
|
|
13297
|
+
toneAlignment: 0.3,
|
|
13298
|
+
vocabularyAdherence: 0.25,
|
|
13299
|
+
avoidCompliance: 0.2,
|
|
13300
|
+
audienceRelevance: 0.15,
|
|
13301
|
+
brandMentions: 0.05,
|
|
13302
|
+
structuralPatterns: 0.05
|
|
13303
|
+
};
|
|
13304
|
+
function norm(content) {
|
|
13305
|
+
return content.toLowerCase().replace(/[^a-z0-9\s]/g, " ");
|
|
13306
|
+
}
|
|
13307
|
+
function findMatches(content, terms) {
|
|
13308
|
+
const n = norm(content);
|
|
13309
|
+
return terms.filter((t) => n.includes(t.toLowerCase()));
|
|
13310
|
+
}
|
|
13311
|
+
function findMissing(content, terms) {
|
|
13312
|
+
const n = norm(content);
|
|
13313
|
+
return terms.filter((t) => !n.includes(t.toLowerCase()));
|
|
13314
|
+
}
|
|
13315
|
+
var FABRICATION_PATTERNS = [
|
|
13316
|
+
{ regex: /\b\d+[,.]?\d*\s*(%|percent)/gi, label: "unverified percentage" },
|
|
13317
|
+
{ regex: /\b(award[- ]?winning|best[- ]selling|#\s*1)\b/gi, label: "unverified ranking" },
|
|
13318
|
+
{
|
|
13319
|
+
regex: /\b(guaranteed|proven to|studies show|scientifically proven)\b/gi,
|
|
13320
|
+
label: "unverified claim"
|
|
13321
|
+
},
|
|
13322
|
+
{
|
|
13323
|
+
regex: /\b(always works|100% effective|risk[- ]?free|no risk)\b/gi,
|
|
13324
|
+
label: "absolute claim"
|
|
13325
|
+
}
|
|
13326
|
+
];
|
|
13327
|
+
function detectFabricationPatterns(content) {
|
|
13328
|
+
const matches = [];
|
|
13329
|
+
for (const { regex, label } of FABRICATION_PATTERNS) {
|
|
13330
|
+
const re = new RegExp(regex.source, regex.flags);
|
|
13331
|
+
let m;
|
|
13332
|
+
while ((m = re.exec(content)) !== null) {
|
|
13333
|
+
matches.push({ label, match: m[0] });
|
|
13334
|
+
}
|
|
13335
|
+
}
|
|
13336
|
+
return matches;
|
|
13337
|
+
}
|
|
13338
|
+
function scoreTone(content, profile) {
|
|
13339
|
+
const terms = profile.voiceProfile?.tone || [];
|
|
13340
|
+
if (!terms.length)
|
|
13341
|
+
return {
|
|
13342
|
+
score: 50,
|
|
13343
|
+
weight: WEIGHTS.toneAlignment,
|
|
13344
|
+
issues: [],
|
|
13345
|
+
suggestions: ["Define brand tone words for better consistency measurement"]
|
|
13346
|
+
};
|
|
13347
|
+
const matched = findMatches(content, terms);
|
|
13348
|
+
const missing = findMissing(content, terms);
|
|
13349
|
+
const score = Math.min(100, Math.round(matched.length / terms.length * 100));
|
|
13350
|
+
const issues = [];
|
|
13351
|
+
const suggestions = [];
|
|
13352
|
+
if (missing.length > 0) {
|
|
13353
|
+
issues.push(`Missing tone signals: ${missing.join(", ")}`);
|
|
13354
|
+
suggestions.push(`Try incorporating tone words: ${missing.slice(0, 3).join(", ")}`);
|
|
13355
|
+
}
|
|
13356
|
+
return { score, weight: WEIGHTS.toneAlignment, issues, suggestions };
|
|
13357
|
+
}
|
|
13358
|
+
function scoreVocab(content, profile) {
|
|
13359
|
+
const preferred = [
|
|
13360
|
+
...profile.voiceProfile?.languagePatterns || [],
|
|
13361
|
+
...profile.vocabularyRules?.preferredTerms || []
|
|
13362
|
+
];
|
|
13363
|
+
if (!preferred.length)
|
|
13364
|
+
return {
|
|
13365
|
+
score: 50,
|
|
13366
|
+
weight: WEIGHTS.vocabularyAdherence,
|
|
13367
|
+
issues: [],
|
|
13368
|
+
suggestions: ["Add preferred terms to improve vocabulary scoring"]
|
|
13369
|
+
};
|
|
13370
|
+
const matched = findMatches(content, preferred);
|
|
13371
|
+
const missing = findMissing(content, preferred);
|
|
13372
|
+
const score = Math.min(100, Math.round(matched.length / preferred.length * 100));
|
|
13373
|
+
const issues = [];
|
|
13374
|
+
const suggestions = [];
|
|
13375
|
+
if (missing.length > 0 && score < 60) {
|
|
13376
|
+
issues.push(`Low preferred term usage (${matched.length}/${preferred.length})`);
|
|
13377
|
+
suggestions.push(`Consider using: ${missing.slice(0, 3).join(", ")}`);
|
|
13378
|
+
}
|
|
13379
|
+
return { score, weight: WEIGHTS.vocabularyAdherence, issues, suggestions };
|
|
13380
|
+
}
|
|
13381
|
+
function scoreAvoid(content, profile) {
|
|
13382
|
+
const banned = [
|
|
13383
|
+
...profile.voiceProfile?.avoidPatterns || [],
|
|
13384
|
+
...profile.vocabularyRules?.bannedTerms || []
|
|
13385
|
+
];
|
|
13386
|
+
if (!banned.length)
|
|
13387
|
+
return {
|
|
13388
|
+
score: 100,
|
|
13389
|
+
weight: WEIGHTS.avoidCompliance,
|
|
13390
|
+
issues: [],
|
|
13391
|
+
suggestions: []
|
|
13392
|
+
};
|
|
13393
|
+
const violations = findMatches(content, banned);
|
|
13394
|
+
const score = violations.length === 0 ? 100 : Math.max(0, 100 - violations.length * 25);
|
|
13395
|
+
const issues = [];
|
|
13396
|
+
const suggestions = [];
|
|
13397
|
+
if (violations.length > 0) {
|
|
13398
|
+
issues.push(`Banned/avoided terms found: ${violations.join(", ")}`);
|
|
13399
|
+
suggestions.push(`Remove or replace: ${violations.join(", ")}`);
|
|
13400
|
+
}
|
|
13401
|
+
return { score, weight: WEIGHTS.avoidCompliance, issues, suggestions };
|
|
13402
|
+
}
|
|
13403
|
+
function scoreAudience(content, profile) {
|
|
13404
|
+
const terms = [];
|
|
13405
|
+
const d = profile.targetAudience?.demographics;
|
|
13406
|
+
const p = profile.targetAudience?.psychographics;
|
|
13407
|
+
if (d?.ageRange) terms.push(d.ageRange);
|
|
13408
|
+
if (d?.location) terms.push(d.location);
|
|
13409
|
+
if (p?.interests) terms.push(...p.interests);
|
|
13410
|
+
if (p?.painPoints) terms.push(...p.painPoints);
|
|
13411
|
+
if (p?.aspirations) terms.push(...p.aspirations);
|
|
13412
|
+
const valid = terms.filter(Boolean);
|
|
13413
|
+
if (!valid.length)
|
|
13414
|
+
return {
|
|
13415
|
+
score: 50,
|
|
13416
|
+
weight: WEIGHTS.audienceRelevance,
|
|
13417
|
+
issues: [],
|
|
13418
|
+
suggestions: ["Define target audience details for relevance scoring"]
|
|
13419
|
+
};
|
|
13420
|
+
const matched = findMatches(content, valid);
|
|
13421
|
+
const score = Math.min(100, Math.round(matched.length / valid.length * 100));
|
|
13422
|
+
const issues = [];
|
|
13423
|
+
const suggestions = [];
|
|
13424
|
+
if (score < 40) {
|
|
13425
|
+
issues.push("Content has low audience relevance");
|
|
13426
|
+
suggestions.push(
|
|
13427
|
+
`Reference audience pain points or interests: ${valid.slice(0, 3).join(", ")}`
|
|
13428
|
+
);
|
|
13429
|
+
}
|
|
13430
|
+
return { score, weight: WEIGHTS.audienceRelevance, issues, suggestions };
|
|
13431
|
+
}
|
|
13432
|
+
function scoreBrand(content, profile) {
|
|
13433
|
+
const name = profile.name?.toLowerCase();
|
|
13434
|
+
if (!name)
|
|
13435
|
+
return {
|
|
13436
|
+
score: 50,
|
|
13437
|
+
weight: WEIGHTS.brandMentions,
|
|
13438
|
+
issues: [],
|
|
13439
|
+
suggestions: []
|
|
13440
|
+
};
|
|
13441
|
+
const mentioned = norm(content).includes(name);
|
|
13442
|
+
const issues = [];
|
|
13443
|
+
const suggestions = [];
|
|
13444
|
+
if (!mentioned) {
|
|
13445
|
+
issues.push("Brand name not mentioned");
|
|
13446
|
+
suggestions.push(`Include "${profile.name}" in the content`);
|
|
13447
|
+
}
|
|
13448
|
+
return {
|
|
13449
|
+
score: mentioned ? 100 : 0,
|
|
13450
|
+
weight: WEIGHTS.brandMentions,
|
|
13451
|
+
issues,
|
|
13452
|
+
suggestions
|
|
13453
|
+
};
|
|
13454
|
+
}
|
|
13455
|
+
function scoreStructure(content, profile) {
|
|
13456
|
+
const rules = profile.writingStyleRules;
|
|
13457
|
+
if (!rules)
|
|
13458
|
+
return {
|
|
13459
|
+
score: 50,
|
|
13460
|
+
weight: WEIGHTS.structuralPatterns,
|
|
13461
|
+
issues: [],
|
|
13462
|
+
suggestions: []
|
|
13463
|
+
};
|
|
13464
|
+
let score = 100;
|
|
13465
|
+
const issues = [];
|
|
13466
|
+
const suggestions = [];
|
|
13467
|
+
if (rules.perspective) {
|
|
13468
|
+
const markers = {
|
|
13469
|
+
"first-singular": [/\bI\b/g, /\bmy\b/gi],
|
|
13470
|
+
"first-plural": [/\bwe\b/gi, /\bour\b/gi],
|
|
13471
|
+
second: [/\byou\b/gi, /\byour\b/gi],
|
|
13472
|
+
third: [/\bthey\b/gi, /\btheir\b/gi]
|
|
13473
|
+
};
|
|
13474
|
+
const expected = markers[rules.perspective];
|
|
13475
|
+
if (expected && !expected.some((r) => r.test(content))) {
|
|
13476
|
+
score -= 30;
|
|
13477
|
+
issues.push(`Expected ${rules.perspective} perspective not detected`);
|
|
13478
|
+
suggestions.push(`Use ${rules.perspective} perspective pronouns`);
|
|
13479
|
+
}
|
|
13480
|
+
}
|
|
13481
|
+
if (rules.useContractions === false) {
|
|
13482
|
+
const found = content.match(
|
|
13483
|
+
/\b(don't|won't|can't|isn't|aren't|wasn't|weren't|hasn't|haven't|doesn't|didn't|wouldn't|couldn't|shouldn't|it's|that's|there's|here's|what's|who's|let's|we're|they're|you're|I'm|he's|she's)\b/gi
|
|
13484
|
+
);
|
|
13485
|
+
if (found && found.length > 0) {
|
|
13486
|
+
score -= Math.min(40, found.length * 10);
|
|
13487
|
+
issues.push(`Contractions found (${found.length}): ${found.slice(0, 3).join(", ")}`);
|
|
13488
|
+
suggestions.push("Expand contractions to full forms");
|
|
13489
|
+
}
|
|
13490
|
+
}
|
|
13491
|
+
if (rules.emojiPolicy === "none") {
|
|
13492
|
+
const emojis = content.match(
|
|
13493
|
+
/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu
|
|
13494
|
+
);
|
|
13495
|
+
if (emojis && emojis.length > 0) {
|
|
13496
|
+
score -= 20;
|
|
13497
|
+
issues.push('Emojis found but emoji policy is "none"');
|
|
13498
|
+
suggestions.push("Remove emojis from content");
|
|
13499
|
+
}
|
|
13500
|
+
}
|
|
13501
|
+
return {
|
|
13502
|
+
score: Math.max(0, score),
|
|
13503
|
+
weight: WEIGHTS.structuralPatterns,
|
|
13504
|
+
issues,
|
|
13505
|
+
suggestions
|
|
13506
|
+
};
|
|
13507
|
+
}
|
|
13508
|
+
function computeBrandConsistency(content, profile, threshold = 60) {
|
|
13509
|
+
if (!content || !profile) {
|
|
13510
|
+
const neutral = {
|
|
13511
|
+
score: 50,
|
|
13512
|
+
weight: 0,
|
|
13513
|
+
issues: [],
|
|
13514
|
+
suggestions: []
|
|
13515
|
+
};
|
|
13516
|
+
return {
|
|
13517
|
+
overall: 50,
|
|
13518
|
+
passed: false,
|
|
13519
|
+
dimensions: {
|
|
13520
|
+
toneAlignment: { ...neutral, weight: WEIGHTS.toneAlignment },
|
|
13521
|
+
vocabularyAdherence: { ...neutral, weight: WEIGHTS.vocabularyAdherence },
|
|
13522
|
+
avoidCompliance: { ...neutral, weight: WEIGHTS.avoidCompliance },
|
|
13523
|
+
audienceRelevance: { ...neutral, weight: WEIGHTS.audienceRelevance },
|
|
13524
|
+
brandMentions: { ...neutral, weight: WEIGHTS.brandMentions },
|
|
13525
|
+
structuralPatterns: { ...neutral, weight: WEIGHTS.structuralPatterns }
|
|
13526
|
+
},
|
|
13527
|
+
preferredTermsUsed: [],
|
|
13528
|
+
bannedTermsFound: [],
|
|
13529
|
+
fabricationWarnings: []
|
|
13530
|
+
};
|
|
13531
|
+
}
|
|
13532
|
+
const dimensions = {
|
|
13533
|
+
toneAlignment: scoreTone(content, profile),
|
|
13534
|
+
vocabularyAdherence: scoreVocab(content, profile),
|
|
13535
|
+
avoidCompliance: scoreAvoid(content, profile),
|
|
13536
|
+
audienceRelevance: scoreAudience(content, profile),
|
|
13537
|
+
brandMentions: scoreBrand(content, profile),
|
|
13538
|
+
structuralPatterns: scoreStructure(content, profile)
|
|
13539
|
+
};
|
|
13540
|
+
const overall = Math.round(
|
|
13541
|
+
Object.values(dimensions).reduce((sum, d) => sum + d.score * d.weight, 0)
|
|
13542
|
+
);
|
|
13543
|
+
const preferred = [
|
|
13544
|
+
...profile.voiceProfile?.languagePatterns || [],
|
|
13545
|
+
...profile.vocabularyRules?.preferredTerms || []
|
|
13546
|
+
];
|
|
13547
|
+
const banned = [
|
|
13548
|
+
...profile.voiceProfile?.avoidPatterns || [],
|
|
13549
|
+
...profile.vocabularyRules?.bannedTerms || []
|
|
13550
|
+
];
|
|
13551
|
+
const fabrications = detectFabricationPatterns(content);
|
|
13552
|
+
return {
|
|
13553
|
+
overall: Math.max(0, Math.min(100, overall)),
|
|
13554
|
+
passed: overall >= threshold,
|
|
13555
|
+
dimensions,
|
|
13556
|
+
preferredTermsUsed: findMatches(content, preferred),
|
|
13557
|
+
bannedTermsFound: findMatches(content, banned),
|
|
13558
|
+
fabricationWarnings: fabrications.map((f) => `${f.label}: "${f.match}"`)
|
|
13559
|
+
};
|
|
13560
|
+
}
|
|
13561
|
+
|
|
13562
|
+
// src/lib/colorAudit.ts
|
|
13563
|
+
function hexToRgb(hex) {
|
|
13564
|
+
const h = hex.replace("#", "");
|
|
13565
|
+
const full = h.length === 3 ? h[0] + h[0] + h[1] + h[1] + h[2] + h[2] : h;
|
|
13566
|
+
const n = parseInt(full, 16);
|
|
13567
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
13568
|
+
}
|
|
13569
|
+
function srgbToLinear(c) {
|
|
13570
|
+
const s = c / 255;
|
|
13571
|
+
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
13572
|
+
}
|
|
13573
|
+
function hexToLab(hex) {
|
|
13574
|
+
const [r, g, b] = hexToRgb(hex);
|
|
13575
|
+
const lr = srgbToLinear(r);
|
|
13576
|
+
const lg = srgbToLinear(g);
|
|
13577
|
+
const lb = srgbToLinear(b);
|
|
13578
|
+
const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
|
|
13579
|
+
const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
|
|
13580
|
+
const z29 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
|
|
13581
|
+
const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
|
|
13582
|
+
const fx = f(x / 0.95047);
|
|
13583
|
+
const fy = f(y / 1);
|
|
13584
|
+
const fz = f(z29 / 1.08883);
|
|
13585
|
+
return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
|
|
13586
|
+
}
|
|
13587
|
+
function deltaE2000(lab1, lab2) {
|
|
13588
|
+
const { L: L1, a: a1, b: b1 } = lab1;
|
|
13589
|
+
const { L: L2, a: a2, b: b2 } = lab2;
|
|
13590
|
+
const C1 = Math.sqrt(a1 * a1 + b1 * b1);
|
|
13591
|
+
const C2 = Math.sqrt(a2 * a2 + b2 * b2);
|
|
13592
|
+
const Cab = (C1 + C2) / 2;
|
|
13593
|
+
const Cab7 = Math.pow(Cab, 7);
|
|
13594
|
+
const G = 0.5 * (1 - Math.sqrt(Cab7 / (Cab7 + Math.pow(25, 7))));
|
|
13595
|
+
const a1p = a1 * (1 + G);
|
|
13596
|
+
const a2p = a2 * (1 + G);
|
|
13597
|
+
const C1p = Math.sqrt(a1p * a1p + b1 * b1);
|
|
13598
|
+
const C2p = Math.sqrt(a2p * a2p + b2 * b2);
|
|
13599
|
+
let h1p = Math.atan2(b1, a1p) * (180 / Math.PI);
|
|
13600
|
+
if (h1p < 0) h1p += 360;
|
|
13601
|
+
let h2p = Math.atan2(b2, a2p) * (180 / Math.PI);
|
|
13602
|
+
if (h2p < 0) h2p += 360;
|
|
13603
|
+
const dLp = L2 - L1;
|
|
13604
|
+
const dCp = C2p - C1p;
|
|
13605
|
+
let dhp;
|
|
13606
|
+
if (C1p * C2p === 0) dhp = 0;
|
|
13607
|
+
else if (Math.abs(h2p - h1p) <= 180) dhp = h2p - h1p;
|
|
13608
|
+
else if (h2p - h1p > 180) dhp = h2p - h1p - 360;
|
|
13609
|
+
else dhp = h2p - h1p + 360;
|
|
13610
|
+
const dHp = 2 * Math.sqrt(C1p * C2p) * Math.sin(dhp * Math.PI / 360);
|
|
13611
|
+
const Lp = (L1 + L2) / 2;
|
|
13612
|
+
const Cp = (C1p + C2p) / 2;
|
|
13613
|
+
let hp;
|
|
13614
|
+
if (C1p * C2p === 0) hp = h1p + h2p;
|
|
13615
|
+
else if (Math.abs(h1p - h2p) <= 180) hp = (h1p + h2p) / 2;
|
|
13616
|
+
else if (h1p + h2p < 360) hp = (h1p + h2p + 360) / 2;
|
|
13617
|
+
else hp = (h1p + h2p - 360) / 2;
|
|
13618
|
+
const T = 1 - 0.17 * Math.cos((hp - 30) * Math.PI / 180) + 0.24 * Math.cos(2 * hp * Math.PI / 180) + 0.32 * Math.cos((3 * hp + 6) * Math.PI / 180) - 0.2 * Math.cos((4 * hp - 63) * Math.PI / 180);
|
|
13619
|
+
const SL = 1 + 0.015 * (Lp - 50) * (Lp - 50) / Math.sqrt(20 + (Lp - 50) * (Lp - 50));
|
|
13620
|
+
const SC = 1 + 0.045 * Cp;
|
|
13621
|
+
const SH = 1 + 0.015 * Cp * T;
|
|
13622
|
+
const Cp7 = Math.pow(Cp, 7);
|
|
13623
|
+
const RT = -2 * Math.sqrt(Cp7 / (Cp7 + Math.pow(25, 7))) * Math.sin(60 * Math.exp(-Math.pow((hp - 275) / 25, 2)) * Math.PI / 180);
|
|
13624
|
+
return Math.sqrt(
|
|
13625
|
+
Math.pow(dLp / SL, 2) + Math.pow(dCp / SC, 2) + Math.pow(dHp / SH, 2) + RT * (dCp / SC) * (dHp / SH)
|
|
13626
|
+
);
|
|
13627
|
+
}
|
|
13628
|
+
var COLOR_SLOTS = [
|
|
13629
|
+
"primary",
|
|
13630
|
+
"secondary",
|
|
13631
|
+
"accent",
|
|
13632
|
+
"background",
|
|
13633
|
+
"success",
|
|
13634
|
+
"warning",
|
|
13635
|
+
"error",
|
|
13636
|
+
"text",
|
|
13637
|
+
"textSecondary"
|
|
13638
|
+
];
|
|
13639
|
+
function auditBrandColors(palette, contentColors, threshold = 10) {
|
|
13640
|
+
if (!contentColors.length) return { entries: [], overallScore: 100, passed: true };
|
|
13641
|
+
const brandColors = [];
|
|
13642
|
+
for (const slot of COLOR_SLOTS) {
|
|
13643
|
+
const hex = palette[slot];
|
|
13644
|
+
if (typeof hex === "string" && hex.startsWith("#")) {
|
|
13645
|
+
brandColors.push({ slot, hex, lab: hexToLab(hex) });
|
|
13646
|
+
}
|
|
13647
|
+
}
|
|
13648
|
+
if (!brandColors.length) return { entries: [], overallScore: 50, passed: false };
|
|
13649
|
+
const entries = contentColors.map((color) => {
|
|
13650
|
+
const colorLab = hexToLab(color);
|
|
13651
|
+
let minDE = Infinity;
|
|
13652
|
+
let closest = brandColors[0];
|
|
13653
|
+
for (const bc of brandColors) {
|
|
13654
|
+
const de = deltaE2000(colorLab, bc.lab);
|
|
13655
|
+
if (de < minDE) {
|
|
13656
|
+
minDE = de;
|
|
13657
|
+
closest = bc;
|
|
13658
|
+
}
|
|
13659
|
+
}
|
|
13660
|
+
return {
|
|
13661
|
+
color,
|
|
13662
|
+
closestBrandColor: closest.hex,
|
|
13663
|
+
closestBrandSlot: closest.slot,
|
|
13664
|
+
deltaE: Math.round(minDE * 100) / 100,
|
|
13665
|
+
passed: minDE <= threshold
|
|
13666
|
+
};
|
|
13667
|
+
});
|
|
13668
|
+
const passedCount = entries.filter((e) => e.passed).length;
|
|
13669
|
+
const overallScore = Math.round(passedCount / entries.length * 100);
|
|
13670
|
+
return { entries, overallScore, passed: overallScore >= 80 };
|
|
13671
|
+
}
|
|
13672
|
+
function exportDesignTokens(palette, typography, format) {
|
|
13673
|
+
if (format === "css") return exportCSS(palette, typography);
|
|
13674
|
+
if (format === "tailwind") return JSON.stringify(exportTailwind(palette), null, 2);
|
|
13675
|
+
return JSON.stringify(exportFigma(palette, typography), null, 2);
|
|
13676
|
+
}
|
|
13677
|
+
function exportCSS(palette, typography) {
|
|
13678
|
+
const lines = [":root {"];
|
|
13679
|
+
const slots = [
|
|
13680
|
+
["--brand-primary", "primary"],
|
|
13681
|
+
["--brand-secondary", "secondary"],
|
|
13682
|
+
["--brand-accent", "accent"],
|
|
13683
|
+
["--brand-background", "background"],
|
|
13684
|
+
["--brand-success", "success"],
|
|
13685
|
+
["--brand-warning", "warning"],
|
|
13686
|
+
["--brand-error", "error"],
|
|
13687
|
+
["--brand-text", "text"],
|
|
13688
|
+
["--brand-text-secondary", "textSecondary"]
|
|
13689
|
+
];
|
|
13690
|
+
for (const [varName, key] of slots) {
|
|
13691
|
+
const v = palette[key];
|
|
13692
|
+
if (typeof v === "string" && v) lines.push(` ${varName}: ${v};`);
|
|
13693
|
+
}
|
|
13694
|
+
if (typography) {
|
|
13695
|
+
const hf = typography.headingFont;
|
|
13696
|
+
const bf = typography.bodyFont;
|
|
13697
|
+
if (hf) lines.push(` --brand-font-heading: ${hf};`);
|
|
13698
|
+
if (bf) lines.push(` --brand-font-body: ${bf};`);
|
|
13699
|
+
}
|
|
13700
|
+
lines.push("}");
|
|
13701
|
+
return lines.join("\n");
|
|
13702
|
+
}
|
|
13703
|
+
function exportTailwind(palette) {
|
|
13704
|
+
const colors = {};
|
|
13705
|
+
const map = [
|
|
13706
|
+
["brand-primary", "primary"],
|
|
13707
|
+
["brand-secondary", "secondary"],
|
|
13708
|
+
["brand-accent", "accent"],
|
|
13709
|
+
["brand-bg", "background"]
|
|
13710
|
+
];
|
|
13711
|
+
for (const [tw, key] of map) {
|
|
13712
|
+
const v = palette[key];
|
|
13713
|
+
if (typeof v === "string" && v) colors[tw] = v;
|
|
13714
|
+
}
|
|
13715
|
+
return colors;
|
|
13716
|
+
}
|
|
13717
|
+
function exportFigma(palette, typography) {
|
|
13718
|
+
const tokens = { color: {} };
|
|
13719
|
+
for (const slot of ["primary", "secondary", "accent", "background"]) {
|
|
13720
|
+
const v = palette[slot];
|
|
13721
|
+
if (typeof v === "string") tokens.color[slot] = { value: v, type: "color" };
|
|
13722
|
+
}
|
|
13723
|
+
if (typography) {
|
|
13724
|
+
const hf = typography.headingFont;
|
|
13725
|
+
const bf = typography.bodyFont;
|
|
13726
|
+
if (hf || bf) {
|
|
13727
|
+
tokens.fontFamily = {};
|
|
13728
|
+
if (hf) tokens.fontFamily.heading = { value: String(hf), type: "fontFamilies" };
|
|
13729
|
+
if (bf) tokens.fontFamily.body = { value: String(bf), type: "fontFamilies" };
|
|
13730
|
+
}
|
|
13731
|
+
}
|
|
13732
|
+
return tokens;
|
|
13733
|
+
}
|
|
13734
|
+
|
|
13735
|
+
// src/tools/brandRuntime.ts
|
|
13736
|
+
function asEnvelope21(data) {
|
|
12919
13737
|
return {
|
|
12920
13738
|
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
12921
13739
|
data
|
|
@@ -12926,7 +13744,7 @@ function registerBrandRuntimeTools(server2) {
|
|
|
12926
13744
|
"get_brand_runtime",
|
|
12927
13745
|
"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.",
|
|
12928
13746
|
{
|
|
12929
|
-
project_id:
|
|
13747
|
+
project_id: z26.string().optional().describe("Project ID. Defaults to current project.")
|
|
12930
13748
|
},
|
|
12931
13749
|
async ({ project_id }) => {
|
|
12932
13750
|
const projectId = project_id || await getDefaultProjectId();
|
|
@@ -12986,7 +13804,7 @@ function registerBrandRuntimeTools(server2) {
|
|
|
12986
13804
|
pagesScraped: meta.pagesScraped || 0
|
|
12987
13805
|
}
|
|
12988
13806
|
};
|
|
12989
|
-
const envelope =
|
|
13807
|
+
const envelope = asEnvelope21(runtime);
|
|
12990
13808
|
return {
|
|
12991
13809
|
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
|
|
12992
13810
|
};
|
|
@@ -12996,7 +13814,7 @@ function registerBrandRuntimeTools(server2) {
|
|
|
12996
13814
|
"explain_brand_system",
|
|
12997
13815
|
"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.",
|
|
12998
13816
|
{
|
|
12999
|
-
project_id:
|
|
13817
|
+
project_id: z26.string().optional().describe("Project ID. Defaults to current project.")
|
|
13000
13818
|
},
|
|
13001
13819
|
async ({ project_id }) => {
|
|
13002
13820
|
const projectId = project_id || await getDefaultProjectId();
|
|
@@ -13108,8 +13926,8 @@ function registerBrandRuntimeTools(server2) {
|
|
|
13108
13926
|
"check_brand_consistency",
|
|
13109
13927
|
"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.",
|
|
13110
13928
|
{
|
|
13111
|
-
content:
|
|
13112
|
-
project_id:
|
|
13929
|
+
content: z26.string().describe("The content text to check for brand consistency."),
|
|
13930
|
+
project_id: z26.string().optional().describe("Project ID. Defaults to current project.")
|
|
13113
13931
|
},
|
|
13114
13932
|
async ({ content, project_id }) => {
|
|
13115
13933
|
const projectId = project_id || await getDefaultProjectId();
|
|
@@ -13127,45 +13945,77 @@ function registerBrandRuntimeTools(server2) {
|
|
|
13127
13945
|
};
|
|
13128
13946
|
}
|
|
13129
13947
|
const profile = row.profile_data;
|
|
13130
|
-
const
|
|
13131
|
-
const
|
|
13132
|
-
|
|
13133
|
-
|
|
13134
|
-
|
|
13135
|
-
|
|
13136
|
-
|
|
13137
|
-
|
|
13138
|
-
|
|
13139
|
-
|
|
13140
|
-
|
|
13141
|
-
|
|
13142
|
-
|
|
13143
|
-
|
|
13144
|
-
|
|
13145
|
-
|
|
13146
|
-
const
|
|
13147
|
-
|
|
13148
|
-
const
|
|
13149
|
-
|
|
13150
|
-
{
|
|
13151
|
-
|
|
13152
|
-
|
|
13153
|
-
|
|
13154
|
-
|
|
13155
|
-
|
|
13156
|
-
|
|
13157
|
-
|
|
13158
|
-
}
|
|
13159
|
-
}
|
|
13160
|
-
|
|
13161
|
-
|
|
13162
|
-
|
|
13163
|
-
|
|
13164
|
-
|
|
13165
|
-
|
|
13166
|
-
|
|
13948
|
+
const checkResult = computeBrandConsistency(content, profile);
|
|
13949
|
+
const envelope = asEnvelope21(checkResult);
|
|
13950
|
+
return {
|
|
13951
|
+
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
|
|
13952
|
+
};
|
|
13953
|
+
}
|
|
13954
|
+
);
|
|
13955
|
+
server2.tool(
|
|
13956
|
+
"audit_brand_colors",
|
|
13957
|
+
"Audit content colors against the brand palette using perceptual color distance (Delta E 2000). Returns per-color compliance scores and identifies the closest brand color for each input.",
|
|
13958
|
+
{
|
|
13959
|
+
content_colors: z26.array(z26.string()).describe('Hex color strings used in the content (e.g., ["#FF0000", "#00FF00"])'),
|
|
13960
|
+
project_id: z26.string().optional().describe("Project ID. Defaults to current project."),
|
|
13961
|
+
threshold: z26.number().optional().describe("Max Delta E for on-brand (default 10). Lower = stricter.")
|
|
13962
|
+
},
|
|
13963
|
+
async ({ content_colors, project_id, threshold }) => {
|
|
13964
|
+
const projectId = project_id || await getDefaultProjectId();
|
|
13965
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", { action: "brand-profile", projectId });
|
|
13966
|
+
const row = !efError && result?.success ? result.profile : null;
|
|
13967
|
+
if (!row?.profile_data?.colorPalette) {
|
|
13968
|
+
return {
|
|
13969
|
+
content: [
|
|
13970
|
+
{
|
|
13971
|
+
type: "text",
|
|
13972
|
+
text: "No brand color palette found. Extract a brand profile first."
|
|
13973
|
+
}
|
|
13974
|
+
],
|
|
13975
|
+
isError: true
|
|
13976
|
+
};
|
|
13977
|
+
}
|
|
13978
|
+
const auditResult = auditBrandColors(
|
|
13979
|
+
row.profile_data.colorPalette,
|
|
13980
|
+
content_colors,
|
|
13981
|
+
threshold ?? 10
|
|
13982
|
+
);
|
|
13983
|
+
const envelope = asEnvelope21(auditResult);
|
|
13984
|
+
return {
|
|
13985
|
+
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
|
|
13167
13986
|
};
|
|
13168
|
-
|
|
13987
|
+
}
|
|
13988
|
+
);
|
|
13989
|
+
server2.tool(
|
|
13990
|
+
"export_design_tokens",
|
|
13991
|
+
"Export brand palette and typography as design tokens. Supports CSS custom properties, Tailwind config, and Figma Tokens JSON formats.",
|
|
13992
|
+
{
|
|
13993
|
+
format: z26.enum(["css", "tailwind", "figma"]).describe(
|
|
13994
|
+
"Output format: css (CSS variables), tailwind (theme.extend.colors), figma (Figma Tokens JSON)"
|
|
13995
|
+
),
|
|
13996
|
+
project_id: z26.string().optional().describe("Project ID. Defaults to current project.")
|
|
13997
|
+
},
|
|
13998
|
+
async ({ format, project_id }) => {
|
|
13999
|
+
const projectId = project_id || await getDefaultProjectId();
|
|
14000
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", { action: "brand-profile", projectId });
|
|
14001
|
+
const row = !efError && result?.success ? result.profile : null;
|
|
14002
|
+
if (!row?.profile_data?.colorPalette) {
|
|
14003
|
+
return {
|
|
14004
|
+
content: [
|
|
14005
|
+
{
|
|
14006
|
+
type: "text",
|
|
14007
|
+
text: "No brand color palette found. Extract a brand profile first."
|
|
14008
|
+
}
|
|
14009
|
+
],
|
|
14010
|
+
isError: true
|
|
14011
|
+
};
|
|
14012
|
+
}
|
|
14013
|
+
const output = exportDesignTokens(
|
|
14014
|
+
row.profile_data.colorPalette,
|
|
14015
|
+
row.profile_data.typography,
|
|
14016
|
+
format
|
|
14017
|
+
);
|
|
14018
|
+
const envelope = asEnvelope21({ format, tokens: output });
|
|
13169
14019
|
return {
|
|
13170
14020
|
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
|
|
13171
14021
|
};
|
|
@@ -13173,6 +14023,391 @@ function registerBrandRuntimeTools(server2) {
|
|
|
13173
14023
|
);
|
|
13174
14024
|
}
|
|
13175
14025
|
|
|
14026
|
+
// src/tools/carousel.ts
|
|
14027
|
+
init_edge_function();
|
|
14028
|
+
import { z as z27 } from "zod";
|
|
14029
|
+
init_supabase();
|
|
14030
|
+
init_request_context();
|
|
14031
|
+
init_version();
|
|
14032
|
+
var MAX_CREDITS_PER_RUN2 = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
|
|
14033
|
+
var MAX_ASSETS_PER_RUN2 = Math.max(0, Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0));
|
|
14034
|
+
var _globalCreditsUsed2 = 0;
|
|
14035
|
+
var _globalAssetsGenerated2 = 0;
|
|
14036
|
+
function getCreditsUsed2() {
|
|
14037
|
+
const ctx = requestContext.getStore();
|
|
14038
|
+
return ctx ? ctx.creditsUsed : _globalCreditsUsed2;
|
|
14039
|
+
}
|
|
14040
|
+
function addCreditsUsed2(amount) {
|
|
14041
|
+
const ctx = requestContext.getStore();
|
|
14042
|
+
if (ctx) {
|
|
14043
|
+
ctx.creditsUsed += amount;
|
|
14044
|
+
} else {
|
|
14045
|
+
_globalCreditsUsed2 += amount;
|
|
14046
|
+
}
|
|
14047
|
+
}
|
|
14048
|
+
function getAssetsGenerated2() {
|
|
14049
|
+
const ctx = requestContext.getStore();
|
|
14050
|
+
return ctx ? ctx.assetsGenerated : _globalAssetsGenerated2;
|
|
14051
|
+
}
|
|
14052
|
+
function addAssetsGenerated2(count) {
|
|
14053
|
+
const ctx = requestContext.getStore();
|
|
14054
|
+
if (ctx) {
|
|
14055
|
+
ctx.assetsGenerated += count;
|
|
14056
|
+
} else {
|
|
14057
|
+
_globalAssetsGenerated2 += count;
|
|
14058
|
+
}
|
|
14059
|
+
}
|
|
14060
|
+
function checkCreditBudget2(estimatedCost) {
|
|
14061
|
+
if (MAX_CREDITS_PER_RUN2 <= 0) return { ok: true };
|
|
14062
|
+
const used = getCreditsUsed2();
|
|
14063
|
+
if (used + estimatedCost > MAX_CREDITS_PER_RUN2) {
|
|
14064
|
+
return {
|
|
14065
|
+
ok: false,
|
|
14066
|
+
message: `Credit budget exceeded: ${used} used + ${estimatedCost} estimated > ${MAX_CREDITS_PER_RUN2} limit. Use a smaller slide count or cheaper image model.`
|
|
14067
|
+
};
|
|
14068
|
+
}
|
|
14069
|
+
return { ok: true };
|
|
14070
|
+
}
|
|
14071
|
+
function checkAssetBudget2() {
|
|
14072
|
+
if (MAX_ASSETS_PER_RUN2 <= 0) return { ok: true };
|
|
14073
|
+
const gen = getAssetsGenerated2();
|
|
14074
|
+
if (gen >= MAX_ASSETS_PER_RUN2) {
|
|
14075
|
+
return {
|
|
14076
|
+
ok: false,
|
|
14077
|
+
message: `Asset limit reached: ${gen}/${MAX_ASSETS_PER_RUN2} assets generated this run.`
|
|
14078
|
+
};
|
|
14079
|
+
}
|
|
14080
|
+
return { ok: true };
|
|
14081
|
+
}
|
|
14082
|
+
var IMAGE_CREDIT_ESTIMATES2 = {
|
|
14083
|
+
midjourney: 20,
|
|
14084
|
+
"nano-banana": 15,
|
|
14085
|
+
"nano-banana-pro": 25,
|
|
14086
|
+
"flux-pro": 30,
|
|
14087
|
+
"flux-max": 50,
|
|
14088
|
+
"gpt4o-image": 40,
|
|
14089
|
+
imagen4: 35,
|
|
14090
|
+
"imagen4-fast": 25,
|
|
14091
|
+
seedream: 20
|
|
14092
|
+
};
|
|
14093
|
+
async function fetchBrandVisualContext(projectId) {
|
|
14094
|
+
const { data, error } = await callEdgeFunction("mcp-data", { action: "brand-profile", projectId });
|
|
14095
|
+
if (error || !data?.success || !data.profile?.profile_data) return null;
|
|
14096
|
+
const profile = data.profile.profile_data;
|
|
14097
|
+
const parts = [];
|
|
14098
|
+
const palette = profile.colorPalette;
|
|
14099
|
+
if (palette) {
|
|
14100
|
+
const colors = Object.entries(palette).filter(([, v]) => typeof v === "string" && v.startsWith("#")).map(([k, v]) => `${k}: ${v}`).slice(0, 5);
|
|
14101
|
+
if (colors.length > 0) {
|
|
14102
|
+
parts.push(`Brand color palette: ${colors.join(", ")}`);
|
|
14103
|
+
}
|
|
14104
|
+
}
|
|
14105
|
+
const logoUrl = profile.logoUrl;
|
|
14106
|
+
let logoDesc = null;
|
|
14107
|
+
if (logoUrl) {
|
|
14108
|
+
const brandName = profile.name || "brand";
|
|
14109
|
+
logoDesc = `Include a small "${brandName}" logo watermark in the bottom-right corner`;
|
|
14110
|
+
parts.push(logoDesc);
|
|
14111
|
+
}
|
|
14112
|
+
const voice = profile.voiceProfile;
|
|
14113
|
+
if (voice?.tone && Array.isArray(voice.tone) && voice.tone.length > 0) {
|
|
14114
|
+
parts.push(`Visual mood: ${voice.tone.slice(0, 3).join(", ")}`);
|
|
14115
|
+
}
|
|
14116
|
+
if (parts.length === 0) return null;
|
|
14117
|
+
return {
|
|
14118
|
+
stylePrefix: parts.join(". "),
|
|
14119
|
+
brandName: profile.name || null,
|
|
14120
|
+
logoDescription: logoDesc
|
|
14121
|
+
};
|
|
14122
|
+
}
|
|
14123
|
+
function registerCarouselTools(server2) {
|
|
14124
|
+
server2.tool(
|
|
14125
|
+
"create_carousel",
|
|
14126
|
+
"End-to-end carousel creation: generates slide text + kicks off image generation for each slide in parallel. When brand_id is provided, auto-injects brand colors, logo watermark, and visual mood into every image prompt. Returns carousel data + image job_ids. Poll each job_id with check_status until complete, then call schedule_post with job_ids to publish as Instagram carousel (media_type=CAROUSEL_ALBUM).",
|
|
14127
|
+
{
|
|
14128
|
+
topic: z27.string().max(200).describe(
|
|
14129
|
+
'Carousel topic/hook \u2014 be specific. Example: "5 pricing mistakes that kill SaaS startups" beats "SaaS tips".'
|
|
14130
|
+
),
|
|
14131
|
+
image_model: z27.enum([
|
|
14132
|
+
"midjourney",
|
|
14133
|
+
"nano-banana",
|
|
14134
|
+
"nano-banana-pro",
|
|
14135
|
+
"flux-pro",
|
|
14136
|
+
"flux-max",
|
|
14137
|
+
"gpt4o-image",
|
|
14138
|
+
"imagen4",
|
|
14139
|
+
"imagen4-fast",
|
|
14140
|
+
"seedream"
|
|
14141
|
+
]).describe(
|
|
14142
|
+
"Image model for slide visuals. flux-pro for general purpose, imagen4 for photorealistic, midjourney for artistic."
|
|
14143
|
+
),
|
|
14144
|
+
template_id: z27.enum([
|
|
14145
|
+
"educational-series",
|
|
14146
|
+
"product-showcase",
|
|
14147
|
+
"story-arc",
|
|
14148
|
+
"before-after",
|
|
14149
|
+
"step-by-step",
|
|
14150
|
+
"quote-collection",
|
|
14151
|
+
"data-stats",
|
|
14152
|
+
"myth-vs-reality",
|
|
14153
|
+
"hormozi-authority"
|
|
14154
|
+
]).optional().describe("Carousel template. Default: hormozi-authority."),
|
|
14155
|
+
slide_count: z27.number().min(3).max(10).optional().describe("Number of slides (3-10). Default: 7."),
|
|
14156
|
+
aspect_ratio: z27.enum(["1:1", "4:5", "9:16"]).optional().describe("Aspect ratio for both carousel and images. Default: 1:1."),
|
|
14157
|
+
style: z27.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe("Visual style. Default: hormozi for hormozi-authority template."),
|
|
14158
|
+
image_style_suffix: z27.string().max(500).optional().describe(
|
|
14159
|
+
'Style suffix appended to every image prompt for visual consistency across slides. Example: "dark moody lighting, cinematic, 35mm film grain".'
|
|
14160
|
+
),
|
|
14161
|
+
project_id: z27.string().optional().describe("Project ID to associate the carousel with."),
|
|
14162
|
+
response_format: z27.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
14163
|
+
},
|
|
14164
|
+
async ({
|
|
14165
|
+
topic,
|
|
14166
|
+
image_model,
|
|
14167
|
+
template_id,
|
|
14168
|
+
slide_count,
|
|
14169
|
+
aspect_ratio,
|
|
14170
|
+
style,
|
|
14171
|
+
image_style_suffix,
|
|
14172
|
+
brand_id,
|
|
14173
|
+
project_id,
|
|
14174
|
+
response_format
|
|
14175
|
+
}) => {
|
|
14176
|
+
const format = response_format ?? "text";
|
|
14177
|
+
const startedAt = Date.now();
|
|
14178
|
+
const templateId = template_id ?? "hormozi-authority";
|
|
14179
|
+
const resolvedStyle = style ?? (templateId === "hormozi-authority" ? "hormozi" : "professional");
|
|
14180
|
+
const slideCount = slide_count ?? 7;
|
|
14181
|
+
const ratio = aspect_ratio ?? "1:1";
|
|
14182
|
+
let brandContext = null;
|
|
14183
|
+
const brandProjectId = brand_id || project_id || await getDefaultProjectId();
|
|
14184
|
+
if (brandProjectId) {
|
|
14185
|
+
brandContext = await fetchBrandVisualContext(brandProjectId);
|
|
14186
|
+
}
|
|
14187
|
+
const carouselTextCost = 10 + slideCount * 2;
|
|
14188
|
+
const perImageCost = IMAGE_CREDIT_ESTIMATES2[image_model] ?? 30;
|
|
14189
|
+
const totalEstimatedCost = carouselTextCost + slideCount * perImageCost;
|
|
14190
|
+
const budgetCheck = checkCreditBudget2(totalEstimatedCost);
|
|
14191
|
+
if (!budgetCheck.ok) {
|
|
14192
|
+
await logMcpToolInvocation({
|
|
14193
|
+
toolName: "create_carousel",
|
|
14194
|
+
status: "error",
|
|
14195
|
+
durationMs: Date.now() - startedAt,
|
|
14196
|
+
details: { error: budgetCheck.message, totalEstimatedCost }
|
|
14197
|
+
});
|
|
14198
|
+
return {
|
|
14199
|
+
content: [{ type: "text", text: budgetCheck.message }],
|
|
14200
|
+
isError: true
|
|
14201
|
+
};
|
|
14202
|
+
}
|
|
14203
|
+
const assetBudget = checkAssetBudget2();
|
|
14204
|
+
if (!assetBudget.ok) {
|
|
14205
|
+
await logMcpToolInvocation({
|
|
14206
|
+
toolName: "create_carousel",
|
|
14207
|
+
status: "error",
|
|
14208
|
+
durationMs: Date.now() - startedAt,
|
|
14209
|
+
details: { error: assetBudget.message }
|
|
14210
|
+
});
|
|
14211
|
+
return {
|
|
14212
|
+
content: [{ type: "text", text: assetBudget.message }],
|
|
14213
|
+
isError: true
|
|
14214
|
+
};
|
|
14215
|
+
}
|
|
14216
|
+
const userId = await getDefaultUserId();
|
|
14217
|
+
const rateLimit = checkRateLimit("posting", `create_carousel:${userId}`);
|
|
14218
|
+
if (!rateLimit.allowed) {
|
|
14219
|
+
await logMcpToolInvocation({
|
|
14220
|
+
toolName: "create_carousel",
|
|
14221
|
+
status: "rate_limited",
|
|
14222
|
+
durationMs: Date.now() - startedAt,
|
|
14223
|
+
details: { retryAfter: rateLimit.retryAfter }
|
|
14224
|
+
});
|
|
14225
|
+
return {
|
|
14226
|
+
content: [
|
|
14227
|
+
{
|
|
14228
|
+
type: "text",
|
|
14229
|
+
text: `Rate limit exceeded. Retry in ~${rateLimit.retryAfter}s.`
|
|
14230
|
+
}
|
|
14231
|
+
],
|
|
14232
|
+
isError: true
|
|
14233
|
+
};
|
|
14234
|
+
}
|
|
14235
|
+
const { data: carouselData, error: carouselError } = await callEdgeFunction(
|
|
14236
|
+
"generate-carousel",
|
|
14237
|
+
{
|
|
14238
|
+
topic,
|
|
14239
|
+
templateId,
|
|
14240
|
+
slideCount,
|
|
14241
|
+
aspectRatio: ratio,
|
|
14242
|
+
style: resolvedStyle,
|
|
14243
|
+
projectId: project_id
|
|
14244
|
+
},
|
|
14245
|
+
{ timeoutMs: 6e4 }
|
|
14246
|
+
);
|
|
14247
|
+
if (carouselError || !carouselData?.carousel) {
|
|
14248
|
+
const errMsg = carouselError ?? "No carousel data returned";
|
|
14249
|
+
await logMcpToolInvocation({
|
|
14250
|
+
toolName: "create_carousel",
|
|
14251
|
+
status: "error",
|
|
14252
|
+
durationMs: Date.now() - startedAt,
|
|
14253
|
+
details: { phase: "text_generation", error: errMsg }
|
|
14254
|
+
});
|
|
14255
|
+
return {
|
|
14256
|
+
content: [{ type: "text", text: `Carousel text generation failed: ${errMsg}` }],
|
|
14257
|
+
isError: true
|
|
14258
|
+
};
|
|
14259
|
+
}
|
|
14260
|
+
const carousel = carouselData.carousel;
|
|
14261
|
+
const textCredits = carousel.credits?.used ?? carouselTextCost;
|
|
14262
|
+
addCreditsUsed2(textCredits);
|
|
14263
|
+
const imageJobs = await Promise.all(
|
|
14264
|
+
carousel.slides.map(async (slide) => {
|
|
14265
|
+
const promptParts = [];
|
|
14266
|
+
if (brandContext) promptParts.push(brandContext.stylePrefix);
|
|
14267
|
+
if (slide.headline) promptParts.push(slide.headline);
|
|
14268
|
+
if (slide.body) promptParts.push(slide.body);
|
|
14269
|
+
if (promptParts.length === 0) promptParts.push(topic);
|
|
14270
|
+
if (image_style_suffix) promptParts.push(image_style_suffix);
|
|
14271
|
+
const imagePrompt = promptParts.join(". ");
|
|
14272
|
+
try {
|
|
14273
|
+
const { data, error } = await callEdgeFunction(
|
|
14274
|
+
"kie-image-generate",
|
|
14275
|
+
{
|
|
14276
|
+
prompt: imagePrompt,
|
|
14277
|
+
model: image_model,
|
|
14278
|
+
aspectRatio: ratio
|
|
14279
|
+
},
|
|
14280
|
+
{ timeoutMs: 3e4 }
|
|
14281
|
+
);
|
|
14282
|
+
if (error || !data?.taskId && !data?.asyncJobId) {
|
|
14283
|
+
return {
|
|
14284
|
+
slideNumber: slide.slideNumber,
|
|
14285
|
+
jobId: null,
|
|
14286
|
+
model: image_model,
|
|
14287
|
+
error: error ?? "No job ID returned"
|
|
14288
|
+
};
|
|
14289
|
+
}
|
|
14290
|
+
const jobId = data.asyncJobId ?? data.taskId ?? null;
|
|
14291
|
+
if (jobId) {
|
|
14292
|
+
addCreditsUsed2(perImageCost);
|
|
14293
|
+
addAssetsGenerated2(1);
|
|
14294
|
+
}
|
|
14295
|
+
return {
|
|
14296
|
+
slideNumber: slide.slideNumber,
|
|
14297
|
+
jobId,
|
|
14298
|
+
model: image_model,
|
|
14299
|
+
error: null
|
|
14300
|
+
};
|
|
14301
|
+
} catch (err) {
|
|
14302
|
+
return {
|
|
14303
|
+
slideNumber: slide.slideNumber,
|
|
14304
|
+
jobId: null,
|
|
14305
|
+
model: image_model,
|
|
14306
|
+
error: sanitizeError2(err)
|
|
14307
|
+
};
|
|
14308
|
+
}
|
|
14309
|
+
})
|
|
14310
|
+
);
|
|
14311
|
+
const successfulJobs = imageJobs.filter((j) => j.jobId !== null);
|
|
14312
|
+
const failedJobs = imageJobs.filter((j) => j.jobId === null);
|
|
14313
|
+
await logMcpToolInvocation({
|
|
14314
|
+
toolName: "create_carousel",
|
|
14315
|
+
status: failedJobs.length === imageJobs.length ? "error" : "success",
|
|
14316
|
+
durationMs: Date.now() - startedAt,
|
|
14317
|
+
details: {
|
|
14318
|
+
carouselId: carousel.id,
|
|
14319
|
+
templateId,
|
|
14320
|
+
slideCount: carousel.slides.length,
|
|
14321
|
+
imagesStarted: successfulJobs.length,
|
|
14322
|
+
imagesFailed: failedJobs.length,
|
|
14323
|
+
imageModel: image_model,
|
|
14324
|
+
creditsUsed: getCreditsUsed2()
|
|
14325
|
+
}
|
|
14326
|
+
});
|
|
14327
|
+
if (format === "json") {
|
|
14328
|
+
return {
|
|
14329
|
+
content: [
|
|
14330
|
+
{
|
|
14331
|
+
type: "text",
|
|
14332
|
+
text: JSON.stringify(
|
|
14333
|
+
{
|
|
14334
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
14335
|
+
data: {
|
|
14336
|
+
carouselId: carousel.id,
|
|
14337
|
+
templateId,
|
|
14338
|
+
style: resolvedStyle,
|
|
14339
|
+
slideCount: carousel.slides.length,
|
|
14340
|
+
slides: carousel.slides.map((s) => {
|
|
14341
|
+
const job = imageJobs.find((j) => j.slideNumber === s.slideNumber);
|
|
14342
|
+
return {
|
|
14343
|
+
...s,
|
|
14344
|
+
imageJobId: job?.jobId ?? null,
|
|
14345
|
+
imageError: job?.error ?? null
|
|
14346
|
+
};
|
|
14347
|
+
}),
|
|
14348
|
+
imageModel: image_model,
|
|
14349
|
+
brandApplied: brandContext ? {
|
|
14350
|
+
brandName: brandContext.brandName,
|
|
14351
|
+
hasLogo: !!brandContext.logoDescription,
|
|
14352
|
+
stylePrefix: brandContext.stylePrefix
|
|
14353
|
+
} : null,
|
|
14354
|
+
jobIds: successfulJobs.map((j) => j.jobId),
|
|
14355
|
+
failedSlides: failedJobs.map((j) => ({
|
|
14356
|
+
slideNumber: j.slideNumber,
|
|
14357
|
+
error: j.error
|
|
14358
|
+
})),
|
|
14359
|
+
credits: {
|
|
14360
|
+
textGeneration: textCredits,
|
|
14361
|
+
imagesEstimated: successfulJobs.length * perImageCost,
|
|
14362
|
+
totalEstimated: textCredits + successfulJobs.length * perImageCost
|
|
14363
|
+
}
|
|
14364
|
+
}
|
|
14365
|
+
},
|
|
14366
|
+
null,
|
|
14367
|
+
2
|
|
14368
|
+
)
|
|
14369
|
+
}
|
|
14370
|
+
]
|
|
14371
|
+
};
|
|
14372
|
+
}
|
|
14373
|
+
const lines = [
|
|
14374
|
+
`Carousel created: ${carousel.slides.length} slides + ${successfulJobs.length} image jobs started.`,
|
|
14375
|
+
` Carousel ID: ${carousel.id}`,
|
|
14376
|
+
` Template: ${templateId} | Style: ${resolvedStyle}`,
|
|
14377
|
+
` Image model: ${image_model}`,
|
|
14378
|
+
` Credits: ~${textCredits + successfulJobs.length * perImageCost} (${textCredits} text + ${successfulJobs.length * perImageCost} images)`
|
|
14379
|
+
];
|
|
14380
|
+
if (brandContext) {
|
|
14381
|
+
lines.push(
|
|
14382
|
+
` Brand: ${brandContext.brandName || "unnamed"}${brandContext.logoDescription ? " (logo overlay via prompt)" : ""}`
|
|
14383
|
+
);
|
|
14384
|
+
}
|
|
14385
|
+
lines.push("", "Slides:");
|
|
14386
|
+
for (const slide of carousel.slides) {
|
|
14387
|
+
const job = imageJobs.find((j) => j.slideNumber === slide.slideNumber);
|
|
14388
|
+
const status = job?.jobId ? `image: ${job.jobId}` : `image FAILED: ${job?.error}`;
|
|
14389
|
+
lines.push(` ${slide.slideNumber}. ${slide.headline || "(no headline)"} [${status}]`);
|
|
14390
|
+
}
|
|
14391
|
+
if (failedJobs.length > 0) {
|
|
14392
|
+
lines.push("");
|
|
14393
|
+
lines.push(
|
|
14394
|
+
`WARNING: ${failedJobs.length}/${imageJobs.length} image generations failed. Use generate_image manually for failed slides.`
|
|
14395
|
+
);
|
|
14396
|
+
}
|
|
14397
|
+
const jobIdList = successfulJobs.map((j) => j.jobId).join(", ");
|
|
14398
|
+
lines.push("");
|
|
14399
|
+
lines.push("Next steps:");
|
|
14400
|
+
lines.push(` 1. Poll each job: check_status with job_id for each of: ${jobIdList}`);
|
|
14401
|
+
lines.push(
|
|
14402
|
+
" 2. When all complete: schedule_post with job_ids=[...] and media_type=CAROUSEL_ALBUM"
|
|
14403
|
+
);
|
|
14404
|
+
return {
|
|
14405
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
14406
|
+
};
|
|
14407
|
+
}
|
|
14408
|
+
);
|
|
14409
|
+
}
|
|
14410
|
+
|
|
13176
14411
|
// src/lib/register-tools.ts
|
|
13177
14412
|
function applyScopeEnforcement(server2, scopeResolver) {
|
|
13178
14413
|
const originalTool = server2.tool.bind(server2);
|
|
@@ -13267,6 +14502,7 @@ function registerAllTools(server2, options) {
|
|
|
13267
14502
|
registerLoopSummaryTools(server2);
|
|
13268
14503
|
registerUsageTools(server2);
|
|
13269
14504
|
registerAutopilotTools(server2);
|
|
14505
|
+
registerRecipeTools(server2);
|
|
13270
14506
|
registerExtractionTools(server2);
|
|
13271
14507
|
registerQualityTools(server2);
|
|
13272
14508
|
registerPlanningTools(server2);
|
|
@@ -13276,21 +14512,22 @@ function registerAllTools(server2, options) {
|
|
|
13276
14512
|
registerSuggestTools(server2);
|
|
13277
14513
|
registerDigestTools(server2);
|
|
13278
14514
|
registerBrandRuntimeTools(server2);
|
|
14515
|
+
registerCarouselTools(server2);
|
|
13279
14516
|
applyAnnotations(server2);
|
|
13280
14517
|
}
|
|
13281
14518
|
|
|
13282
14519
|
// src/prompts.ts
|
|
13283
|
-
import { z as
|
|
14520
|
+
import { z as z28 } from "zod";
|
|
13284
14521
|
function registerPrompts(server2) {
|
|
13285
14522
|
server2.prompt(
|
|
13286
14523
|
"create_weekly_content_plan",
|
|
13287
14524
|
"Generate a full week of social media content (7 days, multiple platforms). Returns a structured plan with topics, formats, and posting times.",
|
|
13288
14525
|
{
|
|
13289
|
-
niche:
|
|
13290
|
-
platforms:
|
|
14526
|
+
niche: z28.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
|
|
14527
|
+
platforms: z28.string().optional().describe(
|
|
13291
14528
|
'Comma-separated platforms to target (default: "YouTube, Instagram, TikTok, LinkedIn")'
|
|
13292
14529
|
),
|
|
13293
|
-
tone:
|
|
14530
|
+
tone: z28.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
|
|
13294
14531
|
},
|
|
13295
14532
|
({ niche, platforms, tone }) => {
|
|
13296
14533
|
const targetPlatforms = platforms || "YouTube, Instagram, TikTok, LinkedIn";
|
|
@@ -13332,8 +14569,8 @@ After building the plan, use \`save_content_plan\` to save it.`
|
|
|
13332
14569
|
"analyze_top_content",
|
|
13333
14570
|
"Analyze your best-performing posts to identify patterns and replicate success. Returns insights on hooks, formats, timing, and topics that resonate.",
|
|
13334
14571
|
{
|
|
13335
|
-
timeframe:
|
|
13336
|
-
platform:
|
|
14572
|
+
timeframe: z28.string().optional().describe('Analysis period (default: "30 days"). E.g., "7 days", "90 days"'),
|
|
14573
|
+
platform: z28.string().optional().describe('Filter to a specific platform (e.g., "youtube", "instagram")')
|
|
13337
14574
|
},
|
|
13338
14575
|
({ timeframe, platform: platform3 }) => {
|
|
13339
14576
|
const period = timeframe || "30 days";
|
|
@@ -13370,10 +14607,10 @@ Format as a clear, actionable performance report.`
|
|
|
13370
14607
|
"repurpose_content",
|
|
13371
14608
|
"Take one piece of content and transform it into 8-10 pieces across multiple platforms and formats.",
|
|
13372
14609
|
{
|
|
13373
|
-
source:
|
|
14610
|
+
source: z28.string().describe(
|
|
13374
14611
|
"The source content to repurpose \u2014 a URL, transcript, or the content text itself"
|
|
13375
14612
|
),
|
|
13376
|
-
target_platforms:
|
|
14613
|
+
target_platforms: z28.string().optional().describe(
|
|
13377
14614
|
'Comma-separated target platforms (default: "Twitter, LinkedIn, Instagram, TikTok, YouTube")'
|
|
13378
14615
|
)
|
|
13379
14616
|
},
|
|
@@ -13415,9 +14652,9 @@ For each piece, include the platform, format, character count, and suggested pos
|
|
|
13415
14652
|
"setup_brand_voice",
|
|
13416
14653
|
"Define or refine your brand voice profile so all generated content stays on-brand. Walks through tone, audience, values, and style.",
|
|
13417
14654
|
{
|
|
13418
|
-
brand_name:
|
|
13419
|
-
industry:
|
|
13420
|
-
website:
|
|
14655
|
+
brand_name: z28.string().describe("Your brand or business name"),
|
|
14656
|
+
industry: z28.string().optional().describe('Your industry or niche (e.g., "B2B SaaS", "fitness coaching")'),
|
|
14657
|
+
website: z28.string().optional().describe("Your website URL for context")
|
|
13421
14658
|
},
|
|
13422
14659
|
({ brand_name, industry, website }) => {
|
|
13423
14660
|
const industryContext = industry ? ` in the ${industry} space` : "";
|