@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.
Files changed (5) hide show
  1. package/CHANGELOG.md +123 -17
  2. package/README.md +77 -19
  3. package/dist/http.js +1463 -280
  4. package/dist/index.js +1467 -230
  5. 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.0";
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: [{ type: "text", text: `Failed to sign R2 key: ${r2_key}` }],
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 R2 key at index ${failIdx}: ${r2_keys[failIdx]}`
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: ${resolveErr instanceof Error ? resolveErr.message : String(resolveErr)}`
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 = err instanceof Error ? err.message : String(err);
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 = err instanceof Error ? err.message : String(err);
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: ${uploadErr instanceof Error ? uploadErr.message : String(uploadErr)}`
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 = err instanceof Error ? err.message : String(err);
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 = err instanceof Error ? err.message : String(err);
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 = err instanceof Error ? err.message : String(err);
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 = err instanceof Error ? err.message : String(err);
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/extraction.ts
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: z17.string().url().describe("URL to extract content from"),
7136
- extract_type: z17.enum(["auto", "transcript", "article", "product"]).default("auto").describe("Type of extraction"),
7137
- include_comments: z17.boolean().default(false).describe("Include top comments (YouTube only)"),
7138
- max_results: z17.number().min(1).max(100).default(10).describe("Max comments to include"),
7139
- response_format: z17.enum(["text", "json"]).default("text")
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(asEnvelope13(extracted), null, 2) }
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 = err instanceof Error ? err.message : String(err);
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 z18 } from "zod";
7631
+ import { z as z19 } from "zod";
7303
7632
  init_supabase();
7304
- function asEnvelope14(data) {
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: z18.string().describe(
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: z18.string().optional().describe("Post title (important for YouTube)"),
7316
- platforms: z18.array(
7317
- z18.enum([
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: z18.number().min(0).max(35).default(26).describe(
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: z18.string().optional().describe("Brand keyword for alignment check"),
7332
- brand_avoid_patterns: z18.array(z18.string()).optional(),
7333
- custom_banned_terms: z18.array(z18.string()).optional(),
7334
- response_format: z18.enum(["text", "json"]).default("text")
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(asEnvelope14(result), null, 2) }],
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: z18.object({
7394
- posts: z18.array(
7395
- z18.object({
7396
- id: z18.string(),
7397
- caption: z18.string(),
7398
- title: z18.string().optional(),
7399
- platform: z18.string()
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: z18.number().min(0).max(35).default(26).describe(
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: z18.enum(["text", "json"]).default("text")
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(asEnvelope14({ posts: postsWithQuality, summary }), null, 2)
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 z19 } from "zod";
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 asEnvelope15(data) {
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: z19.string().describe("Main topic or content theme"),
7571
- source_url: z19.string().optional().describe("URL to extract content from (YouTube, article)"),
7572
- platforms: z19.array(
7573
- z19.enum([
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: z19.number().min(1).max(5).default(1).describe("Posts per platform per day"),
7585
- days: z19.number().min(1).max(7).default(5).describe("Number of days to plan"),
7586
- start_date: z19.string().optional().describe("ISO date, defaults to tomorrow"),
7587
- brand_voice: z19.string().optional().describe("Override brand voice description"),
7588
- project_id: z19.string().optional().describe("Project ID for brand/insights context"),
7589
- response_format: z19.enum(["text", "json"]).default("json")
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 = persistErr instanceof Error ? persistErr.message : String(persistErr);
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(asEnvelope15(plan), null, 2) }],
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 = err instanceof Error ? err.message : String(err);
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: z19.object({
7892
- topic: z19.string(),
7893
- posts: z19.array(z19.record(z19.string(), z19.unknown()))
8220
+ plan: z20.object({
8221
+ topic: z20.string(),
8222
+ posts: z20.array(z20.record(z20.string(), z20.unknown()))
7894
8223
  }).passthrough(),
7895
- project_id: z19.string().uuid().optional(),
7896
- status: z19.enum(["draft", "in_review", "approved", "scheduled", "completed"]).default("draft"),
7897
- response_format: z19.enum(["text", "json"]).default("json")
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(asEnvelope15(result), null, 2) }],
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 = err instanceof Error ? err.message : String(err);
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: z19.string().uuid().describe("Persisted content plan ID"),
7984
- response_format: z19.enum(["text", "json"]).default("json")
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(asEnvelope15(payload), null, 2) }],
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: z19.string().uuid(),
8038
- post_updates: z19.array(
8039
- z19.object({
8040
- post_id: z19.string(),
8041
- caption: z19.string().optional(),
8042
- title: z19.string().optional(),
8043
- hashtags: z19.array(z19.string()).optional(),
8044
- hook: z19.string().optional(),
8045
- angle: z19.string().optional(),
8046
- visual_direction: z19.string().optional(),
8047
- media_url: z19.string().optional(),
8048
- schedule_at: z19.string().optional(),
8049
- platform: z19.string().optional(),
8050
- status: z19.enum(["approved", "rejected", "needs_edit"]).optional()
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: z19.enum(["text", "json"]).default("json")
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(asEnvelope15(payload), null, 2) }],
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: z19.string().uuid(),
8111
- response_format: z19.enum(["text", "json"]).default("json")
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(asEnvelope15(payload), null, 2) }],
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 z20 } from "zod";
8491
+ import { z as z21 } from "zod";
8163
8492
  init_supabase();
8164
- function asEnvelope16(data) {
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: z20.string().uuid().describe("Content plan ID"),
8179
- posts: z20.array(
8180
- z20.object({
8181
- id: z20.string(),
8182
- platform: z20.string().optional(),
8183
- caption: z20.string().optional(),
8184
- title: z20.string().optional(),
8185
- media_url: z20.string().optional(),
8186
- schedule_at: z20.string().optional()
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: z20.string().uuid().optional().describe("Project ID. Defaults to active project context."),
8190
- response_format: z20.enum(["text", "json"]).optional()
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(asEnvelope16(payload), null, 2) }],
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: z20.string().uuid().describe("Content plan ID"),
8259
- status: z20.enum(["pending", "approved", "rejected", "edited"]).optional(),
8260
- response_format: z20.enum(["text", "json"]).optional()
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(asEnvelope16(payload), null, 2) }],
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: z20.string().uuid().describe("Approval item ID"),
8319
- decision: z20.enum(["approved", "rejected", "edited"]),
8320
- edited_post: z20.record(z20.string(), z20.unknown()).optional(),
8321
- reason: z20.string().max(1e3).optional(),
8322
- response_format: z20.enum(["text", "json"]).optional()
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(asEnvelope16(data), null, 2) }],
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 z21 } from "zod";
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: z21.string().optional().describe("Search query to filter tools by name or description"),
8833
- module: z21.string().optional().describe('Filter by module name (e.g. "planning", "content", "analytics")'),
8834
- scope: z21.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
8835
- detail: z21.enum(["name", "summary", "full"]).default("summary").describe(
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 z22 } from "zod";
9252
+ import { z as z23 } from "zod";
8879
9253
  import { randomUUID as randomUUID3 } from "node:crypto";
8880
9254
  init_supabase();
8881
- function asEnvelope17(data) {
9255
+ function asEnvelope18(data) {
8882
9256
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
8883
9257
  }
8884
- var PLATFORM_ENUM2 = z22.enum([
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: z22.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
8902
- platforms: z22.array(PLATFORM_ENUM2).min(1).describe("Target platforms to check"),
8903
- estimated_posts: z22.number().min(1).max(50).default(5).describe("Estimated posts to generate"),
8904
- response_format: z22.enum(["text", "json"]).optional().describe("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(asEnvelope17(result), null, 2) }]
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 = err instanceof Error ? err.message : String(err);
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: z22.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
9029
- topic: z22.string().optional().describe("Content topic (required if no source_url)"),
9030
- source_url: z22.string().optional().describe("URL to extract content from"),
9031
- platforms: z22.array(PLATFORM_ENUM2).min(1).describe("Target platforms"),
9032
- days: z22.number().min(1).max(7).default(5).describe("Days to plan"),
9033
- posts_per_day: z22.number().min(1).max(3).default(1).describe("Posts per platform per day"),
9034
- approval_mode: z22.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe(
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: z22.number().min(0).max(35).default(28).describe(
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: z22.number().optional().describe("Credit budget cap"),
9041
- dry_run: z22.boolean().default(false).describe("If true, skip scheduling and return plan only"),
9042
- skip_stages: z22.array(z22.enum(["research", "quality", "schedule"])).optional().describe("Stages to skip"),
9043
- response_format: z22.enum(["text", "json"]).default("json")
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: ${deductErr instanceof Error ? deductErr.message : String(deductErr)}`
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}: ${schedErr instanceof Error ? schedErr.message : String(schedErr)}`
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(asEnvelope17(resultPayload), null, 2) }
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 = err instanceof Error ? err.message : String(err);
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: z22.string().uuid().optional().describe("Pipeline run ID (omit for latest)"),
9467
- response_format: z22.enum(["text", "json"]).optional()
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(asEnvelope17(data), null, 2) }]
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: z22.string().uuid().describe("Content plan ID"),
9528
- quality_threshold: z22.number().min(0).max(35).default(26).describe("Minimum quality score to auto-approve"),
9529
- response_format: z22.enum(["text", "json"]).default("json")
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(asEnvelope17(resultPayload), null, 2) }
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 = err instanceof Error ? err.message : String(err);
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 z23 } from "zod";
10063
+ import { z as z24 } from "zod";
9690
10064
  init_supabase();
9691
- function asEnvelope18(data) {
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: z23.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
9700
- count: z23.number().min(1).max(10).default(3).describe("Number of suggestions to return"),
9701
- response_format: z23.enum(["text", "json"]).optional()
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(asEnvelope18(resultPayload), null, 2) }
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 = err instanceof Error ? err.message : String(err);
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 z24 } from "zod";
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 asEnvelope19(data) {
10328
+ function asEnvelope20(data) {
9955
10329
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
9956
10330
  }
9957
- var PLATFORM_ENUM3 = z24.enum([
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: z24.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
9973
- period: z24.enum(["7d", "14d", "30d"]).default("7d").describe("Time period to analyze"),
9974
- include_recommendations: z24.boolean().default(true),
9975
- response_format: z24.enum(["text", "json"]).optional()
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(asEnvelope19(digest), null, 2) }]
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 = err instanceof Error ? err.message : String(err);
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: z24.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
10191
- days: z24.number().min(7).max(90).default(14).describe("Days to analyze"),
10192
- sensitivity: z24.enum(["low", "medium", "high"]).default("medium").describe("Detection sensitivity: low=50%+, medium=30%+, high=15%+ changes"),
10193
- platforms: z24.array(PLATFORM_ENUM3).optional().describe("Filter to specific platforms"),
10194
- response_format: z24.enum(["text", "json"]).optional()
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(asEnvelope19(resultPayload), null, 2) }
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 = err instanceof Error ? err.message : String(err);
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 z25 } from "zod";
10660
+ import { z as z26 } from "zod";
10287
10661
  init_supabase();
10288
- function asEnvelope20(data) {
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: z25.string().optional().describe("Project ID. Defaults to current project.")
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 = asEnvelope20(runtime);
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: z25.string().optional().describe("Project ID. Defaults to current project.")
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: z25.string().describe("The content text to check for brand consistency."),
10482
- project_id: z25.string().optional().describe("Project ID. Defaults to current project.")
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 contentLower = content.toLowerCase();
10501
- const issues = [];
10502
- let score = 70;
10503
- const banned = profile.vocabularyRules?.bannedTerms || [];
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
- const envelope = asEnvelope20(checkResult);
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 z26 } from "zod";
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: z26.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
10660
- platforms: z26.string().optional().describe(
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: z26.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
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: z26.string().optional().describe('Analysis period (default: "30 days"). E.g., "7 days", "90 days"'),
10706
- platform: z26.string().optional().describe('Filter to a specific platform (e.g., "youtube", "instagram")')
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: z26.string().describe(
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: z26.string().optional().describe(
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: z26.string().describe("Your brand or business name"),
10789
- industry: z26.string().optional().describe('Your industry or niche (e.g., "B2B SaaS", "fitness coaching")'),
10790
- website: z26.string().optional().describe("Your website URL for context")
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: sanitizeError(err) } });
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.message });
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", () => {