@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/http.js
CHANGED
|
@@ -570,6 +570,8 @@ var TOOL_SCOPES = {
|
|
|
570
570
|
get_brand_runtime: "mcp:read",
|
|
571
571
|
explain_brand_system: "mcp:read",
|
|
572
572
|
check_brand_consistency: "mcp:read",
|
|
573
|
+
audit_brand_colors: "mcp:read",
|
|
574
|
+
export_design_tokens: "mcp:read",
|
|
573
575
|
get_ideation_context: "mcp:read",
|
|
574
576
|
get_credit_balance: "mcp:read",
|
|
575
577
|
get_budget_status: "mcp:read",
|
|
@@ -591,6 +593,7 @@ var TOOL_SCOPES = {
|
|
|
591
593
|
create_storyboard: "mcp:write",
|
|
592
594
|
generate_voiceover: "mcp:write",
|
|
593
595
|
generate_carousel: "mcp:write",
|
|
596
|
+
create_carousel: "mcp:write",
|
|
594
597
|
upload_media: "mcp:write",
|
|
595
598
|
// mcp:read (media)
|
|
596
599
|
get_media_url: "mcp:read",
|
|
@@ -609,6 +612,11 @@ var TOOL_SCOPES = {
|
|
|
609
612
|
list_autopilot_configs: "mcp:autopilot",
|
|
610
613
|
update_autopilot_config: "mcp:autopilot",
|
|
611
614
|
get_autopilot_status: "mcp:autopilot",
|
|
615
|
+
// Recipes
|
|
616
|
+
list_recipes: "mcp:read",
|
|
617
|
+
get_recipe_details: "mcp:read",
|
|
618
|
+
execute_recipe: "mcp:write",
|
|
619
|
+
get_recipe_run_status: "mcp:read",
|
|
612
620
|
// mcp:read (content lifecycle — read-only tools)
|
|
613
621
|
extract_url_content: "mcp:read",
|
|
614
622
|
quality_check: "mcp:read",
|
|
@@ -1375,7 +1383,7 @@ init_supabase();
|
|
|
1375
1383
|
init_request_context();
|
|
1376
1384
|
|
|
1377
1385
|
// src/lib/version.ts
|
|
1378
|
-
var MCP_VERSION = "1.7.
|
|
1386
|
+
var MCP_VERSION = "1.7.4";
|
|
1379
1387
|
|
|
1380
1388
|
// src/tools/content.ts
|
|
1381
1389
|
var MAX_CREDITS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
|
|
@@ -2507,6 +2515,56 @@ Return ONLY valid JSON in this exact format:
|
|
|
2507
2515
|
// src/tools/distribution.ts
|
|
2508
2516
|
import { z as z3 } from "zod";
|
|
2509
2517
|
import { createHash as createHash2 } from "node:crypto";
|
|
2518
|
+
|
|
2519
|
+
// src/lib/sanitize-error.ts
|
|
2520
|
+
var ERROR_PATTERNS = [
|
|
2521
|
+
// Postgres / PostgREST
|
|
2522
|
+
[/PGRST301|permission denied/i, "Access denied. Check your account permissions."],
|
|
2523
|
+
[/42P01|does not exist/i, "Service temporarily unavailable. Please try again."],
|
|
2524
|
+
[/23505|unique.*constraint|duplicate key/i, "A duplicate record already exists."],
|
|
2525
|
+
[/23503|foreign key/i, "Referenced record not found."],
|
|
2526
|
+
// Gemini / Google AI
|
|
2527
|
+
[/google.*api.*key|googleapis\.com.*40[13]/i, "Content generation failed. Please try again."],
|
|
2528
|
+
[
|
|
2529
|
+
/RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
|
|
2530
|
+
"AI service rate limit reached. Please wait and retry."
|
|
2531
|
+
],
|
|
2532
|
+
[
|
|
2533
|
+
/SAFETY|prompt.*blocked|content.*filter/i,
|
|
2534
|
+
"Content was blocked by the AI safety filter. Try rephrasing."
|
|
2535
|
+
],
|
|
2536
|
+
[/gemini.*error|generativelanguage/i, "Content generation failed. Please try again."],
|
|
2537
|
+
// Kie.ai
|
|
2538
|
+
[/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
|
|
2539
|
+
// Stripe
|
|
2540
|
+
[/stripe.*api|sk_live_|sk_test_/i, "Payment processing error. Please try again."],
|
|
2541
|
+
// Network / fetch
|
|
2542
|
+
[
|
|
2543
|
+
/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
|
|
2544
|
+
"External service unavailable. Please try again."
|
|
2545
|
+
],
|
|
2546
|
+
[/fetch failed|network error|abort.*timeout/i, "Network request failed. Please try again."],
|
|
2547
|
+
[/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
|
|
2548
|
+
// Supabase Edge Function internals
|
|
2549
|
+
[/FunctionsHttpError|non-2xx status/i, "Backend service error. Please try again."],
|
|
2550
|
+
[/JWT|token.*expired|token.*invalid/i, "Authentication expired. Please re-authenticate."],
|
|
2551
|
+
// Generic sensitive patterns (API keys, URLs with secrets)
|
|
2552
|
+
[/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i, "An internal error occurred. Please try again."]
|
|
2553
|
+
];
|
|
2554
|
+
function sanitizeError2(error) {
|
|
2555
|
+
const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
2556
|
+
if (process.env.NODE_ENV !== "production") {
|
|
2557
|
+
console.error("[Error]", msg);
|
|
2558
|
+
}
|
|
2559
|
+
for (const [pattern, userMessage] of ERROR_PATTERNS) {
|
|
2560
|
+
if (pattern.test(msg)) {
|
|
2561
|
+
return userMessage;
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
return "An unexpected error occurred. Please try again.";
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
// src/tools/distribution.ts
|
|
2510
2568
|
init_supabase();
|
|
2511
2569
|
|
|
2512
2570
|
// src/lib/quality.ts
|
|
@@ -2834,7 +2892,12 @@ function registerDistributionTools(server) {
|
|
|
2834
2892
|
const signed = await signR2Key(r2_key);
|
|
2835
2893
|
if (!signed) {
|
|
2836
2894
|
return {
|
|
2837
|
-
content: [
|
|
2895
|
+
content: [
|
|
2896
|
+
{
|
|
2897
|
+
type: "text",
|
|
2898
|
+
text: `Failed to sign media key. Verify the key exists and you have access.`
|
|
2899
|
+
}
|
|
2900
|
+
],
|
|
2838
2901
|
isError: true
|
|
2839
2902
|
};
|
|
2840
2903
|
}
|
|
@@ -2862,7 +2925,7 @@ function registerDistributionTools(server) {
|
|
|
2862
2925
|
content: [
|
|
2863
2926
|
{
|
|
2864
2927
|
type: "text",
|
|
2865
|
-
text: `Failed to sign
|
|
2928
|
+
text: `Failed to sign media key at index ${failIdx}. Verify the key exists and you have access.`
|
|
2866
2929
|
}
|
|
2867
2930
|
],
|
|
2868
2931
|
isError: true
|
|
@@ -2890,7 +2953,7 @@ function registerDistributionTools(server) {
|
|
|
2890
2953
|
content: [
|
|
2891
2954
|
{
|
|
2892
2955
|
type: "text",
|
|
2893
|
-
text: `Failed to resolve media: ${
|
|
2956
|
+
text: `Failed to resolve media: ${sanitizeError2(resolveErr)}`
|
|
2894
2957
|
}
|
|
2895
2958
|
],
|
|
2896
2959
|
isError: true
|
|
@@ -3255,7 +3318,7 @@ Created with Social Neuron`;
|
|
|
3255
3318
|
return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
|
|
3256
3319
|
} catch (err) {
|
|
3257
3320
|
const durationMs = Date.now() - startedAt;
|
|
3258
|
-
const message =
|
|
3321
|
+
const message = sanitizeError2(err);
|
|
3259
3322
|
logMcpToolInvocation({
|
|
3260
3323
|
toolName: "find_next_slots",
|
|
3261
3324
|
status: "error",
|
|
@@ -3775,7 +3838,7 @@ Created with Social Neuron`;
|
|
|
3775
3838
|
};
|
|
3776
3839
|
} catch (err) {
|
|
3777
3840
|
const durationMs = Date.now() - startedAt;
|
|
3778
|
-
const message =
|
|
3841
|
+
const message = sanitizeError2(err);
|
|
3779
3842
|
logMcpToolInvocation({
|
|
3780
3843
|
toolName: "schedule_content_plan",
|
|
3781
3844
|
status: "error",
|
|
@@ -3930,7 +3993,7 @@ function registerMediaTools(server) {
|
|
|
3930
3993
|
content: [
|
|
3931
3994
|
{
|
|
3932
3995
|
type: "text",
|
|
3933
|
-
text: `R2 upload failed: ${
|
|
3996
|
+
text: `R2 upload failed: ${sanitizeError(uploadErr)}`
|
|
3934
3997
|
}
|
|
3935
3998
|
],
|
|
3936
3999
|
isError: true
|
|
@@ -5054,7 +5117,7 @@ function registerScreenshotTools(server) {
|
|
|
5054
5117
|
};
|
|
5055
5118
|
} catch (err) {
|
|
5056
5119
|
await closeBrowser();
|
|
5057
|
-
const message =
|
|
5120
|
+
const message = sanitizeError2(err);
|
|
5058
5121
|
await logMcpToolInvocation({
|
|
5059
5122
|
toolName: "capture_app_page",
|
|
5060
5123
|
status: "error",
|
|
@@ -5210,7 +5273,7 @@ function registerScreenshotTools(server) {
|
|
|
5210
5273
|
};
|
|
5211
5274
|
} catch (err) {
|
|
5212
5275
|
await closeBrowser();
|
|
5213
|
-
const message =
|
|
5276
|
+
const message = sanitizeError2(err);
|
|
5214
5277
|
await logMcpToolInvocation({
|
|
5215
5278
|
toolName: "capture_screenshot",
|
|
5216
5279
|
status: "error",
|
|
@@ -5503,7 +5566,7 @@ function registerRemotionTools(server) {
|
|
|
5503
5566
|
]
|
|
5504
5567
|
};
|
|
5505
5568
|
} catch (err) {
|
|
5506
|
-
const message =
|
|
5569
|
+
const message = sanitizeError2(err);
|
|
5507
5570
|
await logMcpToolInvocation({
|
|
5508
5571
|
toolName: "render_demo_video",
|
|
5509
5572
|
status: "error",
|
|
@@ -5631,7 +5694,7 @@ function registerRemotionTools(server) {
|
|
|
5631
5694
|
]
|
|
5632
5695
|
};
|
|
5633
5696
|
} catch (err) {
|
|
5634
|
-
const message =
|
|
5697
|
+
const message = sanitizeError2(err);
|
|
5635
5698
|
await logMcpToolInvocation({
|
|
5636
5699
|
toolName: "render_template_video",
|
|
5637
5700
|
status: "error",
|
|
@@ -7075,10 +7138,276 @@ Active: ${is_active}`
|
|
|
7075
7138
|
);
|
|
7076
7139
|
}
|
|
7077
7140
|
|
|
7078
|
-
// src/tools/
|
|
7141
|
+
// src/tools/recipes.ts
|
|
7079
7142
|
import { z as z17 } from "zod";
|
|
7080
|
-
init_supabase();
|
|
7081
7143
|
function asEnvelope13(data) {
|
|
7144
|
+
return {
|
|
7145
|
+
_meta: {
|
|
7146
|
+
version: MCP_VERSION,
|
|
7147
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7148
|
+
},
|
|
7149
|
+
data
|
|
7150
|
+
};
|
|
7151
|
+
}
|
|
7152
|
+
function registerRecipeTools(server) {
|
|
7153
|
+
server.tool(
|
|
7154
|
+
"list_recipes",
|
|
7155
|
+
'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.',
|
|
7156
|
+
{
|
|
7157
|
+
category: z17.enum([
|
|
7158
|
+
"content_creation",
|
|
7159
|
+
"distribution",
|
|
7160
|
+
"repurposing",
|
|
7161
|
+
"analytics",
|
|
7162
|
+
"engagement",
|
|
7163
|
+
"general"
|
|
7164
|
+
]).optional().describe("Filter by category. Omit to list all."),
|
|
7165
|
+
featured_only: z17.boolean().optional().describe("If true, only return featured recipes. Defaults to false."),
|
|
7166
|
+
response_format: z17.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
7167
|
+
},
|
|
7168
|
+
async ({ category, featured_only, response_format }) => {
|
|
7169
|
+
const format = response_format ?? "text";
|
|
7170
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
7171
|
+
action: "list-recipes",
|
|
7172
|
+
category: category ?? null,
|
|
7173
|
+
featured_only: featured_only ?? false
|
|
7174
|
+
});
|
|
7175
|
+
if (efError) {
|
|
7176
|
+
return {
|
|
7177
|
+
content: [
|
|
7178
|
+
{
|
|
7179
|
+
type: "text",
|
|
7180
|
+
text: `Error fetching recipes: ${efError}`
|
|
7181
|
+
}
|
|
7182
|
+
],
|
|
7183
|
+
isError: true
|
|
7184
|
+
};
|
|
7185
|
+
}
|
|
7186
|
+
const recipes = result?.recipes ?? [];
|
|
7187
|
+
if (format === "json") {
|
|
7188
|
+
return {
|
|
7189
|
+
content: [
|
|
7190
|
+
{
|
|
7191
|
+
type: "text",
|
|
7192
|
+
text: JSON.stringify(asEnvelope13(recipes))
|
|
7193
|
+
}
|
|
7194
|
+
]
|
|
7195
|
+
};
|
|
7196
|
+
}
|
|
7197
|
+
if (recipes.length === 0) {
|
|
7198
|
+
return {
|
|
7199
|
+
content: [
|
|
7200
|
+
{
|
|
7201
|
+
type: "text",
|
|
7202
|
+
text: "No recipes found. Recipes are pre-built automation templates \u2014 check back after setup."
|
|
7203
|
+
}
|
|
7204
|
+
]
|
|
7205
|
+
};
|
|
7206
|
+
}
|
|
7207
|
+
const lines = recipes.map(
|
|
7208
|
+
(r) => `**${r.name}** (${r.slug})
|
|
7209
|
+
${r.description}
|
|
7210
|
+
Category: ${r.category} | Credits: ~${r.estimated_credits} | Steps: ${r.steps.length}${r.is_featured ? " | \u2B50 Featured" : ""}
|
|
7211
|
+
Inputs: ${r.inputs_schema.map((i) => `${i.label}${i.required ? "*" : ""}`).join(", ")}`
|
|
7212
|
+
);
|
|
7213
|
+
return {
|
|
7214
|
+
content: [
|
|
7215
|
+
{
|
|
7216
|
+
type: "text",
|
|
7217
|
+
text: `## Available Recipes (${recipes.length})
|
|
7218
|
+
|
|
7219
|
+
${lines.join("\n\n")}`
|
|
7220
|
+
}
|
|
7221
|
+
]
|
|
7222
|
+
};
|
|
7223
|
+
}
|
|
7224
|
+
);
|
|
7225
|
+
server.tool(
|
|
7226
|
+
"get_recipe_details",
|
|
7227
|
+
"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.",
|
|
7228
|
+
{
|
|
7229
|
+
slug: z17.string().describe('Recipe slug (e.g., "weekly-instagram-calendar")'),
|
|
7230
|
+
response_format: z17.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
7231
|
+
},
|
|
7232
|
+
async ({ slug, response_format }) => {
|
|
7233
|
+
const format = response_format ?? "text";
|
|
7234
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
7235
|
+
action: "get-recipe-details",
|
|
7236
|
+
slug
|
|
7237
|
+
});
|
|
7238
|
+
if (efError) {
|
|
7239
|
+
return {
|
|
7240
|
+
content: [{ type: "text", text: `Error: ${efError}` }],
|
|
7241
|
+
isError: true
|
|
7242
|
+
};
|
|
7243
|
+
}
|
|
7244
|
+
const recipe = result?.recipe;
|
|
7245
|
+
if (!recipe) {
|
|
7246
|
+
return {
|
|
7247
|
+
content: [
|
|
7248
|
+
{
|
|
7249
|
+
type: "text",
|
|
7250
|
+
text: `Recipe "${slug}" not found. Use list_recipes to see available recipes.`
|
|
7251
|
+
}
|
|
7252
|
+
],
|
|
7253
|
+
isError: true
|
|
7254
|
+
};
|
|
7255
|
+
}
|
|
7256
|
+
if (format === "json") {
|
|
7257
|
+
return {
|
|
7258
|
+
content: [
|
|
7259
|
+
{
|
|
7260
|
+
type: "text",
|
|
7261
|
+
text: JSON.stringify(asEnvelope13(recipe))
|
|
7262
|
+
}
|
|
7263
|
+
]
|
|
7264
|
+
};
|
|
7265
|
+
}
|
|
7266
|
+
const stepsText = recipe.steps.map((s, i) => ` ${i + 1}. **${s.name}** (${s.type})`).join("\n");
|
|
7267
|
+
const inputsText = recipe.inputs_schema.map(
|
|
7268
|
+
(i) => ` - **${i.label}**${i.required ? " (required)" : ""}: ${i.type}${i.placeholder ? ` \u2014 e.g., "${i.placeholder}"` : ""}`
|
|
7269
|
+
).join("\n");
|
|
7270
|
+
return {
|
|
7271
|
+
content: [
|
|
7272
|
+
{
|
|
7273
|
+
type: "text",
|
|
7274
|
+
text: [
|
|
7275
|
+
`## ${recipe.name}`,
|
|
7276
|
+
recipe.description,
|
|
7277
|
+
"",
|
|
7278
|
+
`**Category:** ${recipe.category}`,
|
|
7279
|
+
`**Estimated credits:** ~${recipe.estimated_credits}`,
|
|
7280
|
+
`**Estimated time:** ~${Math.round(recipe.estimated_duration_seconds / 60)} minutes`,
|
|
7281
|
+
"",
|
|
7282
|
+
"### Steps",
|
|
7283
|
+
stepsText,
|
|
7284
|
+
"",
|
|
7285
|
+
"### Required Inputs",
|
|
7286
|
+
inputsText
|
|
7287
|
+
].join("\n")
|
|
7288
|
+
}
|
|
7289
|
+
]
|
|
7290
|
+
};
|
|
7291
|
+
}
|
|
7292
|
+
);
|
|
7293
|
+
server.tool(
|
|
7294
|
+
"execute_recipe",
|
|
7295
|
+
"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.",
|
|
7296
|
+
{
|
|
7297
|
+
slug: z17.string().describe('Recipe slug (e.g., "weekly-instagram-calendar")'),
|
|
7298
|
+
inputs: z17.record(z17.unknown()).describe(
|
|
7299
|
+
"Input values matching the recipe input schema. Use get_recipe_details to see required inputs."
|
|
7300
|
+
),
|
|
7301
|
+
response_format: z17.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
7302
|
+
},
|
|
7303
|
+
async ({ slug, inputs, response_format }) => {
|
|
7304
|
+
const format = response_format ?? "text";
|
|
7305
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
7306
|
+
action: "execute-recipe",
|
|
7307
|
+
slug,
|
|
7308
|
+
inputs
|
|
7309
|
+
});
|
|
7310
|
+
if (efError) {
|
|
7311
|
+
return {
|
|
7312
|
+
content: [{ type: "text", text: `Error: ${efError}` }],
|
|
7313
|
+
isError: true
|
|
7314
|
+
};
|
|
7315
|
+
}
|
|
7316
|
+
if (format === "json") {
|
|
7317
|
+
return {
|
|
7318
|
+
content: [
|
|
7319
|
+
{
|
|
7320
|
+
type: "text",
|
|
7321
|
+
text: JSON.stringify(asEnvelope13(result))
|
|
7322
|
+
}
|
|
7323
|
+
]
|
|
7324
|
+
};
|
|
7325
|
+
}
|
|
7326
|
+
return {
|
|
7327
|
+
content: [
|
|
7328
|
+
{
|
|
7329
|
+
type: "text",
|
|
7330
|
+
text: `Recipe "${slug}" started.
|
|
7331
|
+
|
|
7332
|
+
**Run ID:** ${result?.run_id}
|
|
7333
|
+
**Status:** ${result?.status}
|
|
7334
|
+
|
|
7335
|
+
${result?.message || "Use get_recipe_run_status to check progress."}`
|
|
7336
|
+
}
|
|
7337
|
+
]
|
|
7338
|
+
};
|
|
7339
|
+
}
|
|
7340
|
+
);
|
|
7341
|
+
server.tool(
|
|
7342
|
+
"get_recipe_run_status",
|
|
7343
|
+
"Check the status of a running recipe execution. Shows progress, current step, credits used, and outputs when complete.",
|
|
7344
|
+
{
|
|
7345
|
+
run_id: z17.string().describe("The recipe run ID returned by execute_recipe"),
|
|
7346
|
+
response_format: z17.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
7347
|
+
},
|
|
7348
|
+
async ({ run_id, response_format }) => {
|
|
7349
|
+
const format = response_format ?? "text";
|
|
7350
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
7351
|
+
action: "get-recipe-run-status",
|
|
7352
|
+
run_id
|
|
7353
|
+
});
|
|
7354
|
+
if (efError) {
|
|
7355
|
+
return {
|
|
7356
|
+
content: [{ type: "text", text: `Error: ${efError}` }],
|
|
7357
|
+
isError: true
|
|
7358
|
+
};
|
|
7359
|
+
}
|
|
7360
|
+
const run = result?.run;
|
|
7361
|
+
if (!run) {
|
|
7362
|
+
return {
|
|
7363
|
+
content: [
|
|
7364
|
+
{
|
|
7365
|
+
type: "text",
|
|
7366
|
+
text: `Run "${run_id}" not found.`
|
|
7367
|
+
}
|
|
7368
|
+
],
|
|
7369
|
+
isError: true
|
|
7370
|
+
};
|
|
7371
|
+
}
|
|
7372
|
+
if (format === "json") {
|
|
7373
|
+
return {
|
|
7374
|
+
content: [
|
|
7375
|
+
{
|
|
7376
|
+
type: "text",
|
|
7377
|
+
text: JSON.stringify(asEnvelope13(run))
|
|
7378
|
+
}
|
|
7379
|
+
]
|
|
7380
|
+
};
|
|
7381
|
+
}
|
|
7382
|
+
const statusEmoji = run.status === "completed" ? "Done" : run.status === "failed" ? "Failed" : run.status === "running" ? "Running" : run.status;
|
|
7383
|
+
return {
|
|
7384
|
+
content: [
|
|
7385
|
+
{
|
|
7386
|
+
type: "text",
|
|
7387
|
+
text: [
|
|
7388
|
+
`**Recipe Run:** ${run.id}`,
|
|
7389
|
+
`**Status:** ${statusEmoji}`,
|
|
7390
|
+
`**Progress:** ${run.progress}%`,
|
|
7391
|
+
run.current_step ? `**Current step:** ${run.current_step}` : "",
|
|
7392
|
+
`**Credits used:** ${run.credits_used}`,
|
|
7393
|
+
run.completed_at ? `**Completed:** ${run.completed_at}` : "",
|
|
7394
|
+
run.outputs ? `
|
|
7395
|
+
**Outputs:**
|
|
7396
|
+
\`\`\`json
|
|
7397
|
+
${JSON.stringify(run.outputs, null, 2)}
|
|
7398
|
+
\`\`\`` : ""
|
|
7399
|
+
].filter(Boolean).join("\n")
|
|
7400
|
+
}
|
|
7401
|
+
]
|
|
7402
|
+
};
|
|
7403
|
+
}
|
|
7404
|
+
);
|
|
7405
|
+
}
|
|
7406
|
+
|
|
7407
|
+
// src/tools/extraction.ts
|
|
7408
|
+
import { z as z18 } from "zod";
|
|
7409
|
+
init_supabase();
|
|
7410
|
+
function asEnvelope14(data) {
|
|
7082
7411
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
7083
7412
|
}
|
|
7084
7413
|
function isYouTubeUrl(url) {
|
|
@@ -7132,11 +7461,11 @@ function registerExtractionTools(server) {
|
|
|
7132
7461
|
"extract_url_content",
|
|
7133
7462
|
"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.",
|
|
7134
7463
|
{
|
|
7135
|
-
url:
|
|
7136
|
-
extract_type:
|
|
7137
|
-
include_comments:
|
|
7138
|
-
max_results:
|
|
7139
|
-
response_format:
|
|
7464
|
+
url: z18.string().url().describe("URL to extract content from"),
|
|
7465
|
+
extract_type: z18.enum(["auto", "transcript", "article", "product"]).default("auto").describe("Type of extraction"),
|
|
7466
|
+
include_comments: z18.boolean().default(false).describe("Include top comments (YouTube only)"),
|
|
7467
|
+
max_results: z18.number().min(1).max(100).default(10).describe("Max comments to include"),
|
|
7468
|
+
response_format: z18.enum(["text", "json"]).default("text")
|
|
7140
7469
|
},
|
|
7141
7470
|
async ({ url, extract_type, include_comments, max_results, response_format }) => {
|
|
7142
7471
|
const startedAt = Date.now();
|
|
@@ -7271,7 +7600,7 @@ function registerExtractionTools(server) {
|
|
|
7271
7600
|
if (response_format === "json") {
|
|
7272
7601
|
return {
|
|
7273
7602
|
content: [
|
|
7274
|
-
{ type: "text", text: JSON.stringify(
|
|
7603
|
+
{ type: "text", text: JSON.stringify(asEnvelope14(extracted), null, 2) }
|
|
7275
7604
|
],
|
|
7276
7605
|
isError: false
|
|
7277
7606
|
};
|
|
@@ -7282,7 +7611,7 @@ function registerExtractionTools(server) {
|
|
|
7282
7611
|
};
|
|
7283
7612
|
} catch (err) {
|
|
7284
7613
|
const durationMs = Date.now() - startedAt;
|
|
7285
|
-
const message =
|
|
7614
|
+
const message = sanitizeError2(err);
|
|
7286
7615
|
logMcpToolInvocation({
|
|
7287
7616
|
toolName: "extract_url_content",
|
|
7288
7617
|
status: "error",
|
|
@@ -7299,9 +7628,9 @@ function registerExtractionTools(server) {
|
|
|
7299
7628
|
}
|
|
7300
7629
|
|
|
7301
7630
|
// src/tools/quality.ts
|
|
7302
|
-
import { z as
|
|
7631
|
+
import { z as z19 } from "zod";
|
|
7303
7632
|
init_supabase();
|
|
7304
|
-
function
|
|
7633
|
+
function asEnvelope15(data) {
|
|
7305
7634
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
7306
7635
|
}
|
|
7307
7636
|
function registerQualityTools(server) {
|
|
@@ -7309,12 +7638,12 @@ function registerQualityTools(server) {
|
|
|
7309
7638
|
"quality_check",
|
|
7310
7639
|
"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.",
|
|
7311
7640
|
{
|
|
7312
|
-
caption:
|
|
7641
|
+
caption: z19.string().describe(
|
|
7313
7642
|
"The post text to score. Include hashtags if they will be published \u2014 they affect Platform Fit and Safety/Claims scores."
|
|
7314
7643
|
),
|
|
7315
|
-
title:
|
|
7316
|
-
platforms:
|
|
7317
|
-
|
|
7644
|
+
title: z19.string().optional().describe("Post title (important for YouTube)"),
|
|
7645
|
+
platforms: z19.array(
|
|
7646
|
+
z19.enum([
|
|
7318
7647
|
"youtube",
|
|
7319
7648
|
"tiktok",
|
|
7320
7649
|
"instagram",
|
|
@@ -7325,13 +7654,13 @@ function registerQualityTools(server) {
|
|
|
7325
7654
|
"bluesky"
|
|
7326
7655
|
])
|
|
7327
7656
|
).min(1).describe("Target platforms"),
|
|
7328
|
-
threshold:
|
|
7657
|
+
threshold: z19.number().min(0).max(35).default(26).describe(
|
|
7329
7658
|
"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."
|
|
7330
7659
|
),
|
|
7331
|
-
brand_keyword:
|
|
7332
|
-
brand_avoid_patterns:
|
|
7333
|
-
custom_banned_terms:
|
|
7334
|
-
response_format:
|
|
7660
|
+
brand_keyword: z19.string().optional().describe("Brand keyword for alignment check"),
|
|
7661
|
+
brand_avoid_patterns: z19.array(z19.string()).optional(),
|
|
7662
|
+
custom_banned_terms: z19.array(z19.string()).optional(),
|
|
7663
|
+
response_format: z19.enum(["text", "json"]).default("text")
|
|
7335
7664
|
},
|
|
7336
7665
|
async ({
|
|
7337
7666
|
caption,
|
|
@@ -7362,7 +7691,7 @@ function registerQualityTools(server) {
|
|
|
7362
7691
|
});
|
|
7363
7692
|
if (response_format === "json") {
|
|
7364
7693
|
return {
|
|
7365
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
7694
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope15(result), null, 2) }],
|
|
7366
7695
|
isError: false
|
|
7367
7696
|
};
|
|
7368
7697
|
}
|
|
@@ -7390,20 +7719,20 @@ function registerQualityTools(server) {
|
|
|
7390
7719
|
"quality_check_plan",
|
|
7391
7720
|
"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.",
|
|
7392
7721
|
{
|
|
7393
|
-
plan:
|
|
7394
|
-
posts:
|
|
7395
|
-
|
|
7396
|
-
id:
|
|
7397
|
-
caption:
|
|
7398
|
-
title:
|
|
7399
|
-
platform:
|
|
7722
|
+
plan: z19.object({
|
|
7723
|
+
posts: z19.array(
|
|
7724
|
+
z19.object({
|
|
7725
|
+
id: z19.string(),
|
|
7726
|
+
caption: z19.string(),
|
|
7727
|
+
title: z19.string().optional(),
|
|
7728
|
+
platform: z19.string()
|
|
7400
7729
|
})
|
|
7401
7730
|
)
|
|
7402
7731
|
}).passthrough().describe("Content plan with posts array"),
|
|
7403
|
-
threshold:
|
|
7732
|
+
threshold: z19.number().min(0).max(35).default(26).describe(
|
|
7404
7733
|
"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."
|
|
7405
7734
|
),
|
|
7406
|
-
response_format:
|
|
7735
|
+
response_format: z19.enum(["text", "json"]).default("text")
|
|
7407
7736
|
},
|
|
7408
7737
|
async ({ plan, threshold, response_format }) => {
|
|
7409
7738
|
const startedAt = Date.now();
|
|
@@ -7445,7 +7774,7 @@ function registerQualityTools(server) {
|
|
|
7445
7774
|
content: [
|
|
7446
7775
|
{
|
|
7447
7776
|
type: "text",
|
|
7448
|
-
text: JSON.stringify(
|
|
7777
|
+
text: JSON.stringify(asEnvelope15({ posts: postsWithQuality, summary }), null, 2)
|
|
7449
7778
|
}
|
|
7450
7779
|
],
|
|
7451
7780
|
isError: false
|
|
@@ -7469,7 +7798,7 @@ function registerQualityTools(server) {
|
|
|
7469
7798
|
}
|
|
7470
7799
|
|
|
7471
7800
|
// src/tools/planning.ts
|
|
7472
|
-
import { z as
|
|
7801
|
+
import { z as z20 } from "zod";
|
|
7473
7802
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7474
7803
|
init_supabase();
|
|
7475
7804
|
|
|
@@ -7497,7 +7826,7 @@ function extractJsonArray(text) {
|
|
|
7497
7826
|
function toRecord(value) {
|
|
7498
7827
|
return value && typeof value === "object" ? value : void 0;
|
|
7499
7828
|
}
|
|
7500
|
-
function
|
|
7829
|
+
function asEnvelope16(data) {
|
|
7501
7830
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
7502
7831
|
}
|
|
7503
7832
|
function tomorrowIsoDate() {
|
|
@@ -7567,10 +7896,10 @@ function registerPlanningTools(server) {
|
|
|
7567
7896
|
"plan_content_week",
|
|
7568
7897
|
"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.",
|
|
7569
7898
|
{
|
|
7570
|
-
topic:
|
|
7571
|
-
source_url:
|
|
7572
|
-
platforms:
|
|
7573
|
-
|
|
7899
|
+
topic: z20.string().describe("Main topic or content theme"),
|
|
7900
|
+
source_url: z20.string().optional().describe("URL to extract content from (YouTube, article)"),
|
|
7901
|
+
platforms: z20.array(
|
|
7902
|
+
z20.enum([
|
|
7574
7903
|
"youtube",
|
|
7575
7904
|
"tiktok",
|
|
7576
7905
|
"instagram",
|
|
@@ -7581,12 +7910,12 @@ function registerPlanningTools(server) {
|
|
|
7581
7910
|
"bluesky"
|
|
7582
7911
|
])
|
|
7583
7912
|
).min(1).describe("Target platforms"),
|
|
7584
|
-
posts_per_day:
|
|
7585
|
-
days:
|
|
7586
|
-
start_date:
|
|
7587
|
-
brand_voice:
|
|
7588
|
-
project_id:
|
|
7589
|
-
response_format:
|
|
7913
|
+
posts_per_day: z20.number().min(1).max(5).default(1).describe("Posts per platform per day"),
|
|
7914
|
+
days: z20.number().min(1).max(7).default(5).describe("Number of days to plan"),
|
|
7915
|
+
start_date: z20.string().optional().describe("ISO date, defaults to tomorrow"),
|
|
7916
|
+
brand_voice: z20.string().optional().describe("Override brand voice description"),
|
|
7917
|
+
project_id: z20.string().optional().describe("Project ID for brand/insights context"),
|
|
7918
|
+
response_format: z20.enum(["text", "json"]).default("json")
|
|
7590
7919
|
},
|
|
7591
7920
|
async ({
|
|
7592
7921
|
topic,
|
|
@@ -7838,7 +8167,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7838
8167
|
}
|
|
7839
8168
|
} catch (persistErr) {
|
|
7840
8169
|
const durationMs2 = Date.now() - startedAt;
|
|
7841
|
-
const message =
|
|
8170
|
+
const message = sanitizeError2(persistErr);
|
|
7842
8171
|
logMcpToolInvocation({
|
|
7843
8172
|
toolName: "plan_content_week",
|
|
7844
8173
|
status: "error",
|
|
@@ -7860,7 +8189,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7860
8189
|
});
|
|
7861
8190
|
if (response_format === "json") {
|
|
7862
8191
|
return {
|
|
7863
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
8192
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope16(plan), null, 2) }],
|
|
7864
8193
|
isError: false
|
|
7865
8194
|
};
|
|
7866
8195
|
}
|
|
@@ -7870,7 +8199,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7870
8199
|
};
|
|
7871
8200
|
} catch (err) {
|
|
7872
8201
|
const durationMs = Date.now() - startedAt;
|
|
7873
|
-
const message =
|
|
8202
|
+
const message = sanitizeError2(err);
|
|
7874
8203
|
logMcpToolInvocation({
|
|
7875
8204
|
toolName: "plan_content_week",
|
|
7876
8205
|
status: "error",
|
|
@@ -7888,13 +8217,13 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7888
8217
|
"save_content_plan",
|
|
7889
8218
|
"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.",
|
|
7890
8219
|
{
|
|
7891
|
-
plan:
|
|
7892
|
-
topic:
|
|
7893
|
-
posts:
|
|
8220
|
+
plan: z20.object({
|
|
8221
|
+
topic: z20.string(),
|
|
8222
|
+
posts: z20.array(z20.record(z20.string(), z20.unknown()))
|
|
7894
8223
|
}).passthrough(),
|
|
7895
|
-
project_id:
|
|
7896
|
-
status:
|
|
7897
|
-
response_format:
|
|
8224
|
+
project_id: z20.string().uuid().optional(),
|
|
8225
|
+
status: z20.enum(["draft", "in_review", "approved", "scheduled", "completed"]).default("draft"),
|
|
8226
|
+
response_format: z20.enum(["text", "json"]).default("json")
|
|
7898
8227
|
},
|
|
7899
8228
|
async ({ plan, project_id, status, response_format }) => {
|
|
7900
8229
|
const startedAt = Date.now();
|
|
@@ -7950,7 +8279,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7950
8279
|
};
|
|
7951
8280
|
if (response_format === "json") {
|
|
7952
8281
|
return {
|
|
7953
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
8282
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope16(result), null, 2) }],
|
|
7954
8283
|
isError: false
|
|
7955
8284
|
};
|
|
7956
8285
|
}
|
|
@@ -7962,7 +8291,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7962
8291
|
};
|
|
7963
8292
|
} catch (err) {
|
|
7964
8293
|
const durationMs = Date.now() - startedAt;
|
|
7965
|
-
const message =
|
|
8294
|
+
const message = sanitizeError2(err);
|
|
7966
8295
|
logMcpToolInvocation({
|
|
7967
8296
|
toolName: "save_content_plan",
|
|
7968
8297
|
status: "error",
|
|
@@ -7980,8 +8309,8 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7980
8309
|
"get_content_plan",
|
|
7981
8310
|
"Retrieve a persisted content plan by ID.",
|
|
7982
8311
|
{
|
|
7983
|
-
plan_id:
|
|
7984
|
-
response_format:
|
|
8312
|
+
plan_id: z20.string().uuid().describe("Persisted content plan ID"),
|
|
8313
|
+
response_format: z20.enum(["text", "json"]).default("json")
|
|
7985
8314
|
},
|
|
7986
8315
|
async ({ plan_id, response_format }) => {
|
|
7987
8316
|
const { data: result, error } = await callEdgeFunction("mcp-data", { action: "get-content-plan", plan_id }, { timeoutMs: 1e4 });
|
|
@@ -8016,7 +8345,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
8016
8345
|
};
|
|
8017
8346
|
if (response_format === "json") {
|
|
8018
8347
|
return {
|
|
8019
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
8348
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope16(payload), null, 2) }],
|
|
8020
8349
|
isError: false
|
|
8021
8350
|
};
|
|
8022
8351
|
}
|
|
@@ -8034,23 +8363,23 @@ ${rawText.slice(0, 1e3)}`
|
|
|
8034
8363
|
"update_content_plan",
|
|
8035
8364
|
"Update individual posts in a persisted content plan.",
|
|
8036
8365
|
{
|
|
8037
|
-
plan_id:
|
|
8038
|
-
post_updates:
|
|
8039
|
-
|
|
8040
|
-
post_id:
|
|
8041
|
-
caption:
|
|
8042
|
-
title:
|
|
8043
|
-
hashtags:
|
|
8044
|
-
hook:
|
|
8045
|
-
angle:
|
|
8046
|
-
visual_direction:
|
|
8047
|
-
media_url:
|
|
8048
|
-
schedule_at:
|
|
8049
|
-
platform:
|
|
8050
|
-
status:
|
|
8366
|
+
plan_id: z20.string().uuid(),
|
|
8367
|
+
post_updates: z20.array(
|
|
8368
|
+
z20.object({
|
|
8369
|
+
post_id: z20.string(),
|
|
8370
|
+
caption: z20.string().optional(),
|
|
8371
|
+
title: z20.string().optional(),
|
|
8372
|
+
hashtags: z20.array(z20.string()).optional(),
|
|
8373
|
+
hook: z20.string().optional(),
|
|
8374
|
+
angle: z20.string().optional(),
|
|
8375
|
+
visual_direction: z20.string().optional(),
|
|
8376
|
+
media_url: z20.string().optional(),
|
|
8377
|
+
schedule_at: z20.string().optional(),
|
|
8378
|
+
platform: z20.string().optional(),
|
|
8379
|
+
status: z20.enum(["approved", "rejected", "needs_edit"]).optional()
|
|
8051
8380
|
})
|
|
8052
8381
|
).min(1),
|
|
8053
|
-
response_format:
|
|
8382
|
+
response_format: z20.enum(["text", "json"]).default("json")
|
|
8054
8383
|
},
|
|
8055
8384
|
async ({ plan_id, post_updates, response_format }) => {
|
|
8056
8385
|
const { data: result, error } = await callEdgeFunction(
|
|
@@ -8088,7 +8417,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
8088
8417
|
};
|
|
8089
8418
|
if (response_format === "json") {
|
|
8090
8419
|
return {
|
|
8091
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
8420
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope16(payload), null, 2) }],
|
|
8092
8421
|
isError: false
|
|
8093
8422
|
};
|
|
8094
8423
|
}
|
|
@@ -8107,8 +8436,8 @@ ${rawText.slice(0, 1e3)}`
|
|
|
8107
8436
|
"submit_content_plan_for_approval",
|
|
8108
8437
|
"Create pending approval items for each post in a plan and mark plan status as in_review.",
|
|
8109
8438
|
{
|
|
8110
|
-
plan_id:
|
|
8111
|
-
response_format:
|
|
8439
|
+
plan_id: z20.string().uuid(),
|
|
8440
|
+
response_format: z20.enum(["text", "json"]).default("json")
|
|
8112
8441
|
},
|
|
8113
8442
|
async ({ plan_id, response_format }) => {
|
|
8114
8443
|
const { data: result, error } = await callEdgeFunction("mcp-data", { action: "submit-plan-approval", plan_id }, { timeoutMs: 15e3 });
|
|
@@ -8141,7 +8470,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
8141
8470
|
};
|
|
8142
8471
|
if (response_format === "json") {
|
|
8143
8472
|
return {
|
|
8144
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
8473
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope16(payload), null, 2) }],
|
|
8145
8474
|
isError: false
|
|
8146
8475
|
};
|
|
8147
8476
|
}
|
|
@@ -8159,9 +8488,9 @@ ${rawText.slice(0, 1e3)}`
|
|
|
8159
8488
|
}
|
|
8160
8489
|
|
|
8161
8490
|
// src/tools/plan-approvals.ts
|
|
8162
|
-
import { z as
|
|
8491
|
+
import { z as z21 } from "zod";
|
|
8163
8492
|
init_supabase();
|
|
8164
|
-
function
|
|
8493
|
+
function asEnvelope17(data) {
|
|
8165
8494
|
return {
|
|
8166
8495
|
_meta: {
|
|
8167
8496
|
version: MCP_VERSION,
|
|
@@ -8175,19 +8504,19 @@ function registerPlanApprovalTools(server) {
|
|
|
8175
8504
|
"create_plan_approvals",
|
|
8176
8505
|
"Create pending approval rows for each post in a content plan.",
|
|
8177
8506
|
{
|
|
8178
|
-
plan_id:
|
|
8179
|
-
posts:
|
|
8180
|
-
|
|
8181
|
-
id:
|
|
8182
|
-
platform:
|
|
8183
|
-
caption:
|
|
8184
|
-
title:
|
|
8185
|
-
media_url:
|
|
8186
|
-
schedule_at:
|
|
8507
|
+
plan_id: z21.string().uuid().describe("Content plan ID"),
|
|
8508
|
+
posts: z21.array(
|
|
8509
|
+
z21.object({
|
|
8510
|
+
id: z21.string(),
|
|
8511
|
+
platform: z21.string().optional(),
|
|
8512
|
+
caption: z21.string().optional(),
|
|
8513
|
+
title: z21.string().optional(),
|
|
8514
|
+
media_url: z21.string().optional(),
|
|
8515
|
+
schedule_at: z21.string().optional()
|
|
8187
8516
|
}).passthrough()
|
|
8188
8517
|
).min(1).describe("Posts to create approval entries for."),
|
|
8189
|
-
project_id:
|
|
8190
|
-
response_format:
|
|
8518
|
+
project_id: z21.string().uuid().optional().describe("Project ID. Defaults to active project context."),
|
|
8519
|
+
response_format: z21.enum(["text", "json"]).optional()
|
|
8191
8520
|
},
|
|
8192
8521
|
async ({ plan_id, posts, project_id, response_format }) => {
|
|
8193
8522
|
const projectId = project_id || await getDefaultProjectId();
|
|
@@ -8236,7 +8565,7 @@ function registerPlanApprovalTools(server) {
|
|
|
8236
8565
|
};
|
|
8237
8566
|
if ((response_format || "text") === "json") {
|
|
8238
8567
|
return {
|
|
8239
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
8568
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope17(payload), null, 2) }],
|
|
8240
8569
|
isError: false
|
|
8241
8570
|
};
|
|
8242
8571
|
}
|
|
@@ -8255,9 +8584,9 @@ function registerPlanApprovalTools(server) {
|
|
|
8255
8584
|
"list_plan_approvals",
|
|
8256
8585
|
"List MCP-native approval items for a specific content plan.",
|
|
8257
8586
|
{
|
|
8258
|
-
plan_id:
|
|
8259
|
-
status:
|
|
8260
|
-
response_format:
|
|
8587
|
+
plan_id: z21.string().uuid().describe("Content plan ID"),
|
|
8588
|
+
status: z21.enum(["pending", "approved", "rejected", "edited"]).optional(),
|
|
8589
|
+
response_format: z21.enum(["text", "json"]).optional()
|
|
8261
8590
|
},
|
|
8262
8591
|
async ({ plan_id, status, response_format }) => {
|
|
8263
8592
|
const { data: result, error } = await callEdgeFunction(
|
|
@@ -8288,7 +8617,7 @@ function registerPlanApprovalTools(server) {
|
|
|
8288
8617
|
};
|
|
8289
8618
|
if ((response_format || "text") === "json") {
|
|
8290
8619
|
return {
|
|
8291
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
8620
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope17(payload), null, 2) }],
|
|
8292
8621
|
isError: false
|
|
8293
8622
|
};
|
|
8294
8623
|
}
|
|
@@ -8315,11 +8644,11 @@ function registerPlanApprovalTools(server) {
|
|
|
8315
8644
|
"respond_plan_approval",
|
|
8316
8645
|
"Approve, reject, or edit a pending plan approval item.",
|
|
8317
8646
|
{
|
|
8318
|
-
approval_id:
|
|
8319
|
-
decision:
|
|
8320
|
-
edited_post:
|
|
8321
|
-
reason:
|
|
8322
|
-
response_format:
|
|
8647
|
+
approval_id: z21.string().uuid().describe("Approval item ID"),
|
|
8648
|
+
decision: z21.enum(["approved", "rejected", "edited"]),
|
|
8649
|
+
edited_post: z21.record(z21.string(), z21.unknown()).optional(),
|
|
8650
|
+
reason: z21.string().max(1e3).optional(),
|
|
8651
|
+
response_format: z21.enum(["text", "json"]).optional()
|
|
8323
8652
|
},
|
|
8324
8653
|
async ({ approval_id, decision, edited_post, reason, response_format }) => {
|
|
8325
8654
|
if (decision === "edited" && !edited_post) {
|
|
@@ -8366,7 +8695,7 @@ function registerPlanApprovalTools(server) {
|
|
|
8366
8695
|
}
|
|
8367
8696
|
if ((response_format || "text") === "json") {
|
|
8368
8697
|
return {
|
|
8369
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
8698
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope17(data), null, 2) }],
|
|
8370
8699
|
isError: false
|
|
8371
8700
|
};
|
|
8372
8701
|
}
|
|
@@ -8384,7 +8713,7 @@ function registerPlanApprovalTools(server) {
|
|
|
8384
8713
|
}
|
|
8385
8714
|
|
|
8386
8715
|
// src/tools/discovery.ts
|
|
8387
|
-
import { z as
|
|
8716
|
+
import { z as z22 } from "zod";
|
|
8388
8717
|
|
|
8389
8718
|
// src/lib/tool-catalog.ts
|
|
8390
8719
|
var TOOL_CATALOG = [
|
|
@@ -8451,6 +8780,12 @@ var TOOL_CATALOG = [
|
|
|
8451
8780
|
module: "content",
|
|
8452
8781
|
scope: "mcp:write"
|
|
8453
8782
|
},
|
|
8783
|
+
{
|
|
8784
|
+
name: "create_carousel",
|
|
8785
|
+
description: "End-to-end carousel: generate text + kick off image jobs for each slide",
|
|
8786
|
+
module: "carousel",
|
|
8787
|
+
scope: "mcp:write"
|
|
8788
|
+
},
|
|
8454
8789
|
// media
|
|
8455
8790
|
{
|
|
8456
8791
|
name: "upload_media",
|
|
@@ -8808,6 +9143,45 @@ var TOOL_CATALOG = [
|
|
|
8808
9143
|
description: "Create a new autopilot configuration",
|
|
8809
9144
|
module: "autopilot",
|
|
8810
9145
|
scope: "mcp:autopilot"
|
|
9146
|
+
},
|
|
9147
|
+
// brand runtime (additions)
|
|
9148
|
+
{
|
|
9149
|
+
name: "audit_brand_colors",
|
|
9150
|
+
description: "Audit brand color palette for accessibility, contrast, and harmony",
|
|
9151
|
+
module: "brandRuntime",
|
|
9152
|
+
scope: "mcp:read"
|
|
9153
|
+
},
|
|
9154
|
+
{
|
|
9155
|
+
name: "export_design_tokens",
|
|
9156
|
+
description: "Export brand design tokens in CSS/Tailwind/JSON formats",
|
|
9157
|
+
module: "brandRuntime",
|
|
9158
|
+
scope: "mcp:read"
|
|
9159
|
+
},
|
|
9160
|
+
// carousel (already listed in content section above)
|
|
9161
|
+
// recipes
|
|
9162
|
+
{
|
|
9163
|
+
name: "list_recipes",
|
|
9164
|
+
description: "List available recipe templates for automated content workflows",
|
|
9165
|
+
module: "recipes",
|
|
9166
|
+
scope: "mcp:read"
|
|
9167
|
+
},
|
|
9168
|
+
{
|
|
9169
|
+
name: "get_recipe_details",
|
|
9170
|
+
description: "Get full details of a recipe template including steps and required inputs",
|
|
9171
|
+
module: "recipes",
|
|
9172
|
+
scope: "mcp:read"
|
|
9173
|
+
},
|
|
9174
|
+
{
|
|
9175
|
+
name: "execute_recipe",
|
|
9176
|
+
description: "Execute a recipe template with provided inputs to run a multi-step workflow",
|
|
9177
|
+
module: "recipes",
|
|
9178
|
+
scope: "mcp:write"
|
|
9179
|
+
},
|
|
9180
|
+
{
|
|
9181
|
+
name: "get_recipe_run_status",
|
|
9182
|
+
description: "Check the status and progress of a running recipe execution",
|
|
9183
|
+
module: "recipes",
|
|
9184
|
+
scope: "mcp:read"
|
|
8811
9185
|
}
|
|
8812
9186
|
];
|
|
8813
9187
|
function getToolsByModule(module) {
|
|
@@ -8829,10 +9203,10 @@ function registerDiscoveryTools(server) {
|
|
|
8829
9203
|
"search_tools",
|
|
8830
9204
|
'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.',
|
|
8831
9205
|
{
|
|
8832
|
-
query:
|
|
8833
|
-
module:
|
|
8834
|
-
scope:
|
|
8835
|
-
detail:
|
|
9206
|
+
query: z22.string().optional().describe("Search query to filter tools by name or description"),
|
|
9207
|
+
module: z22.string().optional().describe('Filter by module name (e.g. "planning", "content", "analytics")'),
|
|
9208
|
+
scope: z22.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
|
|
9209
|
+
detail: z22.enum(["name", "summary", "full"]).default("summary").describe(
|
|
8836
9210
|
'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
|
|
8837
9211
|
)
|
|
8838
9212
|
},
|
|
@@ -8875,13 +9249,13 @@ function registerDiscoveryTools(server) {
|
|
|
8875
9249
|
}
|
|
8876
9250
|
|
|
8877
9251
|
// src/tools/pipeline.ts
|
|
8878
|
-
import { z as
|
|
9252
|
+
import { z as z23 } from "zod";
|
|
8879
9253
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
8880
9254
|
init_supabase();
|
|
8881
|
-
function
|
|
9255
|
+
function asEnvelope18(data) {
|
|
8882
9256
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
8883
9257
|
}
|
|
8884
|
-
var PLATFORM_ENUM2 =
|
|
9258
|
+
var PLATFORM_ENUM2 = z23.enum([
|
|
8885
9259
|
"youtube",
|
|
8886
9260
|
"tiktok",
|
|
8887
9261
|
"instagram",
|
|
@@ -8898,10 +9272,10 @@ function registerPipelineTools(server) {
|
|
|
8898
9272
|
"check_pipeline_readiness",
|
|
8899
9273
|
"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.",
|
|
8900
9274
|
{
|
|
8901
|
-
project_id:
|
|
8902
|
-
platforms:
|
|
8903
|
-
estimated_posts:
|
|
8904
|
-
response_format:
|
|
9275
|
+
project_id: z23.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
|
|
9276
|
+
platforms: z23.array(PLATFORM_ENUM2).min(1).describe("Target platforms to check"),
|
|
9277
|
+
estimated_posts: z23.number().min(1).max(50).default(5).describe("Estimated posts to generate"),
|
|
9278
|
+
response_format: z23.enum(["text", "json"]).optional().describe("Response format")
|
|
8905
9279
|
},
|
|
8906
9280
|
async ({ project_id, platforms, estimated_posts, response_format }) => {
|
|
8907
9281
|
const format = response_format ?? "text";
|
|
@@ -8977,7 +9351,7 @@ function registerPipelineTools(server) {
|
|
|
8977
9351
|
});
|
|
8978
9352
|
if (format === "json") {
|
|
8979
9353
|
return {
|
|
8980
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
9354
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope18(result), null, 2) }]
|
|
8981
9355
|
};
|
|
8982
9356
|
}
|
|
8983
9357
|
const lines = [];
|
|
@@ -9007,7 +9381,7 @@ function registerPipelineTools(server) {
|
|
|
9007
9381
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
9008
9382
|
} catch (err) {
|
|
9009
9383
|
const durationMs = Date.now() - startedAt;
|
|
9010
|
-
const message =
|
|
9384
|
+
const message = sanitizeError2(err);
|
|
9011
9385
|
logMcpToolInvocation({
|
|
9012
9386
|
toolName: "check_pipeline_readiness",
|
|
9013
9387
|
status: "error",
|
|
@@ -9025,22 +9399,22 @@ function registerPipelineTools(server) {
|
|
|
9025
9399
|
"run_content_pipeline",
|
|
9026
9400
|
"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.",
|
|
9027
9401
|
{
|
|
9028
|
-
project_id:
|
|
9029
|
-
topic:
|
|
9030
|
-
source_url:
|
|
9031
|
-
platforms:
|
|
9032
|
-
days:
|
|
9033
|
-
posts_per_day:
|
|
9034
|
-
approval_mode:
|
|
9402
|
+
project_id: z23.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
|
|
9403
|
+
topic: z23.string().optional().describe("Content topic (required if no source_url)"),
|
|
9404
|
+
source_url: z23.string().optional().describe("URL to extract content from"),
|
|
9405
|
+
platforms: z23.array(PLATFORM_ENUM2).min(1).describe("Target platforms"),
|
|
9406
|
+
days: z23.number().min(1).max(7).default(5).describe("Days to plan"),
|
|
9407
|
+
posts_per_day: z23.number().min(1).max(3).default(1).describe("Posts per platform per day"),
|
|
9408
|
+
approval_mode: z23.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe(
|
|
9035
9409
|
"auto: approve all passing quality. review_all: flag everything. review_low_confidence: auto-approve high scorers."
|
|
9036
9410
|
),
|
|
9037
|
-
auto_approve_threshold:
|
|
9411
|
+
auto_approve_threshold: z23.number().min(0).max(35).default(28).describe(
|
|
9038
9412
|
"Quality score threshold for auto-approval (used in auto/review_low_confidence modes)"
|
|
9039
9413
|
),
|
|
9040
|
-
max_credits:
|
|
9041
|
-
dry_run:
|
|
9042
|
-
skip_stages:
|
|
9043
|
-
response_format:
|
|
9414
|
+
max_credits: z23.number().optional().describe("Credit budget cap"),
|
|
9415
|
+
dry_run: z23.boolean().default(false).describe("If true, skip scheduling and return plan only"),
|
|
9416
|
+
skip_stages: z23.array(z23.enum(["research", "quality", "schedule"])).optional().describe("Stages to skip"),
|
|
9417
|
+
response_format: z23.enum(["text", "json"]).default("json")
|
|
9044
9418
|
},
|
|
9045
9419
|
async ({
|
|
9046
9420
|
project_id,
|
|
@@ -9168,7 +9542,7 @@ function registerPipelineTools(server) {
|
|
|
9168
9542
|
} catch (deductErr) {
|
|
9169
9543
|
errors.push({
|
|
9170
9544
|
stage: "planning",
|
|
9171
|
-
message: `Credit deduction failed: ${
|
|
9545
|
+
message: `Credit deduction failed: ${sanitizeError2(deductErr)}`
|
|
9172
9546
|
});
|
|
9173
9547
|
}
|
|
9174
9548
|
}
|
|
@@ -9339,7 +9713,7 @@ function registerPipelineTools(server) {
|
|
|
9339
9713
|
} catch (schedErr) {
|
|
9340
9714
|
errors.push({
|
|
9341
9715
|
stage: "schedule",
|
|
9342
|
-
message: `Failed to schedule ${post.id}: ${
|
|
9716
|
+
message: `Failed to schedule ${post.id}: ${sanitizeError2(schedErr)}`
|
|
9343
9717
|
});
|
|
9344
9718
|
}
|
|
9345
9719
|
}
|
|
@@ -9402,7 +9776,7 @@ function registerPipelineTools(server) {
|
|
|
9402
9776
|
if (response_format === "json") {
|
|
9403
9777
|
return {
|
|
9404
9778
|
content: [
|
|
9405
|
-
{ type: "text", text: JSON.stringify(
|
|
9779
|
+
{ type: "text", text: JSON.stringify(asEnvelope18(resultPayload), null, 2) }
|
|
9406
9780
|
]
|
|
9407
9781
|
};
|
|
9408
9782
|
}
|
|
@@ -9428,7 +9802,7 @@ function registerPipelineTools(server) {
|
|
|
9428
9802
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
9429
9803
|
} catch (err) {
|
|
9430
9804
|
const durationMs = Date.now() - startedAt;
|
|
9431
|
-
const message =
|
|
9805
|
+
const message = sanitizeError2(err);
|
|
9432
9806
|
logMcpToolInvocation({
|
|
9433
9807
|
toolName: "run_content_pipeline",
|
|
9434
9808
|
status: "error",
|
|
@@ -9463,8 +9837,8 @@ function registerPipelineTools(server) {
|
|
|
9463
9837
|
"get_pipeline_status",
|
|
9464
9838
|
"Check status of a pipeline run, including stages completed, pending approvals, and scheduled posts.",
|
|
9465
9839
|
{
|
|
9466
|
-
pipeline_id:
|
|
9467
|
-
response_format:
|
|
9840
|
+
pipeline_id: z23.string().uuid().optional().describe("Pipeline run ID (omit for latest)"),
|
|
9841
|
+
response_format: z23.enum(["text", "json"]).optional()
|
|
9468
9842
|
},
|
|
9469
9843
|
async ({ pipeline_id, response_format }) => {
|
|
9470
9844
|
const format = response_format ?? "text";
|
|
@@ -9490,7 +9864,7 @@ function registerPipelineTools(server) {
|
|
|
9490
9864
|
}
|
|
9491
9865
|
if (format === "json") {
|
|
9492
9866
|
return {
|
|
9493
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
9867
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope18(data), null, 2) }]
|
|
9494
9868
|
};
|
|
9495
9869
|
}
|
|
9496
9870
|
const lines = [];
|
|
@@ -9524,9 +9898,9 @@ function registerPipelineTools(server) {
|
|
|
9524
9898
|
"auto_approve_plan",
|
|
9525
9899
|
"Batch auto-approve posts in a content plan that meet quality thresholds. Posts below the threshold are flagged for manual review.",
|
|
9526
9900
|
{
|
|
9527
|
-
plan_id:
|
|
9528
|
-
quality_threshold:
|
|
9529
|
-
response_format:
|
|
9901
|
+
plan_id: z23.string().uuid().describe("Content plan ID"),
|
|
9902
|
+
quality_threshold: z23.number().min(0).max(35).default(26).describe("Minimum quality score to auto-approve"),
|
|
9903
|
+
response_format: z23.enum(["text", "json"]).default("json")
|
|
9530
9904
|
},
|
|
9531
9905
|
async ({ plan_id, quality_threshold, response_format }) => {
|
|
9532
9906
|
const startedAt = Date.now();
|
|
@@ -9632,7 +10006,7 @@ function registerPipelineTools(server) {
|
|
|
9632
10006
|
if (response_format === "json") {
|
|
9633
10007
|
return {
|
|
9634
10008
|
content: [
|
|
9635
|
-
{ type: "text", text: JSON.stringify(
|
|
10009
|
+
{ type: "text", text: JSON.stringify(asEnvelope18(resultPayload), null, 2) }
|
|
9636
10010
|
]
|
|
9637
10011
|
};
|
|
9638
10012
|
}
|
|
@@ -9652,7 +10026,7 @@ function registerPipelineTools(server) {
|
|
|
9652
10026
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
9653
10027
|
} catch (err) {
|
|
9654
10028
|
const durationMs = Date.now() - startedAt;
|
|
9655
|
-
const message =
|
|
10029
|
+
const message = sanitizeError2(err);
|
|
9656
10030
|
logMcpToolInvocation({
|
|
9657
10031
|
toolName: "auto_approve_plan",
|
|
9658
10032
|
status: "error",
|
|
@@ -9686,9 +10060,9 @@ function buildPlanPrompt(topic, platforms, days, postsPerDay, sourceUrl) {
|
|
|
9686
10060
|
}
|
|
9687
10061
|
|
|
9688
10062
|
// src/tools/suggest.ts
|
|
9689
|
-
import { z as
|
|
10063
|
+
import { z as z24 } from "zod";
|
|
9690
10064
|
init_supabase();
|
|
9691
|
-
function
|
|
10065
|
+
function asEnvelope19(data) {
|
|
9692
10066
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
9693
10067
|
}
|
|
9694
10068
|
function registerSuggestTools(server) {
|
|
@@ -9696,9 +10070,9 @@ function registerSuggestTools(server) {
|
|
|
9696
10070
|
"suggest_next_content",
|
|
9697
10071
|
"Suggest next content topics based on performance insights, past content, and competitor patterns. No AI call, no credit cost \u2014 purely data-driven recommendations.",
|
|
9698
10072
|
{
|
|
9699
|
-
project_id:
|
|
9700
|
-
count:
|
|
9701
|
-
response_format:
|
|
10073
|
+
project_id: z24.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
|
|
10074
|
+
count: z24.number().min(1).max(10).default(3).describe("Number of suggestions to return"),
|
|
10075
|
+
response_format: z24.enum(["text", "json"]).optional()
|
|
9702
10076
|
},
|
|
9703
10077
|
async ({ project_id, count, response_format }) => {
|
|
9704
10078
|
const format = response_format ?? "text";
|
|
@@ -9808,7 +10182,7 @@ function registerSuggestTools(server) {
|
|
|
9808
10182
|
if (format === "json") {
|
|
9809
10183
|
return {
|
|
9810
10184
|
content: [
|
|
9811
|
-
{ type: "text", text: JSON.stringify(
|
|
10185
|
+
{ type: "text", text: JSON.stringify(asEnvelope19(resultPayload), null, 2) }
|
|
9812
10186
|
]
|
|
9813
10187
|
};
|
|
9814
10188
|
}
|
|
@@ -9830,7 +10204,7 @@ ${i + 1}. ${s.topic}`);
|
|
|
9830
10204
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
9831
10205
|
} catch (err) {
|
|
9832
10206
|
const durationMs = Date.now() - startedAt;
|
|
9833
|
-
const message =
|
|
10207
|
+
const message = sanitizeError2(err);
|
|
9834
10208
|
logMcpToolInvocation({
|
|
9835
10209
|
toolName: "suggest_next_content",
|
|
9836
10210
|
status: "error",
|
|
@@ -9847,7 +10221,7 @@ ${i + 1}. ${s.topic}`);
|
|
|
9847
10221
|
}
|
|
9848
10222
|
|
|
9849
10223
|
// src/tools/digest.ts
|
|
9850
|
-
import { z as
|
|
10224
|
+
import { z as z25 } from "zod";
|
|
9851
10225
|
init_supabase();
|
|
9852
10226
|
|
|
9853
10227
|
// src/lib/anomaly-detector.ts
|
|
@@ -9951,10 +10325,10 @@ function detectAnomalies(currentData, previousData, sensitivity = "medium", aver
|
|
|
9951
10325
|
}
|
|
9952
10326
|
|
|
9953
10327
|
// src/tools/digest.ts
|
|
9954
|
-
function
|
|
10328
|
+
function asEnvelope20(data) {
|
|
9955
10329
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
9956
10330
|
}
|
|
9957
|
-
var PLATFORM_ENUM3 =
|
|
10331
|
+
var PLATFORM_ENUM3 = z25.enum([
|
|
9958
10332
|
"youtube",
|
|
9959
10333
|
"tiktok",
|
|
9960
10334
|
"instagram",
|
|
@@ -9969,10 +10343,10 @@ function registerDigestTools(server) {
|
|
|
9969
10343
|
"generate_performance_digest",
|
|
9970
10344
|
"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.",
|
|
9971
10345
|
{
|
|
9972
|
-
project_id:
|
|
9973
|
-
period:
|
|
9974
|
-
include_recommendations:
|
|
9975
|
-
response_format:
|
|
10346
|
+
project_id: z25.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
|
|
10347
|
+
period: z25.enum(["7d", "14d", "30d"]).default("7d").describe("Time period to analyze"),
|
|
10348
|
+
include_recommendations: z25.boolean().default(true),
|
|
10349
|
+
response_format: z25.enum(["text", "json"]).optional()
|
|
9976
10350
|
},
|
|
9977
10351
|
async ({ project_id, period, include_recommendations, response_format }) => {
|
|
9978
10352
|
const format = response_format ?? "text";
|
|
@@ -10128,7 +10502,7 @@ function registerDigestTools(server) {
|
|
|
10128
10502
|
});
|
|
10129
10503
|
if (format === "json") {
|
|
10130
10504
|
return {
|
|
10131
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
10505
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope20(digest), null, 2) }]
|
|
10132
10506
|
};
|
|
10133
10507
|
}
|
|
10134
10508
|
const lines = [];
|
|
@@ -10169,7 +10543,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
|
|
|
10169
10543
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
10170
10544
|
} catch (err) {
|
|
10171
10545
|
const durationMs = Date.now() - startedAt;
|
|
10172
|
-
const message =
|
|
10546
|
+
const message = sanitizeError2(err);
|
|
10173
10547
|
logMcpToolInvocation({
|
|
10174
10548
|
toolName: "generate_performance_digest",
|
|
10175
10549
|
status: "error",
|
|
@@ -10187,11 +10561,11 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
|
|
|
10187
10561
|
"detect_anomalies",
|
|
10188
10562
|
"Detect significant performance changes: spikes, drops, viral content, trend shifts. Compares current period against previous equal-length period. No AI call, no credit cost.",
|
|
10189
10563
|
{
|
|
10190
|
-
project_id:
|
|
10191
|
-
days:
|
|
10192
|
-
sensitivity:
|
|
10193
|
-
platforms:
|
|
10194
|
-
response_format:
|
|
10564
|
+
project_id: z25.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
|
|
10565
|
+
days: z25.number().min(7).max(90).default(14).describe("Days to analyze"),
|
|
10566
|
+
sensitivity: z25.enum(["low", "medium", "high"]).default("medium").describe("Detection sensitivity: low=50%+, medium=30%+, high=15%+ changes"),
|
|
10567
|
+
platforms: z25.array(PLATFORM_ENUM3).optional().describe("Filter to specific platforms"),
|
|
10568
|
+
response_format: z25.enum(["text", "json"]).optional()
|
|
10195
10569
|
},
|
|
10196
10570
|
async ({ project_id, days, sensitivity, platforms, response_format }) => {
|
|
10197
10571
|
const format = response_format ?? "text";
|
|
@@ -10242,7 +10616,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
|
|
|
10242
10616
|
if (format === "json") {
|
|
10243
10617
|
return {
|
|
10244
10618
|
content: [
|
|
10245
|
-
{ type: "text", text: JSON.stringify(
|
|
10619
|
+
{ type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
|
|
10246
10620
|
]
|
|
10247
10621
|
};
|
|
10248
10622
|
}
|
|
@@ -10266,7 +10640,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
|
|
|
10266
10640
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
10267
10641
|
} catch (err) {
|
|
10268
10642
|
const durationMs = Date.now() - startedAt;
|
|
10269
|
-
const message =
|
|
10643
|
+
const message = sanitizeError2(err);
|
|
10270
10644
|
logMcpToolInvocation({
|
|
10271
10645
|
toolName: "detect_anomalies",
|
|
10272
10646
|
status: "error",
|
|
@@ -10283,9 +10657,451 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
|
|
|
10283
10657
|
}
|
|
10284
10658
|
|
|
10285
10659
|
// src/tools/brandRuntime.ts
|
|
10286
|
-
import { z as
|
|
10660
|
+
import { z as z26 } from "zod";
|
|
10287
10661
|
init_supabase();
|
|
10288
|
-
|
|
10662
|
+
|
|
10663
|
+
// src/lib/brandScoring.ts
|
|
10664
|
+
var WEIGHTS = {
|
|
10665
|
+
toneAlignment: 0.3,
|
|
10666
|
+
vocabularyAdherence: 0.25,
|
|
10667
|
+
avoidCompliance: 0.2,
|
|
10668
|
+
audienceRelevance: 0.15,
|
|
10669
|
+
brandMentions: 0.05,
|
|
10670
|
+
structuralPatterns: 0.05
|
|
10671
|
+
};
|
|
10672
|
+
function norm(content) {
|
|
10673
|
+
return content.toLowerCase().replace(/[^a-z0-9\s]/g, " ");
|
|
10674
|
+
}
|
|
10675
|
+
function findMatches(content, terms) {
|
|
10676
|
+
const n = norm(content);
|
|
10677
|
+
return terms.filter((t) => n.includes(t.toLowerCase()));
|
|
10678
|
+
}
|
|
10679
|
+
function findMissing(content, terms) {
|
|
10680
|
+
const n = norm(content);
|
|
10681
|
+
return terms.filter((t) => !n.includes(t.toLowerCase()));
|
|
10682
|
+
}
|
|
10683
|
+
var FABRICATION_PATTERNS = [
|
|
10684
|
+
{ regex: /\b\d+[,.]?\d*\s*(%|percent)/gi, label: "unverified percentage" },
|
|
10685
|
+
{ regex: /\b(award[- ]?winning|best[- ]selling|#\s*1)\b/gi, label: "unverified ranking" },
|
|
10686
|
+
{
|
|
10687
|
+
regex: /\b(guaranteed|proven to|studies show|scientifically proven)\b/gi,
|
|
10688
|
+
label: "unverified claim"
|
|
10689
|
+
},
|
|
10690
|
+
{
|
|
10691
|
+
regex: /\b(always works|100% effective|risk[- ]?free|no risk)\b/gi,
|
|
10692
|
+
label: "absolute claim"
|
|
10693
|
+
}
|
|
10694
|
+
];
|
|
10695
|
+
function detectFabricationPatterns(content) {
|
|
10696
|
+
const matches = [];
|
|
10697
|
+
for (const { regex, label } of FABRICATION_PATTERNS) {
|
|
10698
|
+
const re = new RegExp(regex.source, regex.flags);
|
|
10699
|
+
let m;
|
|
10700
|
+
while ((m = re.exec(content)) !== null) {
|
|
10701
|
+
matches.push({ label, match: m[0] });
|
|
10702
|
+
}
|
|
10703
|
+
}
|
|
10704
|
+
return matches;
|
|
10705
|
+
}
|
|
10706
|
+
function scoreTone(content, profile) {
|
|
10707
|
+
const terms = profile.voiceProfile?.tone || [];
|
|
10708
|
+
if (!terms.length)
|
|
10709
|
+
return {
|
|
10710
|
+
score: 50,
|
|
10711
|
+
weight: WEIGHTS.toneAlignment,
|
|
10712
|
+
issues: [],
|
|
10713
|
+
suggestions: ["Define brand tone words for better consistency measurement"]
|
|
10714
|
+
};
|
|
10715
|
+
const matched = findMatches(content, terms);
|
|
10716
|
+
const missing = findMissing(content, terms);
|
|
10717
|
+
const score = Math.min(100, Math.round(matched.length / terms.length * 100));
|
|
10718
|
+
const issues = [];
|
|
10719
|
+
const suggestions = [];
|
|
10720
|
+
if (missing.length > 0) {
|
|
10721
|
+
issues.push(`Missing tone signals: ${missing.join(", ")}`);
|
|
10722
|
+
suggestions.push(`Try incorporating tone words: ${missing.slice(0, 3).join(", ")}`);
|
|
10723
|
+
}
|
|
10724
|
+
return { score, weight: WEIGHTS.toneAlignment, issues, suggestions };
|
|
10725
|
+
}
|
|
10726
|
+
function scoreVocab(content, profile) {
|
|
10727
|
+
const preferred = [
|
|
10728
|
+
...profile.voiceProfile?.languagePatterns || [],
|
|
10729
|
+
...profile.vocabularyRules?.preferredTerms || []
|
|
10730
|
+
];
|
|
10731
|
+
if (!preferred.length)
|
|
10732
|
+
return {
|
|
10733
|
+
score: 50,
|
|
10734
|
+
weight: WEIGHTS.vocabularyAdherence,
|
|
10735
|
+
issues: [],
|
|
10736
|
+
suggestions: ["Add preferred terms to improve vocabulary scoring"]
|
|
10737
|
+
};
|
|
10738
|
+
const matched = findMatches(content, preferred);
|
|
10739
|
+
const missing = findMissing(content, preferred);
|
|
10740
|
+
const score = Math.min(100, Math.round(matched.length / preferred.length * 100));
|
|
10741
|
+
const issues = [];
|
|
10742
|
+
const suggestions = [];
|
|
10743
|
+
if (missing.length > 0 && score < 60) {
|
|
10744
|
+
issues.push(`Low preferred term usage (${matched.length}/${preferred.length})`);
|
|
10745
|
+
suggestions.push(`Consider using: ${missing.slice(0, 3).join(", ")}`);
|
|
10746
|
+
}
|
|
10747
|
+
return { score, weight: WEIGHTS.vocabularyAdherence, issues, suggestions };
|
|
10748
|
+
}
|
|
10749
|
+
function scoreAvoid(content, profile) {
|
|
10750
|
+
const banned = [
|
|
10751
|
+
...profile.voiceProfile?.avoidPatterns || [],
|
|
10752
|
+
...profile.vocabularyRules?.bannedTerms || []
|
|
10753
|
+
];
|
|
10754
|
+
if (!banned.length)
|
|
10755
|
+
return {
|
|
10756
|
+
score: 100,
|
|
10757
|
+
weight: WEIGHTS.avoidCompliance,
|
|
10758
|
+
issues: [],
|
|
10759
|
+
suggestions: []
|
|
10760
|
+
};
|
|
10761
|
+
const violations = findMatches(content, banned);
|
|
10762
|
+
const score = violations.length === 0 ? 100 : Math.max(0, 100 - violations.length * 25);
|
|
10763
|
+
const issues = [];
|
|
10764
|
+
const suggestions = [];
|
|
10765
|
+
if (violations.length > 0) {
|
|
10766
|
+
issues.push(`Banned/avoided terms found: ${violations.join(", ")}`);
|
|
10767
|
+
suggestions.push(`Remove or replace: ${violations.join(", ")}`);
|
|
10768
|
+
}
|
|
10769
|
+
return { score, weight: WEIGHTS.avoidCompliance, issues, suggestions };
|
|
10770
|
+
}
|
|
10771
|
+
function scoreAudience(content, profile) {
|
|
10772
|
+
const terms = [];
|
|
10773
|
+
const d = profile.targetAudience?.demographics;
|
|
10774
|
+
const p = profile.targetAudience?.psychographics;
|
|
10775
|
+
if (d?.ageRange) terms.push(d.ageRange);
|
|
10776
|
+
if (d?.location) terms.push(d.location);
|
|
10777
|
+
if (p?.interests) terms.push(...p.interests);
|
|
10778
|
+
if (p?.painPoints) terms.push(...p.painPoints);
|
|
10779
|
+
if (p?.aspirations) terms.push(...p.aspirations);
|
|
10780
|
+
const valid = terms.filter(Boolean);
|
|
10781
|
+
if (!valid.length)
|
|
10782
|
+
return {
|
|
10783
|
+
score: 50,
|
|
10784
|
+
weight: WEIGHTS.audienceRelevance,
|
|
10785
|
+
issues: [],
|
|
10786
|
+
suggestions: ["Define target audience details for relevance scoring"]
|
|
10787
|
+
};
|
|
10788
|
+
const matched = findMatches(content, valid);
|
|
10789
|
+
const score = Math.min(100, Math.round(matched.length / valid.length * 100));
|
|
10790
|
+
const issues = [];
|
|
10791
|
+
const suggestions = [];
|
|
10792
|
+
if (score < 40) {
|
|
10793
|
+
issues.push("Content has low audience relevance");
|
|
10794
|
+
suggestions.push(
|
|
10795
|
+
`Reference audience pain points or interests: ${valid.slice(0, 3).join(", ")}`
|
|
10796
|
+
);
|
|
10797
|
+
}
|
|
10798
|
+
return { score, weight: WEIGHTS.audienceRelevance, issues, suggestions };
|
|
10799
|
+
}
|
|
10800
|
+
function scoreBrand(content, profile) {
|
|
10801
|
+
const name = profile.name?.toLowerCase();
|
|
10802
|
+
if (!name)
|
|
10803
|
+
return {
|
|
10804
|
+
score: 50,
|
|
10805
|
+
weight: WEIGHTS.brandMentions,
|
|
10806
|
+
issues: [],
|
|
10807
|
+
suggestions: []
|
|
10808
|
+
};
|
|
10809
|
+
const mentioned = norm(content).includes(name);
|
|
10810
|
+
const issues = [];
|
|
10811
|
+
const suggestions = [];
|
|
10812
|
+
if (!mentioned) {
|
|
10813
|
+
issues.push("Brand name not mentioned");
|
|
10814
|
+
suggestions.push(`Include "${profile.name}" in the content`);
|
|
10815
|
+
}
|
|
10816
|
+
return {
|
|
10817
|
+
score: mentioned ? 100 : 0,
|
|
10818
|
+
weight: WEIGHTS.brandMentions,
|
|
10819
|
+
issues,
|
|
10820
|
+
suggestions
|
|
10821
|
+
};
|
|
10822
|
+
}
|
|
10823
|
+
function scoreStructure(content, profile) {
|
|
10824
|
+
const rules = profile.writingStyleRules;
|
|
10825
|
+
if (!rules)
|
|
10826
|
+
return {
|
|
10827
|
+
score: 50,
|
|
10828
|
+
weight: WEIGHTS.structuralPatterns,
|
|
10829
|
+
issues: [],
|
|
10830
|
+
suggestions: []
|
|
10831
|
+
};
|
|
10832
|
+
let score = 100;
|
|
10833
|
+
const issues = [];
|
|
10834
|
+
const suggestions = [];
|
|
10835
|
+
if (rules.perspective) {
|
|
10836
|
+
const markers = {
|
|
10837
|
+
"first-singular": [/\bI\b/g, /\bmy\b/gi],
|
|
10838
|
+
"first-plural": [/\bwe\b/gi, /\bour\b/gi],
|
|
10839
|
+
second: [/\byou\b/gi, /\byour\b/gi],
|
|
10840
|
+
third: [/\bthey\b/gi, /\btheir\b/gi]
|
|
10841
|
+
};
|
|
10842
|
+
const expected = markers[rules.perspective];
|
|
10843
|
+
if (expected && !expected.some((r) => r.test(content))) {
|
|
10844
|
+
score -= 30;
|
|
10845
|
+
issues.push(`Expected ${rules.perspective} perspective not detected`);
|
|
10846
|
+
suggestions.push(`Use ${rules.perspective} perspective pronouns`);
|
|
10847
|
+
}
|
|
10848
|
+
}
|
|
10849
|
+
if (rules.useContractions === false) {
|
|
10850
|
+
const found = content.match(
|
|
10851
|
+
/\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
|
|
10852
|
+
);
|
|
10853
|
+
if (found && found.length > 0) {
|
|
10854
|
+
score -= Math.min(40, found.length * 10);
|
|
10855
|
+
issues.push(`Contractions found (${found.length}): ${found.slice(0, 3).join(", ")}`);
|
|
10856
|
+
suggestions.push("Expand contractions to full forms");
|
|
10857
|
+
}
|
|
10858
|
+
}
|
|
10859
|
+
if (rules.emojiPolicy === "none") {
|
|
10860
|
+
const emojis = content.match(
|
|
10861
|
+
/[\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
|
|
10862
|
+
);
|
|
10863
|
+
if (emojis && emojis.length > 0) {
|
|
10864
|
+
score -= 20;
|
|
10865
|
+
issues.push('Emojis found but emoji policy is "none"');
|
|
10866
|
+
suggestions.push("Remove emojis from content");
|
|
10867
|
+
}
|
|
10868
|
+
}
|
|
10869
|
+
return {
|
|
10870
|
+
score: Math.max(0, score),
|
|
10871
|
+
weight: WEIGHTS.structuralPatterns,
|
|
10872
|
+
issues,
|
|
10873
|
+
suggestions
|
|
10874
|
+
};
|
|
10875
|
+
}
|
|
10876
|
+
function computeBrandConsistency(content, profile, threshold = 60) {
|
|
10877
|
+
if (!content || !profile) {
|
|
10878
|
+
const neutral = {
|
|
10879
|
+
score: 50,
|
|
10880
|
+
weight: 0,
|
|
10881
|
+
issues: [],
|
|
10882
|
+
suggestions: []
|
|
10883
|
+
};
|
|
10884
|
+
return {
|
|
10885
|
+
overall: 50,
|
|
10886
|
+
passed: false,
|
|
10887
|
+
dimensions: {
|
|
10888
|
+
toneAlignment: { ...neutral, weight: WEIGHTS.toneAlignment },
|
|
10889
|
+
vocabularyAdherence: { ...neutral, weight: WEIGHTS.vocabularyAdherence },
|
|
10890
|
+
avoidCompliance: { ...neutral, weight: WEIGHTS.avoidCompliance },
|
|
10891
|
+
audienceRelevance: { ...neutral, weight: WEIGHTS.audienceRelevance },
|
|
10892
|
+
brandMentions: { ...neutral, weight: WEIGHTS.brandMentions },
|
|
10893
|
+
structuralPatterns: { ...neutral, weight: WEIGHTS.structuralPatterns }
|
|
10894
|
+
},
|
|
10895
|
+
preferredTermsUsed: [],
|
|
10896
|
+
bannedTermsFound: [],
|
|
10897
|
+
fabricationWarnings: []
|
|
10898
|
+
};
|
|
10899
|
+
}
|
|
10900
|
+
const dimensions = {
|
|
10901
|
+
toneAlignment: scoreTone(content, profile),
|
|
10902
|
+
vocabularyAdherence: scoreVocab(content, profile),
|
|
10903
|
+
avoidCompliance: scoreAvoid(content, profile),
|
|
10904
|
+
audienceRelevance: scoreAudience(content, profile),
|
|
10905
|
+
brandMentions: scoreBrand(content, profile),
|
|
10906
|
+
structuralPatterns: scoreStructure(content, profile)
|
|
10907
|
+
};
|
|
10908
|
+
const overall = Math.round(
|
|
10909
|
+
Object.values(dimensions).reduce((sum, d) => sum + d.score * d.weight, 0)
|
|
10910
|
+
);
|
|
10911
|
+
const preferred = [
|
|
10912
|
+
...profile.voiceProfile?.languagePatterns || [],
|
|
10913
|
+
...profile.vocabularyRules?.preferredTerms || []
|
|
10914
|
+
];
|
|
10915
|
+
const banned = [
|
|
10916
|
+
...profile.voiceProfile?.avoidPatterns || [],
|
|
10917
|
+
...profile.vocabularyRules?.bannedTerms || []
|
|
10918
|
+
];
|
|
10919
|
+
const fabrications = detectFabricationPatterns(content);
|
|
10920
|
+
return {
|
|
10921
|
+
overall: Math.max(0, Math.min(100, overall)),
|
|
10922
|
+
passed: overall >= threshold,
|
|
10923
|
+
dimensions,
|
|
10924
|
+
preferredTermsUsed: findMatches(content, preferred),
|
|
10925
|
+
bannedTermsFound: findMatches(content, banned),
|
|
10926
|
+
fabricationWarnings: fabrications.map((f) => `${f.label}: "${f.match}"`)
|
|
10927
|
+
};
|
|
10928
|
+
}
|
|
10929
|
+
|
|
10930
|
+
// src/lib/colorAudit.ts
|
|
10931
|
+
function hexToRgb(hex) {
|
|
10932
|
+
const h = hex.replace("#", "");
|
|
10933
|
+
const full = h.length === 3 ? h[0] + h[0] + h[1] + h[1] + h[2] + h[2] : h;
|
|
10934
|
+
const n = parseInt(full, 16);
|
|
10935
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
10936
|
+
}
|
|
10937
|
+
function srgbToLinear(c) {
|
|
10938
|
+
const s = c / 255;
|
|
10939
|
+
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
10940
|
+
}
|
|
10941
|
+
function hexToLab(hex) {
|
|
10942
|
+
const [r, g, b] = hexToRgb(hex);
|
|
10943
|
+
const lr = srgbToLinear(r);
|
|
10944
|
+
const lg = srgbToLinear(g);
|
|
10945
|
+
const lb = srgbToLinear(b);
|
|
10946
|
+
const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
|
|
10947
|
+
const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
|
|
10948
|
+
const z29 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
|
|
10949
|
+
const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
|
|
10950
|
+
const fx = f(x / 0.95047);
|
|
10951
|
+
const fy = f(y / 1);
|
|
10952
|
+
const fz = f(z29 / 1.08883);
|
|
10953
|
+
return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
|
|
10954
|
+
}
|
|
10955
|
+
function deltaE2000(lab1, lab2) {
|
|
10956
|
+
const { L: L1, a: a1, b: b1 } = lab1;
|
|
10957
|
+
const { L: L2, a: a2, b: b2 } = lab2;
|
|
10958
|
+
const C1 = Math.sqrt(a1 * a1 + b1 * b1);
|
|
10959
|
+
const C2 = Math.sqrt(a2 * a2 + b2 * b2);
|
|
10960
|
+
const Cab = (C1 + C2) / 2;
|
|
10961
|
+
const Cab7 = Math.pow(Cab, 7);
|
|
10962
|
+
const G = 0.5 * (1 - Math.sqrt(Cab7 / (Cab7 + Math.pow(25, 7))));
|
|
10963
|
+
const a1p = a1 * (1 + G);
|
|
10964
|
+
const a2p = a2 * (1 + G);
|
|
10965
|
+
const C1p = Math.sqrt(a1p * a1p + b1 * b1);
|
|
10966
|
+
const C2p = Math.sqrt(a2p * a2p + b2 * b2);
|
|
10967
|
+
let h1p = Math.atan2(b1, a1p) * (180 / Math.PI);
|
|
10968
|
+
if (h1p < 0) h1p += 360;
|
|
10969
|
+
let h2p = Math.atan2(b2, a2p) * (180 / Math.PI);
|
|
10970
|
+
if (h2p < 0) h2p += 360;
|
|
10971
|
+
const dLp = L2 - L1;
|
|
10972
|
+
const dCp = C2p - C1p;
|
|
10973
|
+
let dhp;
|
|
10974
|
+
if (C1p * C2p === 0) dhp = 0;
|
|
10975
|
+
else if (Math.abs(h2p - h1p) <= 180) dhp = h2p - h1p;
|
|
10976
|
+
else if (h2p - h1p > 180) dhp = h2p - h1p - 360;
|
|
10977
|
+
else dhp = h2p - h1p + 360;
|
|
10978
|
+
const dHp = 2 * Math.sqrt(C1p * C2p) * Math.sin(dhp * Math.PI / 360);
|
|
10979
|
+
const Lp = (L1 + L2) / 2;
|
|
10980
|
+
const Cp = (C1p + C2p) / 2;
|
|
10981
|
+
let hp;
|
|
10982
|
+
if (C1p * C2p === 0) hp = h1p + h2p;
|
|
10983
|
+
else if (Math.abs(h1p - h2p) <= 180) hp = (h1p + h2p) / 2;
|
|
10984
|
+
else if (h1p + h2p < 360) hp = (h1p + h2p + 360) / 2;
|
|
10985
|
+
else hp = (h1p + h2p - 360) / 2;
|
|
10986
|
+
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);
|
|
10987
|
+
const SL = 1 + 0.015 * (Lp - 50) * (Lp - 50) / Math.sqrt(20 + (Lp - 50) * (Lp - 50));
|
|
10988
|
+
const SC = 1 + 0.045 * Cp;
|
|
10989
|
+
const SH = 1 + 0.015 * Cp * T;
|
|
10990
|
+
const Cp7 = Math.pow(Cp, 7);
|
|
10991
|
+
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);
|
|
10992
|
+
return Math.sqrt(
|
|
10993
|
+
Math.pow(dLp / SL, 2) + Math.pow(dCp / SC, 2) + Math.pow(dHp / SH, 2) + RT * (dCp / SC) * (dHp / SH)
|
|
10994
|
+
);
|
|
10995
|
+
}
|
|
10996
|
+
var COLOR_SLOTS = [
|
|
10997
|
+
"primary",
|
|
10998
|
+
"secondary",
|
|
10999
|
+
"accent",
|
|
11000
|
+
"background",
|
|
11001
|
+
"success",
|
|
11002
|
+
"warning",
|
|
11003
|
+
"error",
|
|
11004
|
+
"text",
|
|
11005
|
+
"textSecondary"
|
|
11006
|
+
];
|
|
11007
|
+
function auditBrandColors(palette, contentColors, threshold = 10) {
|
|
11008
|
+
if (!contentColors.length) return { entries: [], overallScore: 100, passed: true };
|
|
11009
|
+
const brandColors = [];
|
|
11010
|
+
for (const slot of COLOR_SLOTS) {
|
|
11011
|
+
const hex = palette[slot];
|
|
11012
|
+
if (typeof hex === "string" && hex.startsWith("#")) {
|
|
11013
|
+
brandColors.push({ slot, hex, lab: hexToLab(hex) });
|
|
11014
|
+
}
|
|
11015
|
+
}
|
|
11016
|
+
if (!brandColors.length) return { entries: [], overallScore: 50, passed: false };
|
|
11017
|
+
const entries = contentColors.map((color) => {
|
|
11018
|
+
const colorLab = hexToLab(color);
|
|
11019
|
+
let minDE = Infinity;
|
|
11020
|
+
let closest = brandColors[0];
|
|
11021
|
+
for (const bc of brandColors) {
|
|
11022
|
+
const de = deltaE2000(colorLab, bc.lab);
|
|
11023
|
+
if (de < minDE) {
|
|
11024
|
+
minDE = de;
|
|
11025
|
+
closest = bc;
|
|
11026
|
+
}
|
|
11027
|
+
}
|
|
11028
|
+
return {
|
|
11029
|
+
color,
|
|
11030
|
+
closestBrandColor: closest.hex,
|
|
11031
|
+
closestBrandSlot: closest.slot,
|
|
11032
|
+
deltaE: Math.round(minDE * 100) / 100,
|
|
11033
|
+
passed: minDE <= threshold
|
|
11034
|
+
};
|
|
11035
|
+
});
|
|
11036
|
+
const passedCount = entries.filter((e) => e.passed).length;
|
|
11037
|
+
const overallScore = Math.round(passedCount / entries.length * 100);
|
|
11038
|
+
return { entries, overallScore, passed: overallScore >= 80 };
|
|
11039
|
+
}
|
|
11040
|
+
function exportDesignTokens(palette, typography, format) {
|
|
11041
|
+
if (format === "css") return exportCSS(palette, typography);
|
|
11042
|
+
if (format === "tailwind") return JSON.stringify(exportTailwind(palette), null, 2);
|
|
11043
|
+
return JSON.stringify(exportFigma(palette, typography), null, 2);
|
|
11044
|
+
}
|
|
11045
|
+
function exportCSS(palette, typography) {
|
|
11046
|
+
const lines = [":root {"];
|
|
11047
|
+
const slots = [
|
|
11048
|
+
["--brand-primary", "primary"],
|
|
11049
|
+
["--brand-secondary", "secondary"],
|
|
11050
|
+
["--brand-accent", "accent"],
|
|
11051
|
+
["--brand-background", "background"],
|
|
11052
|
+
["--brand-success", "success"],
|
|
11053
|
+
["--brand-warning", "warning"],
|
|
11054
|
+
["--brand-error", "error"],
|
|
11055
|
+
["--brand-text", "text"],
|
|
11056
|
+
["--brand-text-secondary", "textSecondary"]
|
|
11057
|
+
];
|
|
11058
|
+
for (const [varName, key] of slots) {
|
|
11059
|
+
const v = palette[key];
|
|
11060
|
+
if (typeof v === "string" && v) lines.push(` ${varName}: ${v};`);
|
|
11061
|
+
}
|
|
11062
|
+
if (typography) {
|
|
11063
|
+
const hf = typography.headingFont;
|
|
11064
|
+
const bf = typography.bodyFont;
|
|
11065
|
+
if (hf) lines.push(` --brand-font-heading: ${hf};`);
|
|
11066
|
+
if (bf) lines.push(` --brand-font-body: ${bf};`);
|
|
11067
|
+
}
|
|
11068
|
+
lines.push("}");
|
|
11069
|
+
return lines.join("\n");
|
|
11070
|
+
}
|
|
11071
|
+
function exportTailwind(palette) {
|
|
11072
|
+
const colors = {};
|
|
11073
|
+
const map = [
|
|
11074
|
+
["brand-primary", "primary"],
|
|
11075
|
+
["brand-secondary", "secondary"],
|
|
11076
|
+
["brand-accent", "accent"],
|
|
11077
|
+
["brand-bg", "background"]
|
|
11078
|
+
];
|
|
11079
|
+
for (const [tw, key] of map) {
|
|
11080
|
+
const v = palette[key];
|
|
11081
|
+
if (typeof v === "string" && v) colors[tw] = v;
|
|
11082
|
+
}
|
|
11083
|
+
return colors;
|
|
11084
|
+
}
|
|
11085
|
+
function exportFigma(palette, typography) {
|
|
11086
|
+
const tokens = { color: {} };
|
|
11087
|
+
for (const slot of ["primary", "secondary", "accent", "background"]) {
|
|
11088
|
+
const v = palette[slot];
|
|
11089
|
+
if (typeof v === "string") tokens.color[slot] = { value: v, type: "color" };
|
|
11090
|
+
}
|
|
11091
|
+
if (typography) {
|
|
11092
|
+
const hf = typography.headingFont;
|
|
11093
|
+
const bf = typography.bodyFont;
|
|
11094
|
+
if (hf || bf) {
|
|
11095
|
+
tokens.fontFamily = {};
|
|
11096
|
+
if (hf) tokens.fontFamily.heading = { value: String(hf), type: "fontFamilies" };
|
|
11097
|
+
if (bf) tokens.fontFamily.body = { value: String(bf), type: "fontFamilies" };
|
|
11098
|
+
}
|
|
11099
|
+
}
|
|
11100
|
+
return tokens;
|
|
11101
|
+
}
|
|
11102
|
+
|
|
11103
|
+
// src/tools/brandRuntime.ts
|
|
11104
|
+
function asEnvelope21(data) {
|
|
10289
11105
|
return {
|
|
10290
11106
|
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
10291
11107
|
data
|
|
@@ -10296,7 +11112,7 @@ function registerBrandRuntimeTools(server) {
|
|
|
10296
11112
|
"get_brand_runtime",
|
|
10297
11113
|
"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.",
|
|
10298
11114
|
{
|
|
10299
|
-
project_id:
|
|
11115
|
+
project_id: z26.string().optional().describe("Project ID. Defaults to current project.")
|
|
10300
11116
|
},
|
|
10301
11117
|
async ({ project_id }) => {
|
|
10302
11118
|
const projectId = project_id || await getDefaultProjectId();
|
|
@@ -10356,7 +11172,7 @@ function registerBrandRuntimeTools(server) {
|
|
|
10356
11172
|
pagesScraped: meta.pagesScraped || 0
|
|
10357
11173
|
}
|
|
10358
11174
|
};
|
|
10359
|
-
const envelope =
|
|
11175
|
+
const envelope = asEnvelope21(runtime);
|
|
10360
11176
|
return {
|
|
10361
11177
|
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
|
|
10362
11178
|
};
|
|
@@ -10366,7 +11182,7 @@ function registerBrandRuntimeTools(server) {
|
|
|
10366
11182
|
"explain_brand_system",
|
|
10367
11183
|
"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.",
|
|
10368
11184
|
{
|
|
10369
|
-
project_id:
|
|
11185
|
+
project_id: z26.string().optional().describe("Project ID. Defaults to current project.")
|
|
10370
11186
|
},
|
|
10371
11187
|
async ({ project_id }) => {
|
|
10372
11188
|
const projectId = project_id || await getDefaultProjectId();
|
|
@@ -10478,8 +11294,8 @@ function registerBrandRuntimeTools(server) {
|
|
|
10478
11294
|
"check_brand_consistency",
|
|
10479
11295
|
"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.",
|
|
10480
11296
|
{
|
|
10481
|
-
content:
|
|
10482
|
-
project_id:
|
|
11297
|
+
content: z26.string().describe("The content text to check for brand consistency."),
|
|
11298
|
+
project_id: z26.string().optional().describe("Project ID. Defaults to current project.")
|
|
10483
11299
|
},
|
|
10484
11300
|
async ({ content, project_id }) => {
|
|
10485
11301
|
const projectId = project_id || await getDefaultProjectId();
|
|
@@ -10497,45 +11313,77 @@ function registerBrandRuntimeTools(server) {
|
|
|
10497
11313
|
};
|
|
10498
11314
|
}
|
|
10499
11315
|
const profile = row.profile_data;
|
|
10500
|
-
const
|
|
10501
|
-
const
|
|
10502
|
-
|
|
10503
|
-
|
|
10504
|
-
const bannedFound = banned.filter((t) => contentLower.includes(t.toLowerCase()));
|
|
10505
|
-
if (bannedFound.length > 0) {
|
|
10506
|
-
score -= bannedFound.length * 15;
|
|
10507
|
-
issues.push(`Banned terms found: ${bannedFound.join(", ")}`);
|
|
10508
|
-
}
|
|
10509
|
-
const avoid = profile.voiceProfile?.avoidPatterns || [];
|
|
10510
|
-
const avoidFound = avoid.filter((p) => contentLower.includes(p.toLowerCase()));
|
|
10511
|
-
if (avoidFound.length > 0) {
|
|
10512
|
-
score -= avoidFound.length * 10;
|
|
10513
|
-
issues.push(`Avoid patterns found: ${avoidFound.join(", ")}`);
|
|
10514
|
-
}
|
|
10515
|
-
const preferred = profile.vocabularyRules?.preferredTerms || [];
|
|
10516
|
-
const prefUsed = preferred.filter((t) => contentLower.includes(t.toLowerCase()));
|
|
10517
|
-
score += Math.min(15, prefUsed.length * 5);
|
|
10518
|
-
const fabPatterns = [
|
|
10519
|
-
{ regex: /\b\d+[,.]?\d*\s*(%|percent)/gi, label: "unverified percentage" },
|
|
10520
|
-
{ regex: /\b(award[- ]?winning|best[- ]selling|#\s*1)\b/gi, label: "unverified ranking" },
|
|
10521
|
-
{ regex: /\b(guaranteed|proven to|studies show)\b/gi, label: "unverified claim" }
|
|
10522
|
-
];
|
|
10523
|
-
for (const { regex, label } of fabPatterns) {
|
|
10524
|
-
regex.lastIndex = 0;
|
|
10525
|
-
if (regex.test(content)) {
|
|
10526
|
-
score -= 10;
|
|
10527
|
-
issues.push(`Potential ${label} detected`);
|
|
10528
|
-
}
|
|
10529
|
-
}
|
|
10530
|
-
score = Math.max(0, Math.min(100, score));
|
|
10531
|
-
const checkResult = {
|
|
10532
|
-
score,
|
|
10533
|
-
passed: score >= 60,
|
|
10534
|
-
issues,
|
|
10535
|
-
preferredTermsUsed: prefUsed,
|
|
10536
|
-
bannedTermsFound: bannedFound
|
|
11316
|
+
const checkResult = computeBrandConsistency(content, profile);
|
|
11317
|
+
const envelope = asEnvelope21(checkResult);
|
|
11318
|
+
return {
|
|
11319
|
+
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
|
|
10537
11320
|
};
|
|
10538
|
-
|
|
11321
|
+
}
|
|
11322
|
+
);
|
|
11323
|
+
server.tool(
|
|
11324
|
+
"audit_brand_colors",
|
|
11325
|
+
"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.",
|
|
11326
|
+
{
|
|
11327
|
+
content_colors: z26.array(z26.string()).describe('Hex color strings used in the content (e.g., ["#FF0000", "#00FF00"])'),
|
|
11328
|
+
project_id: z26.string().optional().describe("Project ID. Defaults to current project."),
|
|
11329
|
+
threshold: z26.number().optional().describe("Max Delta E for on-brand (default 10). Lower = stricter.")
|
|
11330
|
+
},
|
|
11331
|
+
async ({ content_colors, project_id, threshold }) => {
|
|
11332
|
+
const projectId = project_id || await getDefaultProjectId();
|
|
11333
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", { action: "brand-profile", projectId });
|
|
11334
|
+
const row = !efError && result?.success ? result.profile : null;
|
|
11335
|
+
if (!row?.profile_data?.colorPalette) {
|
|
11336
|
+
return {
|
|
11337
|
+
content: [
|
|
11338
|
+
{
|
|
11339
|
+
type: "text",
|
|
11340
|
+
text: "No brand color palette found. Extract a brand profile first."
|
|
11341
|
+
}
|
|
11342
|
+
],
|
|
11343
|
+
isError: true
|
|
11344
|
+
};
|
|
11345
|
+
}
|
|
11346
|
+
const auditResult = auditBrandColors(
|
|
11347
|
+
row.profile_data.colorPalette,
|
|
11348
|
+
content_colors,
|
|
11349
|
+
threshold ?? 10
|
|
11350
|
+
);
|
|
11351
|
+
const envelope = asEnvelope21(auditResult);
|
|
11352
|
+
return {
|
|
11353
|
+
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
|
|
11354
|
+
};
|
|
11355
|
+
}
|
|
11356
|
+
);
|
|
11357
|
+
server.tool(
|
|
11358
|
+
"export_design_tokens",
|
|
11359
|
+
"Export brand palette and typography as design tokens. Supports CSS custom properties, Tailwind config, and Figma Tokens JSON formats.",
|
|
11360
|
+
{
|
|
11361
|
+
format: z26.enum(["css", "tailwind", "figma"]).describe(
|
|
11362
|
+
"Output format: css (CSS variables), tailwind (theme.extend.colors), figma (Figma Tokens JSON)"
|
|
11363
|
+
),
|
|
11364
|
+
project_id: z26.string().optional().describe("Project ID. Defaults to current project.")
|
|
11365
|
+
},
|
|
11366
|
+
async ({ format, project_id }) => {
|
|
11367
|
+
const projectId = project_id || await getDefaultProjectId();
|
|
11368
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", { action: "brand-profile", projectId });
|
|
11369
|
+
const row = !efError && result?.success ? result.profile : null;
|
|
11370
|
+
if (!row?.profile_data?.colorPalette) {
|
|
11371
|
+
return {
|
|
11372
|
+
content: [
|
|
11373
|
+
{
|
|
11374
|
+
type: "text",
|
|
11375
|
+
text: "No brand color palette found. Extract a brand profile first."
|
|
11376
|
+
}
|
|
11377
|
+
],
|
|
11378
|
+
isError: true
|
|
11379
|
+
};
|
|
11380
|
+
}
|
|
11381
|
+
const output = exportDesignTokens(
|
|
11382
|
+
row.profile_data.colorPalette,
|
|
11383
|
+
row.profile_data.typography,
|
|
11384
|
+
format
|
|
11385
|
+
);
|
|
11386
|
+
const envelope = asEnvelope21({ format, tokens: output });
|
|
10539
11387
|
return {
|
|
10540
11388
|
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
|
|
10541
11389
|
};
|
|
@@ -10543,6 +11391,389 @@ function registerBrandRuntimeTools(server) {
|
|
|
10543
11391
|
);
|
|
10544
11392
|
}
|
|
10545
11393
|
|
|
11394
|
+
// src/tools/carousel.ts
|
|
11395
|
+
import { z as z27 } from "zod";
|
|
11396
|
+
init_supabase();
|
|
11397
|
+
init_request_context();
|
|
11398
|
+
var MAX_CREDITS_PER_RUN2 = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
|
|
11399
|
+
var MAX_ASSETS_PER_RUN2 = Math.max(0, Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0));
|
|
11400
|
+
var _globalCreditsUsed2 = 0;
|
|
11401
|
+
var _globalAssetsGenerated2 = 0;
|
|
11402
|
+
function getCreditsUsed2() {
|
|
11403
|
+
const ctx = requestContext.getStore();
|
|
11404
|
+
return ctx ? ctx.creditsUsed : _globalCreditsUsed2;
|
|
11405
|
+
}
|
|
11406
|
+
function addCreditsUsed2(amount) {
|
|
11407
|
+
const ctx = requestContext.getStore();
|
|
11408
|
+
if (ctx) {
|
|
11409
|
+
ctx.creditsUsed += amount;
|
|
11410
|
+
} else {
|
|
11411
|
+
_globalCreditsUsed2 += amount;
|
|
11412
|
+
}
|
|
11413
|
+
}
|
|
11414
|
+
function getAssetsGenerated2() {
|
|
11415
|
+
const ctx = requestContext.getStore();
|
|
11416
|
+
return ctx ? ctx.assetsGenerated : _globalAssetsGenerated2;
|
|
11417
|
+
}
|
|
11418
|
+
function addAssetsGenerated2(count) {
|
|
11419
|
+
const ctx = requestContext.getStore();
|
|
11420
|
+
if (ctx) {
|
|
11421
|
+
ctx.assetsGenerated += count;
|
|
11422
|
+
} else {
|
|
11423
|
+
_globalAssetsGenerated2 += count;
|
|
11424
|
+
}
|
|
11425
|
+
}
|
|
11426
|
+
function checkCreditBudget2(estimatedCost) {
|
|
11427
|
+
if (MAX_CREDITS_PER_RUN2 <= 0) return { ok: true };
|
|
11428
|
+
const used = getCreditsUsed2();
|
|
11429
|
+
if (used + estimatedCost > MAX_CREDITS_PER_RUN2) {
|
|
11430
|
+
return {
|
|
11431
|
+
ok: false,
|
|
11432
|
+
message: `Credit budget exceeded: ${used} used + ${estimatedCost} estimated > ${MAX_CREDITS_PER_RUN2} limit. Use a smaller slide count or cheaper image model.`
|
|
11433
|
+
};
|
|
11434
|
+
}
|
|
11435
|
+
return { ok: true };
|
|
11436
|
+
}
|
|
11437
|
+
function checkAssetBudget2() {
|
|
11438
|
+
if (MAX_ASSETS_PER_RUN2 <= 0) return { ok: true };
|
|
11439
|
+
const gen = getAssetsGenerated2();
|
|
11440
|
+
if (gen >= MAX_ASSETS_PER_RUN2) {
|
|
11441
|
+
return {
|
|
11442
|
+
ok: false,
|
|
11443
|
+
message: `Asset limit reached: ${gen}/${MAX_ASSETS_PER_RUN2} assets generated this run.`
|
|
11444
|
+
};
|
|
11445
|
+
}
|
|
11446
|
+
return { ok: true };
|
|
11447
|
+
}
|
|
11448
|
+
var IMAGE_CREDIT_ESTIMATES2 = {
|
|
11449
|
+
midjourney: 20,
|
|
11450
|
+
"nano-banana": 15,
|
|
11451
|
+
"nano-banana-pro": 25,
|
|
11452
|
+
"flux-pro": 30,
|
|
11453
|
+
"flux-max": 50,
|
|
11454
|
+
"gpt4o-image": 40,
|
|
11455
|
+
imagen4: 35,
|
|
11456
|
+
"imagen4-fast": 25,
|
|
11457
|
+
seedream: 20
|
|
11458
|
+
};
|
|
11459
|
+
async function fetchBrandVisualContext(projectId) {
|
|
11460
|
+
const { data, error } = await callEdgeFunction("mcp-data", { action: "brand-profile", projectId });
|
|
11461
|
+
if (error || !data?.success || !data.profile?.profile_data) return null;
|
|
11462
|
+
const profile = data.profile.profile_data;
|
|
11463
|
+
const parts = [];
|
|
11464
|
+
const palette = profile.colorPalette;
|
|
11465
|
+
if (palette) {
|
|
11466
|
+
const colors = Object.entries(palette).filter(([, v]) => typeof v === "string" && v.startsWith("#")).map(([k, v]) => `${k}: ${v}`).slice(0, 5);
|
|
11467
|
+
if (colors.length > 0) {
|
|
11468
|
+
parts.push(`Brand color palette: ${colors.join(", ")}`);
|
|
11469
|
+
}
|
|
11470
|
+
}
|
|
11471
|
+
const logoUrl = profile.logoUrl;
|
|
11472
|
+
let logoDesc = null;
|
|
11473
|
+
if (logoUrl) {
|
|
11474
|
+
const brandName = profile.name || "brand";
|
|
11475
|
+
logoDesc = `Include a small "${brandName}" logo watermark in the bottom-right corner`;
|
|
11476
|
+
parts.push(logoDesc);
|
|
11477
|
+
}
|
|
11478
|
+
const voice = profile.voiceProfile;
|
|
11479
|
+
if (voice?.tone && Array.isArray(voice.tone) && voice.tone.length > 0) {
|
|
11480
|
+
parts.push(`Visual mood: ${voice.tone.slice(0, 3).join(", ")}`);
|
|
11481
|
+
}
|
|
11482
|
+
if (parts.length === 0) return null;
|
|
11483
|
+
return {
|
|
11484
|
+
stylePrefix: parts.join(". "),
|
|
11485
|
+
brandName: profile.name || null,
|
|
11486
|
+
logoDescription: logoDesc
|
|
11487
|
+
};
|
|
11488
|
+
}
|
|
11489
|
+
function registerCarouselTools(server) {
|
|
11490
|
+
server.tool(
|
|
11491
|
+
"create_carousel",
|
|
11492
|
+
"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).",
|
|
11493
|
+
{
|
|
11494
|
+
topic: z27.string().max(200).describe(
|
|
11495
|
+
'Carousel topic/hook \u2014 be specific. Example: "5 pricing mistakes that kill SaaS startups" beats "SaaS tips".'
|
|
11496
|
+
),
|
|
11497
|
+
image_model: z27.enum([
|
|
11498
|
+
"midjourney",
|
|
11499
|
+
"nano-banana",
|
|
11500
|
+
"nano-banana-pro",
|
|
11501
|
+
"flux-pro",
|
|
11502
|
+
"flux-max",
|
|
11503
|
+
"gpt4o-image",
|
|
11504
|
+
"imagen4",
|
|
11505
|
+
"imagen4-fast",
|
|
11506
|
+
"seedream"
|
|
11507
|
+
]).describe(
|
|
11508
|
+
"Image model for slide visuals. flux-pro for general purpose, imagen4 for photorealistic, midjourney for artistic."
|
|
11509
|
+
),
|
|
11510
|
+
template_id: z27.enum([
|
|
11511
|
+
"educational-series",
|
|
11512
|
+
"product-showcase",
|
|
11513
|
+
"story-arc",
|
|
11514
|
+
"before-after",
|
|
11515
|
+
"step-by-step",
|
|
11516
|
+
"quote-collection",
|
|
11517
|
+
"data-stats",
|
|
11518
|
+
"myth-vs-reality",
|
|
11519
|
+
"hormozi-authority"
|
|
11520
|
+
]).optional().describe("Carousel template. Default: hormozi-authority."),
|
|
11521
|
+
slide_count: z27.number().min(3).max(10).optional().describe("Number of slides (3-10). Default: 7."),
|
|
11522
|
+
aspect_ratio: z27.enum(["1:1", "4:5", "9:16"]).optional().describe("Aspect ratio for both carousel and images. Default: 1:1."),
|
|
11523
|
+
style: z27.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe("Visual style. Default: hormozi for hormozi-authority template."),
|
|
11524
|
+
image_style_suffix: z27.string().max(500).optional().describe(
|
|
11525
|
+
'Style suffix appended to every image prompt for visual consistency across slides. Example: "dark moody lighting, cinematic, 35mm film grain".'
|
|
11526
|
+
),
|
|
11527
|
+
project_id: z27.string().optional().describe("Project ID to associate the carousel with."),
|
|
11528
|
+
response_format: z27.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
11529
|
+
},
|
|
11530
|
+
async ({
|
|
11531
|
+
topic,
|
|
11532
|
+
image_model,
|
|
11533
|
+
template_id,
|
|
11534
|
+
slide_count,
|
|
11535
|
+
aspect_ratio,
|
|
11536
|
+
style,
|
|
11537
|
+
image_style_suffix,
|
|
11538
|
+
brand_id,
|
|
11539
|
+
project_id,
|
|
11540
|
+
response_format
|
|
11541
|
+
}) => {
|
|
11542
|
+
const format = response_format ?? "text";
|
|
11543
|
+
const startedAt = Date.now();
|
|
11544
|
+
const templateId = template_id ?? "hormozi-authority";
|
|
11545
|
+
const resolvedStyle = style ?? (templateId === "hormozi-authority" ? "hormozi" : "professional");
|
|
11546
|
+
const slideCount = slide_count ?? 7;
|
|
11547
|
+
const ratio = aspect_ratio ?? "1:1";
|
|
11548
|
+
let brandContext = null;
|
|
11549
|
+
const brandProjectId = brand_id || project_id || await getDefaultProjectId();
|
|
11550
|
+
if (brandProjectId) {
|
|
11551
|
+
brandContext = await fetchBrandVisualContext(brandProjectId);
|
|
11552
|
+
}
|
|
11553
|
+
const carouselTextCost = 10 + slideCount * 2;
|
|
11554
|
+
const perImageCost = IMAGE_CREDIT_ESTIMATES2[image_model] ?? 30;
|
|
11555
|
+
const totalEstimatedCost = carouselTextCost + slideCount * perImageCost;
|
|
11556
|
+
const budgetCheck = checkCreditBudget2(totalEstimatedCost);
|
|
11557
|
+
if (!budgetCheck.ok) {
|
|
11558
|
+
await logMcpToolInvocation({
|
|
11559
|
+
toolName: "create_carousel",
|
|
11560
|
+
status: "error",
|
|
11561
|
+
durationMs: Date.now() - startedAt,
|
|
11562
|
+
details: { error: budgetCheck.message, totalEstimatedCost }
|
|
11563
|
+
});
|
|
11564
|
+
return {
|
|
11565
|
+
content: [{ type: "text", text: budgetCheck.message }],
|
|
11566
|
+
isError: true
|
|
11567
|
+
};
|
|
11568
|
+
}
|
|
11569
|
+
const assetBudget = checkAssetBudget2();
|
|
11570
|
+
if (!assetBudget.ok) {
|
|
11571
|
+
await logMcpToolInvocation({
|
|
11572
|
+
toolName: "create_carousel",
|
|
11573
|
+
status: "error",
|
|
11574
|
+
durationMs: Date.now() - startedAt,
|
|
11575
|
+
details: { error: assetBudget.message }
|
|
11576
|
+
});
|
|
11577
|
+
return {
|
|
11578
|
+
content: [{ type: "text", text: assetBudget.message }],
|
|
11579
|
+
isError: true
|
|
11580
|
+
};
|
|
11581
|
+
}
|
|
11582
|
+
const userId = await getDefaultUserId();
|
|
11583
|
+
const rateLimit = checkRateLimit("posting", `create_carousel:${userId}`);
|
|
11584
|
+
if (!rateLimit.allowed) {
|
|
11585
|
+
await logMcpToolInvocation({
|
|
11586
|
+
toolName: "create_carousel",
|
|
11587
|
+
status: "rate_limited",
|
|
11588
|
+
durationMs: Date.now() - startedAt,
|
|
11589
|
+
details: { retryAfter: rateLimit.retryAfter }
|
|
11590
|
+
});
|
|
11591
|
+
return {
|
|
11592
|
+
content: [
|
|
11593
|
+
{
|
|
11594
|
+
type: "text",
|
|
11595
|
+
text: `Rate limit exceeded. Retry in ~${rateLimit.retryAfter}s.`
|
|
11596
|
+
}
|
|
11597
|
+
],
|
|
11598
|
+
isError: true
|
|
11599
|
+
};
|
|
11600
|
+
}
|
|
11601
|
+
const { data: carouselData, error: carouselError } = await callEdgeFunction(
|
|
11602
|
+
"generate-carousel",
|
|
11603
|
+
{
|
|
11604
|
+
topic,
|
|
11605
|
+
templateId,
|
|
11606
|
+
slideCount,
|
|
11607
|
+
aspectRatio: ratio,
|
|
11608
|
+
style: resolvedStyle,
|
|
11609
|
+
projectId: project_id
|
|
11610
|
+
},
|
|
11611
|
+
{ timeoutMs: 6e4 }
|
|
11612
|
+
);
|
|
11613
|
+
if (carouselError || !carouselData?.carousel) {
|
|
11614
|
+
const errMsg = carouselError ?? "No carousel data returned";
|
|
11615
|
+
await logMcpToolInvocation({
|
|
11616
|
+
toolName: "create_carousel",
|
|
11617
|
+
status: "error",
|
|
11618
|
+
durationMs: Date.now() - startedAt,
|
|
11619
|
+
details: { phase: "text_generation", error: errMsg }
|
|
11620
|
+
});
|
|
11621
|
+
return {
|
|
11622
|
+
content: [{ type: "text", text: `Carousel text generation failed: ${errMsg}` }],
|
|
11623
|
+
isError: true
|
|
11624
|
+
};
|
|
11625
|
+
}
|
|
11626
|
+
const carousel = carouselData.carousel;
|
|
11627
|
+
const textCredits = carousel.credits?.used ?? carouselTextCost;
|
|
11628
|
+
addCreditsUsed2(textCredits);
|
|
11629
|
+
const imageJobs = await Promise.all(
|
|
11630
|
+
carousel.slides.map(async (slide) => {
|
|
11631
|
+
const promptParts = [];
|
|
11632
|
+
if (brandContext) promptParts.push(brandContext.stylePrefix);
|
|
11633
|
+
if (slide.headline) promptParts.push(slide.headline);
|
|
11634
|
+
if (slide.body) promptParts.push(slide.body);
|
|
11635
|
+
if (promptParts.length === 0) promptParts.push(topic);
|
|
11636
|
+
if (image_style_suffix) promptParts.push(image_style_suffix);
|
|
11637
|
+
const imagePrompt = promptParts.join(". ");
|
|
11638
|
+
try {
|
|
11639
|
+
const { data, error } = await callEdgeFunction(
|
|
11640
|
+
"kie-image-generate",
|
|
11641
|
+
{
|
|
11642
|
+
prompt: imagePrompt,
|
|
11643
|
+
model: image_model,
|
|
11644
|
+
aspectRatio: ratio
|
|
11645
|
+
},
|
|
11646
|
+
{ timeoutMs: 3e4 }
|
|
11647
|
+
);
|
|
11648
|
+
if (error || !data?.taskId && !data?.asyncJobId) {
|
|
11649
|
+
return {
|
|
11650
|
+
slideNumber: slide.slideNumber,
|
|
11651
|
+
jobId: null,
|
|
11652
|
+
model: image_model,
|
|
11653
|
+
error: error ?? "No job ID returned"
|
|
11654
|
+
};
|
|
11655
|
+
}
|
|
11656
|
+
const jobId = data.asyncJobId ?? data.taskId ?? null;
|
|
11657
|
+
if (jobId) {
|
|
11658
|
+
addCreditsUsed2(perImageCost);
|
|
11659
|
+
addAssetsGenerated2(1);
|
|
11660
|
+
}
|
|
11661
|
+
return {
|
|
11662
|
+
slideNumber: slide.slideNumber,
|
|
11663
|
+
jobId,
|
|
11664
|
+
model: image_model,
|
|
11665
|
+
error: null
|
|
11666
|
+
};
|
|
11667
|
+
} catch (err) {
|
|
11668
|
+
return {
|
|
11669
|
+
slideNumber: slide.slideNumber,
|
|
11670
|
+
jobId: null,
|
|
11671
|
+
model: image_model,
|
|
11672
|
+
error: sanitizeError2(err)
|
|
11673
|
+
};
|
|
11674
|
+
}
|
|
11675
|
+
})
|
|
11676
|
+
);
|
|
11677
|
+
const successfulJobs = imageJobs.filter((j) => j.jobId !== null);
|
|
11678
|
+
const failedJobs = imageJobs.filter((j) => j.jobId === null);
|
|
11679
|
+
await logMcpToolInvocation({
|
|
11680
|
+
toolName: "create_carousel",
|
|
11681
|
+
status: failedJobs.length === imageJobs.length ? "error" : "success",
|
|
11682
|
+
durationMs: Date.now() - startedAt,
|
|
11683
|
+
details: {
|
|
11684
|
+
carouselId: carousel.id,
|
|
11685
|
+
templateId,
|
|
11686
|
+
slideCount: carousel.slides.length,
|
|
11687
|
+
imagesStarted: successfulJobs.length,
|
|
11688
|
+
imagesFailed: failedJobs.length,
|
|
11689
|
+
imageModel: image_model,
|
|
11690
|
+
creditsUsed: getCreditsUsed2()
|
|
11691
|
+
}
|
|
11692
|
+
});
|
|
11693
|
+
if (format === "json") {
|
|
11694
|
+
return {
|
|
11695
|
+
content: [
|
|
11696
|
+
{
|
|
11697
|
+
type: "text",
|
|
11698
|
+
text: JSON.stringify(
|
|
11699
|
+
{
|
|
11700
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
11701
|
+
data: {
|
|
11702
|
+
carouselId: carousel.id,
|
|
11703
|
+
templateId,
|
|
11704
|
+
style: resolvedStyle,
|
|
11705
|
+
slideCount: carousel.slides.length,
|
|
11706
|
+
slides: carousel.slides.map((s) => {
|
|
11707
|
+
const job = imageJobs.find((j) => j.slideNumber === s.slideNumber);
|
|
11708
|
+
return {
|
|
11709
|
+
...s,
|
|
11710
|
+
imageJobId: job?.jobId ?? null,
|
|
11711
|
+
imageError: job?.error ?? null
|
|
11712
|
+
};
|
|
11713
|
+
}),
|
|
11714
|
+
imageModel: image_model,
|
|
11715
|
+
brandApplied: brandContext ? {
|
|
11716
|
+
brandName: brandContext.brandName,
|
|
11717
|
+
hasLogo: !!brandContext.logoDescription,
|
|
11718
|
+
stylePrefix: brandContext.stylePrefix
|
|
11719
|
+
} : null,
|
|
11720
|
+
jobIds: successfulJobs.map((j) => j.jobId),
|
|
11721
|
+
failedSlides: failedJobs.map((j) => ({
|
|
11722
|
+
slideNumber: j.slideNumber,
|
|
11723
|
+
error: j.error
|
|
11724
|
+
})),
|
|
11725
|
+
credits: {
|
|
11726
|
+
textGeneration: textCredits,
|
|
11727
|
+
imagesEstimated: successfulJobs.length * perImageCost,
|
|
11728
|
+
totalEstimated: textCredits + successfulJobs.length * perImageCost
|
|
11729
|
+
}
|
|
11730
|
+
}
|
|
11731
|
+
},
|
|
11732
|
+
null,
|
|
11733
|
+
2
|
|
11734
|
+
)
|
|
11735
|
+
}
|
|
11736
|
+
]
|
|
11737
|
+
};
|
|
11738
|
+
}
|
|
11739
|
+
const lines = [
|
|
11740
|
+
`Carousel created: ${carousel.slides.length} slides + ${successfulJobs.length} image jobs started.`,
|
|
11741
|
+
` Carousel ID: ${carousel.id}`,
|
|
11742
|
+
` Template: ${templateId} | Style: ${resolvedStyle}`,
|
|
11743
|
+
` Image model: ${image_model}`,
|
|
11744
|
+
` Credits: ~${textCredits + successfulJobs.length * perImageCost} (${textCredits} text + ${successfulJobs.length * perImageCost} images)`
|
|
11745
|
+
];
|
|
11746
|
+
if (brandContext) {
|
|
11747
|
+
lines.push(
|
|
11748
|
+
` Brand: ${brandContext.brandName || "unnamed"}${brandContext.logoDescription ? " (logo overlay via prompt)" : ""}`
|
|
11749
|
+
);
|
|
11750
|
+
}
|
|
11751
|
+
lines.push("", "Slides:");
|
|
11752
|
+
for (const slide of carousel.slides) {
|
|
11753
|
+
const job = imageJobs.find((j) => j.slideNumber === slide.slideNumber);
|
|
11754
|
+
const status = job?.jobId ? `image: ${job.jobId}` : `image FAILED: ${job?.error}`;
|
|
11755
|
+
lines.push(` ${slide.slideNumber}. ${slide.headline || "(no headline)"} [${status}]`);
|
|
11756
|
+
}
|
|
11757
|
+
if (failedJobs.length > 0) {
|
|
11758
|
+
lines.push("");
|
|
11759
|
+
lines.push(
|
|
11760
|
+
`WARNING: ${failedJobs.length}/${imageJobs.length} image generations failed. Use generate_image manually for failed slides.`
|
|
11761
|
+
);
|
|
11762
|
+
}
|
|
11763
|
+
const jobIdList = successfulJobs.map((j) => j.jobId).join(", ");
|
|
11764
|
+
lines.push("");
|
|
11765
|
+
lines.push("Next steps:");
|
|
11766
|
+
lines.push(` 1. Poll each job: check_status with job_id for each of: ${jobIdList}`);
|
|
11767
|
+
lines.push(
|
|
11768
|
+
" 2. When all complete: schedule_post with job_ids=[...] and media_type=CAROUSEL_ALBUM"
|
|
11769
|
+
);
|
|
11770
|
+
return {
|
|
11771
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
11772
|
+
};
|
|
11773
|
+
}
|
|
11774
|
+
);
|
|
11775
|
+
}
|
|
11776
|
+
|
|
10546
11777
|
// src/lib/register-tools.ts
|
|
10547
11778
|
function applyScopeEnforcement(server, scopeResolver) {
|
|
10548
11779
|
const originalTool = server.tool.bind(server);
|
|
@@ -10637,6 +11868,7 @@ function registerAllTools(server, options) {
|
|
|
10637
11868
|
registerLoopSummaryTools(server);
|
|
10638
11869
|
registerUsageTools(server);
|
|
10639
11870
|
registerAutopilotTools(server);
|
|
11871
|
+
registerRecipeTools(server);
|
|
10640
11872
|
registerExtractionTools(server);
|
|
10641
11873
|
registerQualityTools(server);
|
|
10642
11874
|
registerPlanningTools(server);
|
|
@@ -10646,21 +11878,22 @@ function registerAllTools(server, options) {
|
|
|
10646
11878
|
registerSuggestTools(server);
|
|
10647
11879
|
registerDigestTools(server);
|
|
10648
11880
|
registerBrandRuntimeTools(server);
|
|
11881
|
+
registerCarouselTools(server);
|
|
10649
11882
|
applyAnnotations(server);
|
|
10650
11883
|
}
|
|
10651
11884
|
|
|
10652
11885
|
// src/prompts.ts
|
|
10653
|
-
import { z as
|
|
11886
|
+
import { z as z28 } from "zod";
|
|
10654
11887
|
function registerPrompts(server) {
|
|
10655
11888
|
server.prompt(
|
|
10656
11889
|
"create_weekly_content_plan",
|
|
10657
11890
|
"Generate a full week of social media content (7 days, multiple platforms). Returns a structured plan with topics, formats, and posting times.",
|
|
10658
11891
|
{
|
|
10659
|
-
niche:
|
|
10660
|
-
platforms:
|
|
11892
|
+
niche: z28.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
|
|
11893
|
+
platforms: z28.string().optional().describe(
|
|
10661
11894
|
'Comma-separated platforms to target (default: "YouTube, Instagram, TikTok, LinkedIn")'
|
|
10662
11895
|
),
|
|
10663
|
-
tone:
|
|
11896
|
+
tone: z28.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
|
|
10664
11897
|
},
|
|
10665
11898
|
({ niche, platforms, tone }) => {
|
|
10666
11899
|
const targetPlatforms = platforms || "YouTube, Instagram, TikTok, LinkedIn";
|
|
@@ -10702,8 +11935,8 @@ After building the plan, use \`save_content_plan\` to save it.`
|
|
|
10702
11935
|
"analyze_top_content",
|
|
10703
11936
|
"Analyze your best-performing posts to identify patterns and replicate success. Returns insights on hooks, formats, timing, and topics that resonate.",
|
|
10704
11937
|
{
|
|
10705
|
-
timeframe:
|
|
10706
|
-
platform:
|
|
11938
|
+
timeframe: z28.string().optional().describe('Analysis period (default: "30 days"). E.g., "7 days", "90 days"'),
|
|
11939
|
+
platform: z28.string().optional().describe('Filter to a specific platform (e.g., "youtube", "instagram")')
|
|
10707
11940
|
},
|
|
10708
11941
|
({ timeframe, platform: platform2 }) => {
|
|
10709
11942
|
const period = timeframe || "30 days";
|
|
@@ -10740,10 +11973,10 @@ Format as a clear, actionable performance report.`
|
|
|
10740
11973
|
"repurpose_content",
|
|
10741
11974
|
"Take one piece of content and transform it into 8-10 pieces across multiple platforms and formats.",
|
|
10742
11975
|
{
|
|
10743
|
-
source:
|
|
11976
|
+
source: z28.string().describe(
|
|
10744
11977
|
"The source content to repurpose \u2014 a URL, transcript, or the content text itself"
|
|
10745
11978
|
),
|
|
10746
|
-
target_platforms:
|
|
11979
|
+
target_platforms: z28.string().optional().describe(
|
|
10747
11980
|
'Comma-separated target platforms (default: "Twitter, LinkedIn, Instagram, TikTok, YouTube")'
|
|
10748
11981
|
)
|
|
10749
11982
|
},
|
|
@@ -10785,9 +12018,9 @@ For each piece, include the platform, format, character count, and suggested pos
|
|
|
10785
12018
|
"setup_brand_voice",
|
|
10786
12019
|
"Define or refine your brand voice profile so all generated content stays on-brand. Walks through tone, audience, values, and style.",
|
|
10787
12020
|
{
|
|
10788
|
-
brand_name:
|
|
10789
|
-
industry:
|
|
10790
|
-
website:
|
|
12021
|
+
brand_name: z28.string().describe("Your brand or business name"),
|
|
12022
|
+
industry: z28.string().optional().describe('Your industry or niche (e.g., "B2B SaaS", "fitness coaching")'),
|
|
12023
|
+
website: z28.string().optional().describe("Your website URL for context")
|
|
10791
12024
|
},
|
|
10792
12025
|
({ brand_name, industry, website }) => {
|
|
10793
12026
|
const industryContext = industry ? ` in the ${industry} space` : "";
|
|
@@ -11386,56 +12619,6 @@ function createOAuthProvider(options) {
|
|
|
11386
12619
|
|
|
11387
12620
|
// src/http.ts
|
|
11388
12621
|
init_posthog();
|
|
11389
|
-
|
|
11390
|
-
// src/lib/sanitize-error.ts
|
|
11391
|
-
var ERROR_PATTERNS = [
|
|
11392
|
-
// Postgres / PostgREST
|
|
11393
|
-
[/PGRST301|permission denied/i, "Access denied. Check your account permissions."],
|
|
11394
|
-
[/42P01|does not exist/i, "Service temporarily unavailable. Please try again."],
|
|
11395
|
-
[/23505|unique.*constraint|duplicate key/i, "A duplicate record already exists."],
|
|
11396
|
-
[/23503|foreign key/i, "Referenced record not found."],
|
|
11397
|
-
// Gemini / Google AI
|
|
11398
|
-
[/google.*api.*key|googleapis\.com.*40[13]/i, "Content generation failed. Please try again."],
|
|
11399
|
-
[
|
|
11400
|
-
/RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
|
|
11401
|
-
"AI service rate limit reached. Please wait and retry."
|
|
11402
|
-
],
|
|
11403
|
-
[
|
|
11404
|
-
/SAFETY|prompt.*blocked|content.*filter/i,
|
|
11405
|
-
"Content was blocked by the AI safety filter. Try rephrasing."
|
|
11406
|
-
],
|
|
11407
|
-
[/gemini.*error|generativelanguage/i, "Content generation failed. Please try again."],
|
|
11408
|
-
// Kie.ai
|
|
11409
|
-
[/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
|
|
11410
|
-
// Stripe
|
|
11411
|
-
[/stripe.*api|sk_live_|sk_test_/i, "Payment processing error. Please try again."],
|
|
11412
|
-
// Network / fetch
|
|
11413
|
-
[
|
|
11414
|
-
/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
|
|
11415
|
-
"External service unavailable. Please try again."
|
|
11416
|
-
],
|
|
11417
|
-
[/fetch failed|network error|abort.*timeout/i, "Network request failed. Please try again."],
|
|
11418
|
-
[/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
|
|
11419
|
-
// Supabase Edge Function internals
|
|
11420
|
-
[/FunctionsHttpError|non-2xx status/i, "Backend service error. Please try again."],
|
|
11421
|
-
[/JWT|token.*expired|token.*invalid/i, "Authentication expired. Please re-authenticate."],
|
|
11422
|
-
// Generic sensitive patterns (API keys, URLs with secrets)
|
|
11423
|
-
[/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i, "An internal error occurred. Please try again."]
|
|
11424
|
-
];
|
|
11425
|
-
function sanitizeError(error) {
|
|
11426
|
-
const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
11427
|
-
if (process.env.NODE_ENV !== "production") {
|
|
11428
|
-
console.error("[Error]", msg);
|
|
11429
|
-
}
|
|
11430
|
-
for (const [pattern, userMessage] of ERROR_PATTERNS) {
|
|
11431
|
-
if (pattern.test(msg)) {
|
|
11432
|
-
return userMessage;
|
|
11433
|
-
}
|
|
11434
|
-
}
|
|
11435
|
-
return "An unexpected error occurred. Please try again.";
|
|
11436
|
-
}
|
|
11437
|
-
|
|
11438
|
-
// src/http.ts
|
|
11439
12622
|
var PORT = parseInt(process.env.PORT ?? "8080", 10);
|
|
11440
12623
|
var SUPABASE_URL2 = process.env.SUPABASE_URL ?? "";
|
|
11441
12624
|
var SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY ?? "";
|
|
@@ -11903,7 +13086,7 @@ app.post("/mcp", authenticateRequest, async (req, res) => {
|
|
|
11903
13086
|
const rawMessage = err instanceof Error ? err.message : "Internal server error";
|
|
11904
13087
|
console.error(`[MCP HTTP] POST /mcp error: ${rawMessage}`);
|
|
11905
13088
|
if (!res.headersSent) {
|
|
11906
|
-
res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message:
|
|
13089
|
+
res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: sanitizeError2(err) } });
|
|
11907
13090
|
}
|
|
11908
13091
|
}
|
|
11909
13092
|
});
|
|
@@ -11945,7 +13128,7 @@ app.delete("/mcp", authenticateRequest, async (req, res) => {
|
|
|
11945
13128
|
app.use((err, _req, res, _next) => {
|
|
11946
13129
|
console.error("[MCP HTTP] Unhandled Express error:", err.stack || err.message || err);
|
|
11947
13130
|
if (!res.headersSent) {
|
|
11948
|
-
res.status(500).json({ error: "internal_error", error_description: err
|
|
13131
|
+
res.status(500).json({ error: "internal_error", error_description: sanitizeError2(err) });
|
|
11949
13132
|
}
|
|
11950
13133
|
});
|
|
11951
13134
|
var httpServer = app.listen(PORT, "0.0.0.0", () => {
|