@socialneuron/mcp-server 1.9.0 → 1.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/sn.js CHANGED
@@ -19,7 +19,7 @@ var MCP_VERSION;
19
19
  var init_version = __esm({
20
20
  "src/lib/version.ts"() {
21
21
  "use strict";
22
- MCP_VERSION = "1.9.0";
22
+ MCP_VERSION = "1.9.2";
23
23
  }
24
24
  });
25
25
 
@@ -3688,26 +3688,35 @@ var init_constants = __esm({
3688
3688
  constants_default = {
3689
3689
  ZERO_WIDTH_CHARS: "[\\u200B\\u200C\\u200D\\u2060\\uFEFF\\u{E0020}-\\u{E007F}]",
3690
3690
  INSTRUCTION_PHRASES: [
3691
- "(?:^|[^A-Za-z])ignore (?:all|previous|prior) instructions",
3692
- "(?:^|[^A-Za-z])disregard (?:the|all) (?:above|prior|previous)",
3693
- "(?:^|[^A-Za-z])forget (?:all|previous|prior|the above)",
3691
+ "(?:^|[^A-Za-z])ignore (?:all |any |the )*(?:previous |prior |preceding |earlier |above )*instructions",
3692
+ "(?:^|[^A-Za-z])disregard (?:all |any |the )*(?:previous |prior |preceding |earlier |above )*(?:instructions|prompts?|context)",
3693
+ "(?:^|[^A-Za-z])forget (?:all |any |the )*(?:previous |prior |preceding |earlier |above )*instructions",
3694
+ "(?:^|[^A-Za-z])forget (?:all |any |the )*(?:previous |prior |preceding |earlier |above )+(?:everything|context)",
3695
+ "(?:^|[^A-Za-z])forget (?:all |any |the )*(?:everything|context) (?:above|previously|prior|before|you (?:were|have been))",
3694
3696
  "(?:^|[^A-Za-z])you are now (?:a |an )",
3695
3697
  "(?:^|[^A-Za-z])system:\\s",
3696
3698
  "<\\|im_start\\|>",
3697
3699
  "<\\|im_end\\|>",
3698
3700
  "</?im_(?:start|end)>",
3699
3701
  "<\\|assistant\\|>",
3700
- "(?:^|[^A-Za-z])\\[INST\\]"
3702
+ "(?:^|[^A-Za-z])\\[INST\\]",
3703
+ "(?:^|[^A-Za-z])disregard (?:the|all) (?:above|prior|previous)",
3704
+ "(?:^|[^A-Za-z])forget (?:all|previous|prior|the above)"
3701
3705
  ],
3702
3706
  PII_PATTERNS: {
3703
3707
  email: "[\\w.+-]+@[\\w-]+\\.[\\w.-]+",
3704
3708
  phone_us: "(?<!\\d|-)(?:\\+?1[-\\s.])?\\(?\\d{3}\\)?[-\\s.]?\\d{3}[-\\s.]?\\d{4}(?!\\d|-)",
3705
3709
  phone_intl: "\\+\\d{1,3}[-\\s.]?\\d{1,4}[-\\s.]?\\d{1,4}[-\\s.]?\\d{4,}",
3706
3710
  jwt: "eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+",
3707
- api_key: "(?:sk|pk|snk|xoxb|ghp|github_pat)_[A-Za-z0-9_-]{16,}",
3711
+ api_key: "(?:sk|pk|snk|sno|xoxb|ghp|github_pat)_[A-Za-z0-9_-]{16,}",
3708
3712
  credit_card: "(?<!\\d|-)(?:\\d{4}[ -]?){3}\\d{4}(?!\\d|-)",
3709
3713
  ssn: "\\d{3}-\\d{2}-\\d{4}",
3710
- ip: "(?<![\\d.])(?:\\d{1,3}\\.){3}\\d{1,3}(?![\\d.])"
3714
+ ip: "(?<![\\d.])(?:\\d{1,3}\\.){3}\\d{1,3}(?![\\d.])",
3715
+ aws_access_key: "AKIA[0-9A-Z]{16}",
3716
+ google_api_key: "AIza[0-9A-Za-z_\\-]{35}",
3717
+ slack_token: "xox[abprs]-[A-Za-z0-9-]{10,}|xapp-[0-9]-[A-Za-z0-9-]{10,}",
3718
+ bearer_token: "Bearer\\s+[A-Za-z0-9._~+/=-]{20,}",
3719
+ private_key_block: "-----BEGIN [A-Z ]*PRIVATE KEY-----[\\s\\S]*?-----END [A-Z ]*PRIVATE KEY-----"
3711
3720
  },
3712
3721
  MAX_LENGTH: 1e4,
3713
3722
  MAX_OUTPUT_LENGTH: 1e6
@@ -3734,13 +3743,90 @@ function normalize(text) {
3734
3743
  out = out.replace(EXCESSIVE_WHITESPACE, " ");
3735
3744
  return out;
3736
3745
  }
3737
- var OVERRIDE_CHARS, HTML_COMMENT, EXCESSIVE_WHITESPACE;
3746
+ function skeleton(text) {
3747
+ if (typeof text !== "string") return "";
3748
+ return text.replace(CONFUSABLE_RE, (ch) => CONFUSABLES[ch] ?? ch);
3749
+ }
3750
+ var OVERRIDE_CHARS, HTML_COMMENT, EXCESSIVE_WHITESPACE, CONFUSABLES, CONFUSABLE_RE;
3738
3751
  var init_normalize = __esm({
3739
3752
  "src/lib/agent-harness/normalize.ts"() {
3740
3753
  "use strict";
3741
3754
  OVERRIDE_CHARS = /[‪-‮⁦-⁩]/g;
3742
3755
  HTML_COMMENT = /<!--[\s\S]*?-->/g;
3743
3756
  EXCESSIVE_WHITESPACE = /\s{2,}/g;
3757
+ CONFUSABLES = {
3758
+ // Cyrillic, lowercase
3759
+ \u0430: "a",
3760
+ \u0432: "b",
3761
+ \u0435: "e",
3762
+ \u0455: "s",
3763
+ \u0456: "i",
3764
+ \u0458: "j",
3765
+ \u043A: "k",
3766
+ \u043C: "m",
3767
+ \u043D: "h",
3768
+ \u043E: "o",
3769
+ \u0440: "p",
3770
+ \u0441: "c",
3771
+ \u0442: "t",
3772
+ \u0443: "y",
3773
+ \u0445: "x",
3774
+ "\u0501": "d",
3775
+ \u0451: "e",
3776
+ \u04BB: "h",
3777
+ \u0475: "v",
3778
+ "\u051B": "q",
3779
+ \u0461: "w",
3780
+ "\u04CF": "l",
3781
+ "\u051D": "w",
3782
+ // Cyrillic, uppercase
3783
+ \u0410: "A",
3784
+ \u0412: "B",
3785
+ \u0415: "E",
3786
+ \u0405: "S",
3787
+ \u0406: "I",
3788
+ \u0408: "J",
3789
+ \u041A: "K",
3790
+ \u041C: "M",
3791
+ \u041D: "H",
3792
+ \u041E: "O",
3793
+ \u0420: "P",
3794
+ \u0421: "C",
3795
+ \u0422: "T",
3796
+ \u0423: "Y",
3797
+ \u0425: "X",
3798
+ // Greek, lowercase
3799
+ \u03B1: "a",
3800
+ \u03B2: "b",
3801
+ \u03B5: "e",
3802
+ \u03B9: "i",
3803
+ \u03BA: "k",
3804
+ \u03BD: "v",
3805
+ \u03BF: "o",
3806
+ \u03C1: "p",
3807
+ \u03C4: "t",
3808
+ \u03C5: "u",
3809
+ \u03C7: "x",
3810
+ \u03F2: "c",
3811
+ \u03B3: "y",
3812
+ \u03B7: "n",
3813
+ // Greek, uppercase
3814
+ \u0391: "A",
3815
+ \u0392: "B",
3816
+ \u0395: "E",
3817
+ \u0396: "Z",
3818
+ \u0397: "H",
3819
+ \u0399: "I",
3820
+ \u039A: "K",
3821
+ \u039C: "M",
3822
+ \u039D: "N",
3823
+ \u039F: "O",
3824
+ \u03A1: "P",
3825
+ \u03A4: "T",
3826
+ \u03A5: "Y",
3827
+ \u03A7: "X"
3828
+ };
3829
+ CONFUSABLE_RE = new RegExp(`[${Object.keys(CONFUSABLES).join("")}]`, "g");
3744
3830
  }
3745
3831
  });
3746
3832
 
@@ -3832,6 +3918,14 @@ function scan(text, options) {
3832
3918
  flagged.add(ipNorm.pattern);
3833
3919
  risk = Math.max(risk, 0.9);
3834
3920
  }
3921
+ const skeletonText = skeleton(normalized);
3922
+ if (skeletonText !== normalized) {
3923
+ const ipSkel = detectInstructionPhrase(skeletonText);
3924
+ if (ipSkel.found) {
3925
+ flagged.add(ipSkel.pattern);
3926
+ risk = Math.max(risk, 0.9);
3927
+ }
3928
+ }
3835
3929
  const flaggedArr = Array.from(flagged);
3836
3930
  if ((options.mode === "block" || options.mode === "sanitize") && flaggedArr.length > 0) {
3837
3931
  return { passed: false, risk_score: risk, flagged_patterns: flaggedArr, pii_redacted: false };
@@ -4347,6 +4441,92 @@ var init_ideation = __esm({
4347
4441
  }
4348
4442
  });
4349
4443
 
4444
+ // src/lib/sanitize-error.ts
4445
+ function redactSensitiveIdentifiers(message) {
4446
+ return message.replace(EMAIL_PATTERN, "[redacted-email]").replace(UUID_PATTERN, "[redacted-id]");
4447
+ }
4448
+ function sanitizeError(error) {
4449
+ const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
4450
+ for (const [pattern, userMessage] of ERROR_PATTERNS) {
4451
+ if (pattern.test(msg)) {
4452
+ return userMessage;
4453
+ }
4454
+ }
4455
+ return "An unexpected error occurred. Please try again.";
4456
+ }
4457
+ var ERROR_PATTERNS, UUID_PATTERN, EMAIL_PATTERN;
4458
+ var init_sanitize_error = __esm({
4459
+ "src/lib/sanitize-error.ts"() {
4460
+ "use strict";
4461
+ ERROR_PATTERNS = [
4462
+ // Postgres / PostgREST
4463
+ [
4464
+ /PGRST301|permission denied/i,
4465
+ "Access denied. Check your account permissions."
4466
+ ],
4467
+ [
4468
+ /42P01|does not exist/i,
4469
+ "Service temporarily unavailable. Please try again."
4470
+ ],
4471
+ [
4472
+ /23505|unique.*constraint|duplicate key/i,
4473
+ "A duplicate record already exists."
4474
+ ],
4475
+ [/23503|foreign key/i, "Referenced record not found."],
4476
+ // Gemini / Google AI
4477
+ [
4478
+ /google.*api.*key|googleapis\.com.*40[13]/i,
4479
+ "Content generation failed. Please try again."
4480
+ ],
4481
+ [
4482
+ /RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
4483
+ "AI service rate limit reached. Please wait and retry."
4484
+ ],
4485
+ [
4486
+ /SAFETY|prompt.*blocked|content.*filter/i,
4487
+ "Content was blocked by the AI safety filter. Try rephrasing."
4488
+ ],
4489
+ [
4490
+ /gemini.*error|generativelanguage/i,
4491
+ "Content generation failed. Please try again."
4492
+ ],
4493
+ // Kie.ai
4494
+ [/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
4495
+ // Stripe
4496
+ [
4497
+ /stripe.*api|sk_live_|sk_test_/i,
4498
+ "Payment processing error. Please try again."
4499
+ ],
4500
+ // Network / fetch
4501
+ [
4502
+ /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
4503
+ "External service unavailable. Please try again."
4504
+ ],
4505
+ [
4506
+ /fetch failed|network error|abort.*timeout/i,
4507
+ "Network request failed. Please try again."
4508
+ ],
4509
+ [/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
4510
+ // Supabase Edge Function internals
4511
+ [
4512
+ /FunctionsHttpError|non-2xx status/i,
4513
+ "Backend service error. Please try again."
4514
+ ],
4515
+ [
4516
+ /JWT|token.*expired|token.*invalid/i,
4517
+ "Authentication expired. Please re-authenticate."
4518
+ ],
4519
+ // Generic sensitive patterns (API keys, URLs with secrets)
4520
+ [
4521
+ /[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i,
4522
+ "An internal error occurred. Please try again."
4523
+ ]
4524
+ ];
4525
+ UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
4526
+ EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
4527
+ }
4528
+ });
4529
+
4350
4530
  // src/lib/checkStatusShape.ts
4351
4531
  function buildCheckStatusPayload(job, liveStatus) {
4352
4532
  const status = liveStatus?.status ?? job.status;
@@ -4442,9 +4622,22 @@ function checkCreditBudget(estimatedCost) {
4442
4622
  }
4443
4623
  const used = getCreditsUsed();
4444
4624
  if (used + estimatedCost > MAX_CREDITS_PER_RUN) {
4625
+ const message = `Per-run credit cap reached \u2014 this is a local spend guard, not a server rate limit, so retrying will not help. The SOCIALNEURON_MAX_CREDITS_PER_RUN environment variable is set to ${MAX_CREDITS_PER_RUN} credits. This run has already spent ${used} credits and this call is estimated at ~${estimatedCost} more (${used} + ${estimatedCost} = ${used + estimatedCost} > ${MAX_CREDITS_PER_RUN}). To proceed, raise or unset SOCIALNEURON_MAX_CREDITS_PER_RUN.`;
4445
4626
  return {
4446
4627
  ok: false,
4447
- message: `Credit budget exceeded for this MCP run. Used=${used}, next~=${estimatedCost}, limit=${MAX_CREDITS_PER_RUN}.`
4628
+ message,
4629
+ error: toolError("billing_error", message, {
4630
+ recover_with: [
4631
+ "Raise SOCIALNEURON_MAX_CREDITS_PER_RUN to a higher credit ceiling.",
4632
+ "Unset SOCIALNEURON_MAX_CREDITS_PER_RUN to remove the per-run cap."
4633
+ ],
4634
+ details: {
4635
+ env_var: "SOCIALNEURON_MAX_CREDITS_PER_RUN",
4636
+ credits_used_this_run: used,
4637
+ estimated_call_cost: estimatedCost,
4638
+ max_credits_per_run: MAX_CREDITS_PER_RUN
4639
+ }
4640
+ })
4448
4641
  };
4449
4642
  }
4450
4643
  return { ok: true };
@@ -4467,8 +4660,15 @@ var init_budget = __esm({
4467
4660
  "src/lib/budget.ts"() {
4468
4661
  "use strict";
4469
4662
  init_request_context();
4470
- MAX_CREDITS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
4471
- MAX_ASSETS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0));
4663
+ init_tool_error();
4664
+ MAX_CREDITS_PER_RUN = Math.max(
4665
+ 0,
4666
+ Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0)
4667
+ );
4668
+ MAX_ASSETS_PER_RUN = Math.max(
4669
+ 0,
4670
+ Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0)
4671
+ );
4472
4672
  _globalCreditsUsed = 0;
4473
4673
  _globalAssetsGenerated = 0;
4474
4674
  }
@@ -4495,7 +4695,7 @@ function asEnvelope2(data) {
4495
4695
  function registerContentTools(server) {
4496
4696
  server.tool(
4497
4697
  "generate_video",
4498
- "Start an async AI video generation job \u2014 returns a job_id immediately. Poll with check_status every 10-30s until complete. Base credit costs (reference config; dynamic models scale with duration/audio/resolution): seedance-2-fast 264 (8s, best quality/credit) \xB7 kling-3 100 (5s no-audio) \xB7 grok-imagine 30 (6s, cheapest) \xB7 veo3-fast 65 (8s) \xB7 kling-3-pro 135 (5s no-audio) \xB7 seedance-2 328 (8s 720p) \xB7 veo3-quality 1000 (8s) \xB7 wan-2.6 105 (5s) \xB7 gemini-omni-video 126 (multi-reference composites) \xB7 hailuo-02-standard 180 / seedance-1.5-pro 150 / kling 170 (legacy/budget fallbacks). Costs are estimated pre-check and reconciled post-response from the actual charge. audio adds ~1.5x on kling-3/kling-3-pro and ~2.65x on kling (enable_audio defaults to FALSE). Check get_credit_balance first for expensive generations.",
4698
+ "Start an async AI video generation job \u2014 returns a job_id immediately. Poll with check_status every 10-30s until complete. Base credit costs (reference config; dynamic models scale with duration/audio/resolution): seedance-2-fast 264 (8s, best quality/credit) \xB7 kling-3 100 (5s no-audio) \xB7 grok-imagine 30 (6s, cheapest) \xB7 veo3-fast 65 (8s) \xB7 kling-3-pro 135 (5s no-audio) \xB7 seedance-2 328 (8s 720p) \xB7 veo3-quality 1000 (8s) \xB7 wan-2.6 105 (5s) \xB7 gemini-omni-video 126 (multi-reference composites) \xB7 hailuo-02-standard 180 / seedance-1.5-pro 150 / kling 170 (legacy/budget fallbacks). Costs are estimated pre-check and reconciled post-response from the actual charge. audio adds ~1.5x on kling-3/kling-3-pro and ~2.65x on kling; enable_audio defaults to FALSE EXCEPT the seedance-2 family (seedance-2-fast, seedance-2) where audio is ON by default and its cost is already included. Check get_credit_balance first for expensive generations.",
4499
4699
  {
4500
4700
  prompt: z2.string().max(2500).describe(
4501
4701
  'Video prompt \u2014 be specific about visual style, camera movement, lighting, and mood. Example: "Aerial drone shot of coastal cliffs at golden hour, slow dolly forward, cinematic 24fps, warm color grading." Vague prompts produce generic results.'
@@ -4510,7 +4710,7 @@ function registerContentTools(server) {
4510
4710
  "Video aspect ratio. 16:9 for YouTube/landscape, 9:16 for TikTok/Reels/Shorts, 1:1 for Instagram feed/square. Defaults to 16:9."
4511
4711
  ),
4512
4712
  enable_audio: z2.boolean().optional().describe(
4513
- "Enable native audio generation. DEFAULTS TO FALSE (cost control). Cost multiplier when true: kling 2.6 ~2.65x (17->45 cr/sec), kling-3 1.5x (20->30 cr/sec), kling-3-pro ~1.5x (27->40 cr/sec). seedance-2/-fast generate audio natively regardless. 5+ languages."
4713
+ "Enable native audio generation. For most models this DEFAULTS TO FALSE (cost control). EXCEPTION: the seedance-2 family (seedance-2-fast, seedance-2) DEFAULTS TO TRUE \u2014 these generate audio natively and their credit cost already includes it, so audio is on unless you explicitly pass false. Cost multiplier when true on other models: kling 2.6 ~2.65x (17->45 cr/sec), kling-3 1.5x (20->30 cr/sec), kling-3-pro ~1.5x (27->40 cr/sec). 5+ languages."
4514
4714
  ),
4515
4715
  image_url: z2.string().optional().describe(
4516
4716
  "Start frame image URL for image-to-video (Kling 3.0 frame control)."
@@ -4546,10 +4746,7 @@ function registerContentTools(server) {
4546
4746
  const estimatedCost = VIDEO_CREDIT_ESTIMATES[model] ?? 120;
4547
4747
  const budgetCheck = checkCreditBudget(estimatedCost);
4548
4748
  if (!budgetCheck.ok) {
4549
- return {
4550
- content: [{ type: "text", text: budgetCheck.message }],
4551
- isError: true
4552
- };
4749
+ return budgetCheck.error;
4553
4750
  }
4554
4751
  const rateLimit = checkRateLimit(
4555
4752
  "generation",
@@ -4573,9 +4770,12 @@ function registerContentTools(server) {
4573
4770
  model,
4574
4771
  duration: duration ?? 5,
4575
4772
  aspectRatio: aspect_ratio ?? "16:9",
4576
- // Default FALSE (2026-07-13): the old `?? true` default silently multiplied
4577
- // kling-family costs (2.65x on kling 2.6) for callers that never asked for audio.
4578
- enableAudio: enable_audio ?? false,
4773
+ // Default FALSE for most models (2026-07-13): the old `?? true` default silently
4774
+ // multiplied kling-family costs (2.65x on kling 2.6) for callers that never asked
4775
+ // for audio. Exception (2026-07-17): the seedance-2 family bills audio into its base
4776
+ // cost and generates it natively, so a false default there shipped silent no-audio
4777
+ // mp4s — those models default TRUE when the caller omits enable_audio.
4778
+ enableAudio: enable_audio ?? AUDIO_NATIVE_DEFAULT_MODELS.has(model),
4579
4779
  ...image_url && { imageUrl: image_url },
4580
4780
  ...end_frame_url && { endFrameUrl: end_frame_url },
4581
4781
  // The server reads projectId and enforces
@@ -4697,10 +4897,7 @@ function registerContentTools(server) {
4697
4897
  const estimatedCost = IMAGE_CREDIT_ESTIMATES[model] ?? 30;
4698
4898
  const budgetCheck = checkCreditBudget(estimatedCost);
4699
4899
  if (!budgetCheck.ok) {
4700
- return {
4701
- content: [{ type: "text", text: budgetCheck.message }],
4702
- isError: true
4703
- };
4900
+ return budgetCheck.error;
4704
4901
  }
4705
4902
  const rateLimit = checkRateLimit(
4706
4903
  "generation",
@@ -4945,7 +5142,7 @@ function registerContentTools(server) {
4945
5142
  );
4946
5143
  }
4947
5144
  if (job.error_message) {
4948
- lines.push(`Error: ${job.error_message}`);
5145
+ lines.push(`Error: ${redactSensitiveIdentifiers(job.error_message)}`);
4949
5146
  }
4950
5147
  const fallbackDisclosure = buildFallbackDisclosureLine(
4951
5148
  job.result_metadata
@@ -5099,10 +5296,7 @@ Return ONLY valid JSON in this exact format:
5099
5296
  const estimatedCost = 10;
5100
5297
  const budgetCheck = checkCreditBudget(estimatedCost);
5101
5298
  if (!budgetCheck.ok) {
5102
- return {
5103
- content: [{ type: "text", text: budgetCheck.message }],
5104
- isError: true
5105
- };
5299
+ return budgetCheck.error;
5106
5300
  }
5107
5301
  const { data, error } = await callEdgeFunction(
5108
5302
  "social-neuron-ai",
@@ -5189,10 +5383,7 @@ Return ONLY valid JSON in this exact format:
5189
5383
  const estimatedCost = 15;
5190
5384
  const budgetCheck = checkCreditBudget(estimatedCost);
5191
5385
  if (!budgetCheck.ok) {
5192
- return {
5193
- content: [{ type: "text", text: budgetCheck.message }],
5194
- isError: true
5195
- };
5386
+ return budgetCheck.error;
5196
5387
  }
5197
5388
  const rateLimit = checkRateLimit(
5198
5389
  "generation",
@@ -5355,10 +5546,7 @@ Return ONLY valid JSON in this exact format:
5355
5546
  const estimatedCost = 10 + slideCount * 2;
5356
5547
  const budgetCheck = checkCreditBudget(estimatedCost);
5357
5548
  if (!budgetCheck.ok) {
5358
- return {
5359
- content: [{ type: "text", text: budgetCheck.message }],
5360
- isError: true
5361
- };
5549
+ return budgetCheck.error;
5362
5550
  }
5363
5551
  const userId = await getDefaultUserId();
5364
5552
  const rateLimit = checkRateLimit(
@@ -5461,13 +5649,14 @@ Return ONLY valid JSON in this exact format:
5461
5649
  }
5462
5650
  );
5463
5651
  }
5464
- var VIDEO_CREDIT_ESTIMATES, VIDEO_MODEL_ENUM, IMAGE_CREDIT_ESTIMATES;
5652
+ var VIDEO_CREDIT_ESTIMATES, AUDIO_NATIVE_DEFAULT_MODELS, VIDEO_MODEL_ENUM, IMAGE_CREDIT_ESTIMATES;
5465
5653
  var init_content2 = __esm({
5466
5654
  "src/tools/content.ts"() {
5467
5655
  "use strict";
5468
5656
  init_edge_function();
5469
5657
  init_rate_limit();
5470
5658
  init_supabase();
5659
+ init_sanitize_error();
5471
5660
  init_version();
5472
5661
  init_checkStatusShape();
5473
5662
  init_budget();
@@ -5485,6 +5674,10 @@ var init_content2 = __esm({
5485
5674
  "seedance-1.5-pro": 150,
5486
5675
  kling: 170
5487
5676
  };
5677
+ AUDIO_NATIVE_DEFAULT_MODELS = /* @__PURE__ */ new Set([
5678
+ "seedance-2-fast",
5679
+ "seedance-2"
5680
+ ]);
5488
5681
  VIDEO_MODEL_ENUM = [
5489
5682
  "seedance-2-fast",
5490
5683
  "kling-3",
@@ -5513,62 +5706,24 @@ var init_content2 = __esm({
5513
5706
  }
5514
5707
  });
5515
5708
 
5516
- // src/lib/sanitize-error.ts
5517
- function sanitizeError(error) {
5518
- const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
5519
- for (const [pattern, userMessage] of ERROR_PATTERNS) {
5520
- if (pattern.test(msg)) {
5521
- return userMessage;
5522
- }
5523
- }
5524
- return "An unexpected error occurred. Please try again.";
5525
- }
5526
- var ERROR_PATTERNS;
5527
- var init_sanitize_error = __esm({
5528
- "src/lib/sanitize-error.ts"() {
5529
- "use strict";
5530
- ERROR_PATTERNS = [
5531
- // Postgres / PostgREST
5532
- [/PGRST301|permission denied/i, "Access denied. Check your account permissions."],
5533
- [/42P01|does not exist/i, "Service temporarily unavailable. Please try again."],
5534
- [/23505|unique.*constraint|duplicate key/i, "A duplicate record already exists."],
5535
- [/23503|foreign key/i, "Referenced record not found."],
5536
- // Gemini / Google AI
5537
- [/google.*api.*key|googleapis\.com.*40[13]/i, "Content generation failed. Please try again."],
5538
- [
5539
- /RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
5540
- "AI service rate limit reached. Please wait and retry."
5541
- ],
5542
- [
5543
- /SAFETY|prompt.*blocked|content.*filter/i,
5544
- "Content was blocked by the AI safety filter. Try rephrasing."
5545
- ],
5546
- [/gemini.*error|generativelanguage/i, "Content generation failed. Please try again."],
5547
- // Kie.ai
5548
- [/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
5549
- // Stripe
5550
- [/stripe.*api|sk_live_|sk_test_/i, "Payment processing error. Please try again."],
5551
- // Network / fetch
5552
- [
5553
- /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
5554
- "External service unavailable. Please try again."
5555
- ],
5556
- [/fetch failed|network error|abort.*timeout/i, "Network request failed. Please try again."],
5557
- [/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
5558
- // Supabase Edge Function internals
5559
- [/FunctionsHttpError|non-2xx status/i, "Backend service error. Please try again."],
5560
- [/JWT|token.*expired|token.*invalid/i, "Authentication expired. Please re-authenticate."],
5561
- // Generic sensitive patterns (API keys, URLs with secrets)
5562
- [/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i, "An internal error occurred. Please try again."]
5563
- ];
5564
- }
5565
- });
5566
-
5567
5709
  // src/lib/ssrf.ts
5568
5710
  import { promises as dnsPromises } from "node:dns";
5711
+ function expandIPv4Mapped(ip) {
5712
+ const dotted = /^::ffff:((?:\d{1,3}\.){3}\d{1,3})$/i.exec(ip);
5713
+ if (dotted) return dotted[1];
5714
+ const hex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i.exec(ip);
5715
+ if (hex) {
5716
+ const high = parseInt(hex[1], 16);
5717
+ const low = parseInt(hex[2], 16);
5718
+ return `${high >> 8 & 255}.${high & 255}.${low >> 8 & 255}.${low & 255}`;
5719
+ }
5720
+ return null;
5721
+ }
5569
5722
  function isBlockedIP(ip) {
5570
5723
  const normalized = ip.replace(/^\[/, "").replace(/\]$/, "");
5571
5724
  if (normalized.includes(":")) {
5725
+ const mapped = expandIPv4Mapped(normalized);
5726
+ if (mapped) return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(mapped));
5572
5727
  return BLOCKED_IPV6_PATTERNS.some((pattern) => pattern.test(normalized));
5573
5728
  }
5574
5729
  return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(normalized));
@@ -13397,6 +13552,14 @@ var init_plan_approvals = __esm({
13397
13552
 
13398
13553
  // src/tools/discovery.ts
13399
13554
  import { z as z23 } from "zod";
13555
+ function isHostedTransport() {
13556
+ return process.env.MCP_TRANSPORT === "http";
13557
+ }
13558
+ function isPubliclyDiscoverable(tool) {
13559
+ if (tool.internal || tool.hiddenFromPublicCount) return false;
13560
+ if (tool.localOnly && isHostedTransport()) return false;
13561
+ return true;
13562
+ }
13400
13563
  function toolKnowledgeDocument(tool) {
13401
13564
  const lines = [
13402
13565
  `Tool: ${tool.name}`,
@@ -13407,7 +13570,8 @@ function toolKnowledgeDocument(tool) {
13407
13570
  if (tool.task_intent) lines.push(`Task intent: ${tool.task_intent}`);
13408
13571
  if (tool.use_when) lines.push(`Use when: ${tool.use_when}`);
13409
13572
  if (tool.avoid_when) lines.push(`Avoid when: ${tool.avoid_when}`);
13410
- if (tool.next_tools?.length) lines.push(`Common next tools: ${tool.next_tools.join(", ")}`);
13573
+ if (tool.next_tools?.length)
13574
+ lines.push(`Common next tools: ${tool.next_tools.join(", ")}`);
13411
13575
  return {
13412
13576
  id: `tool:${tool.name}`,
13413
13577
  title: `MCP tool: ${tool.name}`,
@@ -13424,9 +13588,7 @@ function toolKnowledgeDocument(tool) {
13424
13588
  function getKnowledgeDocuments() {
13425
13589
  return [
13426
13590
  ...STATIC_KNOWLEDGE_DOCUMENTS,
13427
- ...TOOL_CATALOG.filter((t) => !t.internal && !t.hiddenFromPublicCount).map(
13428
- toolKnowledgeDocument
13429
- )
13591
+ ...TOOL_CATALOG.filter(isPubliclyDiscoverable).map(toolKnowledgeDocument)
13430
13592
  ];
13431
13593
  }
13432
13594
  function tokenize(input) {
@@ -13480,7 +13642,9 @@ function registerDiscoveryTools(server) {
13480
13642
  };
13481
13643
  return {
13482
13644
  structuredContent,
13483
- content: [{ type: "text", text: JSON.stringify(structuredContent) }]
13645
+ content: [
13646
+ { type: "text", text: JSON.stringify(structuredContent) }
13647
+ ]
13484
13648
  };
13485
13649
  }
13486
13650
  );
@@ -13501,10 +13665,14 @@ function registerDiscoveryTools(server) {
13501
13665
  }
13502
13666
  },
13503
13667
  async ({ id }) => {
13504
- const doc = getKnowledgeDocuments().find((candidate) => candidate.id === id);
13668
+ const doc = getKnowledgeDocuments().find(
13669
+ (candidate) => candidate.id === id
13670
+ );
13505
13671
  if (!doc) {
13506
13672
  return {
13507
- content: [{ type: "text", text: `Document not found: ${id}` }],
13673
+ content: [
13674
+ { type: "text", text: `Document not found: ${id}` }
13675
+ ],
13508
13676
  isError: true
13509
13677
  };
13510
13678
  }
@@ -13517,7 +13685,9 @@ function registerDiscoveryTools(server) {
13517
13685
  };
13518
13686
  return {
13519
13687
  structuredContent,
13520
- content: [{ type: "text", text: JSON.stringify(structuredContent) }]
13688
+ content: [
13689
+ { type: "text", text: JSON.stringify(structuredContent) }
13690
+ ]
13521
13691
  };
13522
13692
  }
13523
13693
  );
@@ -13526,7 +13696,9 @@ function registerDiscoveryTools(server) {
13526
13696
  '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.',
13527
13697
  {
13528
13698
  query: z23.string().optional().describe("Search query to filter tools by name or description"),
13529
- module: z23.string().optional().describe('Filter by module name (e.g. "planning", "content", "analytics")'),
13699
+ module: z23.string().optional().describe(
13700
+ 'Filter by module name (e.g. "planning", "content", "analytics")'
13701
+ ),
13530
13702
  scope: z23.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
13531
13703
  detail: z23.enum(["name", "summary", "full"]).default("summary").describe(
13532
13704
  'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
@@ -13543,14 +13715,18 @@ function registerDiscoveryTools(server) {
13543
13715
  if (query) {
13544
13716
  results = searchTools(query);
13545
13717
  }
13546
- results = results.filter((t) => !t.internal && !t.hiddenFromPublicCount);
13718
+ results = results.filter(isPubliclyDiscoverable);
13547
13719
  if (module) {
13548
13720
  const moduleTools = getToolsByModule(module);
13549
- results = results.filter((t) => moduleTools.some((mt) => mt.name === t.name));
13721
+ results = results.filter(
13722
+ (t) => moduleTools.some((mt) => mt.name === t.name)
13723
+ );
13550
13724
  }
13551
13725
  if (scope) {
13552
13726
  const scopeTools = getToolsByScope(scope);
13553
- results = results.filter((t) => scopeTools.some((st) => st.name === t.name));
13727
+ results = results.filter(
13728
+ (t) => scopeTools.some((st) => st.name === t.name)
13729
+ );
13554
13730
  }
13555
13731
  if (available_only) {
13556
13732
  results = results.filter(isAvailable);
@@ -13588,7 +13764,12 @@ function registerDiscoveryTools(server) {
13588
13764
  text: JSON.stringify(
13589
13765
  {
13590
13766
  toolCount: results.length,
13591
- ...hasKnownScopes ? { scopes: { available: currentScopes, unavailable_matches: unavailableCount } } : {},
13767
+ ...hasKnownScopes ? {
13768
+ scopes: {
13769
+ available: currentScopes,
13770
+ unavailable_matches: unavailableCount
13771
+ }
13772
+ } : {},
13592
13773
  tools: output
13593
13774
  },
13594
13775
  null,
@@ -16179,25 +16360,39 @@ function registerCarouselTools(server) {
16179
16360
  ]).optional().describe("Carousel template. Default: hormozi-authority."),
16180
16361
  slide_count: z28.number().min(3).max(10).optional().describe("Number of slides (3-10). Default: 7."),
16181
16362
  aspect_ratio: z28.enum(["1:1", "4:5", "9:16"]).optional().describe("Aspect ratio for both carousel and images. Default: 1:1."),
16182
- style: z28.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe("Visual style. Default: hormozi for hormozi-authority template."),
16363
+ style: z28.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
16364
+ "Visual style. Default: hormozi for hormozi-authority template."
16365
+ ),
16183
16366
  image_style_suffix: z28.string().max(500).optional().describe(
16184
16367
  'Style suffix appended to every image prompt for visual consistency across slides. Example: "dark moody lighting, cinematic, 35mm film grain".'
16185
16368
  ),
16186
16369
  hook: z28.string().max(300).optional().describe(
16187
16370
  "Explicit hook/opener for slide 1. Overrides any hook derived from topic. Keep under 15 words for strongest scroll-stop."
16188
16371
  ),
16189
- hook_family: z28.enum(["curiosity", "authority", "pain_point", "contrarian", "data_driven"]).optional().describe(
16372
+ hook_family: z28.enum([
16373
+ "curiosity",
16374
+ "authority",
16375
+ "pain_point",
16376
+ "contrarian",
16377
+ "data_driven"
16378
+ ]).optional().describe(
16190
16379
  "Hook family tag. Recorded on the carousel so downstream analytics can attribute engagement to hook pattern."
16191
16380
  ),
16192
- cta_text: z28.string().max(200).optional().describe("Explicit CTA copy for the final slide. If omitted, derived from topic."),
16193
- cta_url: z28.string().url().optional().describe("URL promoted on the CTA slide. Defaults to the project landing page."),
16381
+ cta_text: z28.string().max(200).optional().describe(
16382
+ "Explicit CTA copy for the final slide. If omitted, derived from topic."
16383
+ ),
16384
+ cta_url: z28.string().url().optional().describe(
16385
+ "URL promoted on the CTA slide. Defaults to the project landing page."
16386
+ ),
16194
16387
  tone: z28.string().max(200).optional().describe(
16195
16388
  'Voice/tone override. Composes with the brand profile voice when present. Example: "educational, confident, not arrogant".'
16196
16389
  ),
16197
16390
  constraints: z28.string().max(500).optional().describe(
16198
16391
  'Content constraints applied at generation time. Example: "No fabricated statistics. Sentence case only. No ALL CAPS."'
16199
16392
  ),
16200
- platform: z28.enum(["linkedin", "instagram", "tiktok", "x"]).optional().describe("Target platform. Affects tone conventions and slide-count guardrails."),
16393
+ platform: z28.enum(["linkedin", "instagram", "tiktok", "x"]).optional().describe(
16394
+ "Target platform. Affects tone conventions and slide-count guardrails."
16395
+ ),
16201
16396
  brand_id: z28.string().optional().describe(
16202
16397
  "Brand/project ID to pull visual context from (colors, logo, mood). Falls back to project_id, then default project."
16203
16398
  ),
@@ -16229,7 +16424,9 @@ function registerCarouselTools(server) {
16229
16424
  const slideCount = slide_count ?? 7;
16230
16425
  const ratio = aspect_ratio ?? "1:1";
16231
16426
  let brandContext = null;
16232
- const brandProjectId = brand_id || project_id || await getDefaultProjectId();
16427
+ const defaultProjectId = await getDefaultProjectId();
16428
+ const brandProjectId = brand_id || project_id || defaultProjectId;
16429
+ const resolvedProjectId = project_id || defaultProjectId;
16233
16430
  if (brandProjectId) {
16234
16431
  brandContext = await fetchBrandVisualContext(brandProjectId);
16235
16432
  }
@@ -16238,10 +16435,7 @@ function registerCarouselTools(server) {
16238
16435
  const totalEstimatedCost = carouselTextCost + slideCount * perImageCost;
16239
16436
  const budgetCheck = checkCreditBudget(totalEstimatedCost);
16240
16437
  if (!budgetCheck.ok) {
16241
- return {
16242
- content: [{ type: "text", text: budgetCheck.message }],
16243
- isError: true
16244
- };
16438
+ return budgetCheck.error;
16245
16439
  }
16246
16440
  const assetBudget = checkAssetBudget(slideCount);
16247
16441
  if (!assetBudget.ok) {
@@ -16251,7 +16445,10 @@ function registerCarouselTools(server) {
16251
16445
  };
16252
16446
  }
16253
16447
  const userId = await getDefaultUserId();
16254
- const rateLimit = checkRateLimit("generation", `create_carousel:${userId}`);
16448
+ const rateLimit = checkRateLimit(
16449
+ "generation",
16450
+ `create_carousel:${userId}`
16451
+ );
16255
16452
  if (!rateLimit.allowed) {
16256
16453
  return {
16257
16454
  content: [
@@ -16271,7 +16468,7 @@ function registerCarouselTools(server) {
16271
16468
  slideCount,
16272
16469
  aspectRatio: ratio,
16273
16470
  style: resolvedStyle,
16274
- projectId: project_id,
16471
+ projectId: resolvedProjectId,
16275
16472
  hook,
16276
16473
  hookFamily: hook_family,
16277
16474
  ctaText: cta_text,
@@ -16285,7 +16482,12 @@ function registerCarouselTools(server) {
16285
16482
  if (carouselError || !carouselData?.carousel) {
16286
16483
  const errMsg = carouselError ?? "No carousel data returned";
16287
16484
  return {
16288
- content: [{ type: "text", text: `Carousel text generation failed: ${errMsg}` }],
16485
+ content: [
16486
+ {
16487
+ type: "text",
16488
+ text: `Carousel text generation failed: ${errMsg}`
16489
+ }
16490
+ ],
16289
16491
  isError: true
16290
16492
  };
16291
16493
  }
@@ -16314,7 +16516,8 @@ function registerCarouselTools(server) {
16314
16516
  {
16315
16517
  prompt: imagePrompt,
16316
16518
  model: image_model,
16317
- aspectRatio: ratio
16519
+ aspectRatio: ratio,
16520
+ ...resolvedProjectId && { projectId: resolvedProjectId }
16318
16521
  },
16319
16522
  { timeoutMs: 3e4 }
16320
16523
  );
@@ -16371,14 +16574,19 @@ function registerCarouselTools(server) {
16371
16574
  type: "text",
16372
16575
  text: JSON.stringify(
16373
16576
  {
16374
- _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
16577
+ _meta: {
16578
+ version: MCP_VERSION,
16579
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
16580
+ },
16375
16581
  data: {
16376
16582
  carouselId: carousel.id,
16377
16583
  templateId,
16378
16584
  style: resolvedStyle,
16379
16585
  slideCount: carousel.slides.length,
16380
16586
  slides: carousel.slides.map((s) => {
16381
- const job = imageJobs.find((j) => j.slideNumber === s.slideNumber);
16587
+ const job = imageJobs.find(
16588
+ (j) => j.slideNumber === s.slideNumber
16589
+ );
16382
16590
  return {
16383
16591
  ...s,
16384
16592
  imageJobId: job?.jobId ?? null,
@@ -16405,9 +16613,17 @@ function registerCarouselTools(server) {
16405
16613
  credits: {
16406
16614
  textGeneration: textCredits,
16407
16615
  imagesEstimated: successfulJobs.length * perImageCost,
16408
- imagesCharged: imageJobs.reduce((sum, job) => sum + (job.creditsCharged ?? 0), 0),
16409
- imagesRefunded: imageJobs.reduce((sum, job) => sum + (job.creditsRefunded ?? 0), 0),
16410
- billingUnknownSlides: imageJobs.filter((job) => job.billingStatus === "unknown").length,
16616
+ imagesCharged: imageJobs.reduce(
16617
+ (sum, job) => sum + (job.creditsCharged ?? 0),
16618
+ 0
16619
+ ),
16620
+ imagesRefunded: imageJobs.reduce(
16621
+ (sum, job) => sum + (job.creditsRefunded ?? 0),
16622
+ 0
16623
+ ),
16624
+ billingUnknownSlides: imageJobs.filter(
16625
+ (job) => job.billingStatus === "unknown"
16626
+ ).length,
16411
16627
  totalEstimated: textCredits + successfulJobs.length * perImageCost
16412
16628
  }
16413
16629
  }
@@ -16435,7 +16651,9 @@ function registerCarouselTools(server) {
16435
16651
  for (const slide of carousel.slides) {
16436
16652
  const job = imageJobs.find((j) => j.slideNumber === slide.slideNumber);
16437
16653
  const status = job?.jobId ? `image: ${job.jobId}` : `image FAILED: ${job?.error}`;
16438
- lines.push(` ${slide.slideNumber}. ${slide.headline || "(no headline)"} [${status}]`);
16654
+ lines.push(
16655
+ ` ${slide.slideNumber}. ${slide.headline || "(no headline)"} [${status}]`
16656
+ );
16439
16657
  }
16440
16658
  if (failedJobs.length > 0) {
16441
16659
  lines.push("");
@@ -16446,7 +16664,9 @@ function registerCarouselTools(server) {
16446
16664
  const jobIdList = successfulJobs.map((j) => j.jobId).join(", ");
16447
16665
  lines.push("");
16448
16666
  lines.push("Next steps:");
16449
- lines.push(` 1. Poll each job: check_status with job_id for each of: ${jobIdList}`);
16667
+ lines.push(
16668
+ ` 1. Poll each job: check_status with job_id for each of: ${jobIdList}`
16669
+ );
16450
16670
  lines.push(
16451
16671
  " 2. When all complete: schedule_post with job_ids=[...] and media_type=CAROUSEL_ALBUM"
16452
16672
  );