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