@socialneuron/mcp-server 1.7.1 → 1.7.3

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 (3) hide show
  1. package/dist/http.js +544 -123
  2. package/dist/index.js +549 -78
  3. package/package.json +1 -1
package/dist/http.js CHANGED
@@ -1375,7 +1375,7 @@ init_supabase();
1375
1375
  init_request_context();
1376
1376
 
1377
1377
  // src/lib/version.ts
1378
- var MCP_VERSION = "1.7.0";
1378
+ var MCP_VERSION = "1.7.2";
1379
1379
 
1380
1380
  // src/tools/content.ts
1381
1381
  var MAX_CREDITS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
@@ -1985,7 +1985,24 @@ function registerContentTools(server) {
1985
1985
  `Status: ${job.status}`
1986
1986
  ];
1987
1987
  if (job.result_url) {
1988
- lines.push(`Result URL: ${job.result_url}`);
1988
+ const isR2Key = !job.result_url.startsWith("http");
1989
+ if (isR2Key) {
1990
+ const segments = job.result_url.split("/");
1991
+ const filename = segments[segments.length - 1] || "media";
1992
+ lines.push(`Media ready: ${filename}`);
1993
+ lines.push(
1994
+ "(Pass job_id directly to schedule_post, or use get_media_url with job_id for a download link)"
1995
+ );
1996
+ } else {
1997
+ lines.push(`Result URL: ${job.result_url}`);
1998
+ }
1999
+ }
2000
+ const allUrls = job.result_metadata?.all_urls;
2001
+ if (allUrls && allUrls.length > 1) {
2002
+ lines.push(`Media files: ${allUrls.length} outputs available`);
2003
+ lines.push(
2004
+ "(Use job_id with schedule_post for carousel, or response_format=json for programmatic access)"
2005
+ );
1989
2006
  }
1990
2007
  if (job.error_message) {
1991
2008
  lines.push(`Error: ${job.error_message}`);
@@ -2002,8 +2019,13 @@ function registerContentTools(server) {
2002
2019
  details: { status: job.status, jobId: job.id }
2003
2020
  });
2004
2021
  if (format === "json") {
2022
+ const enriched = {
2023
+ ...job,
2024
+ r2_key: job.result_url && !job.result_url.startsWith("http") ? job.result_url : null,
2025
+ all_urls: allUrls ?? null
2026
+ };
2005
2027
  return {
2006
- content: [{ type: "text", text: JSON.stringify(asEnvelope(job), null, 2) }]
2028
+ content: [{ type: "text", text: JSON.stringify(asEnvelope(enriched), null, 2) }]
2007
2029
  };
2008
2030
  }
2009
2031
  return {
@@ -2485,6 +2507,56 @@ Return ONLY valid JSON in this exact format:
2485
2507
  // src/tools/distribution.ts
2486
2508
  import { z as z3 } from "zod";
2487
2509
  import { createHash as createHash2 } from "node:crypto";
2510
+
2511
+ // src/lib/sanitize-error.ts
2512
+ var ERROR_PATTERNS = [
2513
+ // Postgres / PostgREST
2514
+ [/PGRST301|permission denied/i, "Access denied. Check your account permissions."],
2515
+ [/42P01|does not exist/i, "Service temporarily unavailable. Please try again."],
2516
+ [/23505|unique.*constraint|duplicate key/i, "A duplicate record already exists."],
2517
+ [/23503|foreign key/i, "Referenced record not found."],
2518
+ // Gemini / Google AI
2519
+ [/google.*api.*key|googleapis\.com.*40[13]/i, "Content generation failed. Please try again."],
2520
+ [
2521
+ /RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
2522
+ "AI service rate limit reached. Please wait and retry."
2523
+ ],
2524
+ [
2525
+ /SAFETY|prompt.*blocked|content.*filter/i,
2526
+ "Content was blocked by the AI safety filter. Try rephrasing."
2527
+ ],
2528
+ [/gemini.*error|generativelanguage/i, "Content generation failed. Please try again."],
2529
+ // Kie.ai
2530
+ [/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
2531
+ // Stripe
2532
+ [/stripe.*api|sk_live_|sk_test_/i, "Payment processing error. Please try again."],
2533
+ // Network / fetch
2534
+ [
2535
+ /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
2536
+ "External service unavailable. Please try again."
2537
+ ],
2538
+ [/fetch failed|network error|abort.*timeout/i, "Network request failed. Please try again."],
2539
+ [/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
2540
+ // Supabase Edge Function internals
2541
+ [/FunctionsHttpError|non-2xx status/i, "Backend service error. Please try again."],
2542
+ [/JWT|token.*expired|token.*invalid/i, "Authentication expired. Please re-authenticate."],
2543
+ // Generic sensitive patterns (API keys, URLs with secrets)
2544
+ [/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i, "An internal error occurred. Please try again."]
2545
+ ];
2546
+ function sanitizeError2(error) {
2547
+ const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
2548
+ if (process.env.NODE_ENV !== "production") {
2549
+ console.error("[Error]", msg);
2550
+ }
2551
+ for (const [pattern, userMessage] of ERROR_PATTERNS) {
2552
+ if (pattern.test(msg)) {
2553
+ return userMessage;
2554
+ }
2555
+ }
2556
+ return "An unexpected error occurred. Please try again.";
2557
+ }
2558
+
2559
+ // src/tools/distribution.ts
2488
2560
  init_supabase();
2489
2561
 
2490
2562
  // src/lib/quality.ts
@@ -2610,6 +2682,22 @@ function evaluateQuality(input) {
2610
2682
  }
2611
2683
 
2612
2684
  // src/tools/distribution.ts
2685
+ function snakeToCamel(obj) {
2686
+ const result = {};
2687
+ for (const [key, value] of Object.entries(obj)) {
2688
+ const camelKey = key.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
2689
+ result[camelKey] = value;
2690
+ }
2691
+ return result;
2692
+ }
2693
+ function convertPlatformMetadata(meta) {
2694
+ if (!meta) return void 0;
2695
+ const converted = {};
2696
+ for (const [platform2, fields] of Object.entries(meta)) {
2697
+ converted[platform2] = snakeToCamel(fields);
2698
+ }
2699
+ return converted;
2700
+ }
2613
2701
  var PLATFORM_CASE_MAP = {
2614
2702
  youtube: "YouTube",
2615
2703
  tiktok: "TikTok",
@@ -2652,8 +2740,53 @@ function registerDistributionTools(server) {
2652
2740
  job_ids: z3.array(z3.string()).optional().describe(
2653
2741
  "Array of async job IDs for carousel posts. Each resolved to its R2 key. Alternative to media_urls/r2_keys."
2654
2742
  ),
2655
- platform_metadata: z3.record(z3.string(), z3.record(z3.string(), z3.unknown())).optional().describe(
2656
- 'Platform-specific metadata. Keys: tiktok (privacy_status), youtube (title, description, privacy), facebook (page_id), threads, bluesky. Example: {"tiktok":{"privacy_status":"PUBLIC_TO_ALL"}}'
2743
+ platform_metadata: z3.object({
2744
+ tiktok: z3.object({
2745
+ privacy_status: z3.enum([
2746
+ "PUBLIC_TO_EVERYONE",
2747
+ "MUTUAL_FOLLOW_FRIENDS",
2748
+ "FOLLOWER_OF_CREATOR",
2749
+ "SELF_ONLY"
2750
+ ]).optional().describe("Required unless useInbox=true. Who can view the video."),
2751
+ enable_duet: z3.boolean().optional(),
2752
+ enable_comment: z3.boolean().optional(),
2753
+ enable_stitch: z3.boolean().optional(),
2754
+ is_ai_generated: z3.boolean().optional(),
2755
+ brand_content: z3.boolean().optional(),
2756
+ brand_organic: z3.boolean().optional(),
2757
+ use_inbox: z3.boolean().optional().describe("Post to TikTok inbox/draft instead of direct publish.")
2758
+ }).optional(),
2759
+ youtube: z3.object({
2760
+ title: z3.string().optional().describe("Video title (required for YouTube)."),
2761
+ description: z3.string().optional(),
2762
+ privacy_status: z3.enum(["public", "unlisted", "private"]).optional(),
2763
+ category_id: z3.string().optional(),
2764
+ tags: z3.array(z3.string()).optional(),
2765
+ made_for_kids: z3.boolean().optional(),
2766
+ notify_subscribers: z3.boolean().optional()
2767
+ }).optional(),
2768
+ facebook: z3.object({
2769
+ page_id: z3.string().optional().describe("Facebook Page ID to post to."),
2770
+ audience: z3.string().optional()
2771
+ }).optional(),
2772
+ instagram: z3.object({
2773
+ location: z3.string().optional(),
2774
+ collaborators: z3.array(z3.string()).optional(),
2775
+ cover_timestamp: z3.number().optional(),
2776
+ share_to_feed: z3.boolean().optional(),
2777
+ first_comment: z3.string().optional(),
2778
+ is_ai_generated: z3.boolean().optional()
2779
+ }).optional(),
2780
+ threads: z3.object({}).passthrough().optional(),
2781
+ bluesky: z3.object({
2782
+ content_labels: z3.array(z3.string()).optional()
2783
+ }).optional(),
2784
+ linkedin: z3.object({
2785
+ article_url: z3.string().optional()
2786
+ }).optional(),
2787
+ twitter: z3.object({}).passthrough().optional()
2788
+ }).optional().describe(
2789
+ 'Platform-specific metadata. Example: {"tiktok":{"privacy_status":"PUBLIC_TO_EVERYONE"}, "youtube":{"title":"My Video"}}'
2657
2790
  ),
2658
2791
  media_type: z3.enum(["IMAGE", "VIDEO", "CAROUSEL_ALBUM"]).optional().describe(
2659
2792
  "Media type. Set to CAROUSEL_ALBUM with media_urls for Instagram carousels. Default: auto-detected from media_url."
@@ -2751,7 +2884,12 @@ function registerDistributionTools(server) {
2751
2884
  const signed = await signR2Key(r2_key);
2752
2885
  if (!signed) {
2753
2886
  return {
2754
- content: [{ type: "text", text: `Failed to sign R2 key: ${r2_key}` }],
2887
+ content: [
2888
+ {
2889
+ type: "text",
2890
+ text: `Failed to sign media key. Verify the key exists and you have access.`
2891
+ }
2892
+ ],
2755
2893
  isError: true
2756
2894
  };
2757
2895
  }
@@ -2779,7 +2917,7 @@ function registerDistributionTools(server) {
2779
2917
  content: [
2780
2918
  {
2781
2919
  type: "text",
2782
- text: `Failed to sign R2 key at index ${failIdx}: ${r2_keys[failIdx]}`
2920
+ text: `Failed to sign media key at index ${failIdx}. Verify the key exists and you have access.`
2783
2921
  }
2784
2922
  ],
2785
2923
  isError: true
@@ -2807,7 +2945,7 @@ function registerDistributionTools(server) {
2807
2945
  content: [
2808
2946
  {
2809
2947
  type: "text",
2810
- text: `Failed to resolve media: ${resolveErr instanceof Error ? resolveErr.message : String(resolveErr)}`
2948
+ text: `Failed to resolve media: ${sanitizeError2(resolveErr)}`
2811
2949
  }
2812
2950
  ],
2813
2951
  isError: true
@@ -2832,7 +2970,11 @@ Created with Social Neuron`;
2832
2970
  hashtags,
2833
2971
  scheduledAt: schedule_at,
2834
2972
  projectId: project_id,
2835
- ...platform_metadata ? { platformMetadata: platform_metadata } : {}
2973
+ ...platform_metadata ? {
2974
+ platformMetadata: convertPlatformMetadata(
2975
+ platform_metadata
2976
+ )
2977
+ } : {}
2836
2978
  },
2837
2979
  { timeoutMs: 3e4 }
2838
2980
  );
@@ -3168,7 +3310,7 @@ Created with Social Neuron`;
3168
3310
  return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
3169
3311
  } catch (err) {
3170
3312
  const durationMs = Date.now() - startedAt;
3171
- const message = err instanceof Error ? err.message : String(err);
3313
+ const message = sanitizeError2(err);
3172
3314
  logMcpToolInvocation({
3173
3315
  toolName: "find_next_slots",
3174
3316
  status: "error",
@@ -3688,7 +3830,7 @@ Created with Social Neuron`;
3688
3830
  };
3689
3831
  } catch (err) {
3690
3832
  const durationMs = Date.now() - startedAt;
3691
- const message = err instanceof Error ? err.message : String(err);
3833
+ const message = sanitizeError2(err);
3692
3834
  logMcpToolInvocation({
3693
3835
  toolName: "schedule_content_plan",
3694
3836
  status: "error",
@@ -3710,6 +3852,10 @@ import { readFile } from "node:fs/promises";
3710
3852
  import { basename, extname } from "node:path";
3711
3853
  init_supabase();
3712
3854
  var MAX_BASE64_SIZE = 10 * 1024 * 1024;
3855
+ function maskR2Key(key) {
3856
+ const segments = key.split("/");
3857
+ return segments.length >= 3 ? `\u2026/${segments.slice(-2).join("/")}` : key;
3858
+ }
3713
3859
  function inferContentType(filePath) {
3714
3860
  const ext = extname(filePath).toLowerCase();
3715
3861
  const map = {
@@ -3794,18 +3940,111 @@ function registerMediaTools(server) {
3794
3940
  isError: true
3795
3941
  };
3796
3942
  }
3943
+ const ct = content_type || inferContentType(source);
3797
3944
  if (fileBuffer.length > MAX_BASE64_SIZE) {
3945
+ const { data: putData, error: putError } = await callEdgeFunction(
3946
+ "get-signed-url",
3947
+ {
3948
+ operation: "put",
3949
+ contentType: ct,
3950
+ filename: basename(source),
3951
+ projectId: project_id
3952
+ },
3953
+ { timeoutMs: 1e4 }
3954
+ );
3955
+ if (putError || !putData?.signedUrl) {
3956
+ return {
3957
+ content: [
3958
+ {
3959
+ type: "text",
3960
+ text: `Failed to get presigned upload URL: ${putError || "No URL returned"}`
3961
+ }
3962
+ ],
3963
+ isError: true
3964
+ };
3965
+ }
3966
+ try {
3967
+ const putResp = await fetch(putData.signedUrl, {
3968
+ method: "PUT",
3969
+ headers: { "Content-Type": ct },
3970
+ body: fileBuffer
3971
+ });
3972
+ if (!putResp.ok) {
3973
+ return {
3974
+ content: [
3975
+ {
3976
+ type: "text",
3977
+ text: `R2 upload failed (HTTP ${putResp.status}): ${await putResp.text().catch(() => "Unknown error")}`
3978
+ }
3979
+ ],
3980
+ isError: true
3981
+ };
3982
+ }
3983
+ } catch (uploadErr) {
3984
+ return {
3985
+ content: [
3986
+ {
3987
+ type: "text",
3988
+ text: `R2 upload failed: ${sanitizeError(uploadErr)}`
3989
+ }
3990
+ ],
3991
+ isError: true
3992
+ };
3993
+ }
3994
+ const { data: signData } = await callEdgeFunction(
3995
+ "get-signed-url",
3996
+ { key: putData.key, operation: "get" },
3997
+ { timeoutMs: 1e4 }
3998
+ );
3999
+ await logMcpToolInvocation({
4000
+ toolName: "upload_media",
4001
+ status: "success",
4002
+ durationMs: Date.now() - startedAt,
4003
+ details: {
4004
+ source: "local-presigned-put",
4005
+ r2Key: putData.key,
4006
+ size: fileBuffer.length,
4007
+ contentType: ct
4008
+ }
4009
+ });
4010
+ if (format === "json") {
4011
+ return {
4012
+ content: [
4013
+ {
4014
+ type: "text",
4015
+ text: JSON.stringify(
4016
+ {
4017
+ r2_key: putData.key,
4018
+ signed_url: signData?.signedUrl ?? null,
4019
+ size: fileBuffer.length,
4020
+ content_type: ct
4021
+ },
4022
+ null,
4023
+ 2
4024
+ )
4025
+ }
4026
+ ],
4027
+ isError: false
4028
+ };
4029
+ }
3798
4030
  return {
3799
4031
  content: [
3800
4032
  {
3801
4033
  type: "text",
3802
- text: `File too large for base64 upload (${(fileBuffer.length / 1024 / 1024).toFixed(1)}MB, max ${MAX_BASE64_SIZE / 1024 / 1024}MB). For large videos, host the file at a public URL first and pass the URL as source.`
4034
+ text: [
4035
+ "Media uploaded successfully (presigned PUT).",
4036
+ `Media key: ${maskR2Key(putData.key)}`,
4037
+ signData?.signedUrl ? `Signed URL: ${signData.signedUrl}` : "",
4038
+ `Size: ${(fileBuffer.length / 1024 / 1024).toFixed(1)}MB`,
4039
+ `Type: ${ct}`,
4040
+ "",
4041
+ "Use job_id or response_format=json with schedule_post to post to any platform."
4042
+ ].filter(Boolean).join("\n")
3803
4043
  }
3804
4044
  ],
3805
- isError: true
4045
+ isError: false
3806
4046
  };
3807
4047
  }
3808
- const ct = content_type || inferContentType(source);
3809
4048
  const base64 = `data:${ct};base64,${fileBuffer.toString("base64")}`;
3810
4049
  uploadBody = {
3811
4050
  fileData: base64,
@@ -3870,12 +4109,12 @@ function registerMediaTools(server) {
3870
4109
  type: "text",
3871
4110
  text: [
3872
4111
  "Media uploaded successfully.",
3873
- `R2 Key: ${data.key}`,
4112
+ `Media key: ${maskR2Key(data.key)}`,
3874
4113
  `Signed URL: ${data.url}`,
3875
4114
  `Size: ${(data.size / 1024).toFixed(0)}KB`,
3876
4115
  `Type: ${data.contentType}`,
3877
4116
  "",
3878
- "Use this r2_key with schedule_post to post to any platform."
4117
+ "Use job_id or response_format=json with schedule_post to post to any platform."
3879
4118
  ].join("\n")
3880
4119
  }
3881
4120
  ],
@@ -3939,7 +4178,7 @@ function registerMediaTools(server) {
3939
4178
  type: "text",
3940
4179
  text: [
3941
4180
  `Signed URL: ${data.signedUrl}`,
3942
- `R2 Key: ${r2_key}`,
4181
+ `Media key: ${maskR2Key(r2_key)}`,
3943
4182
  `Expires in: ${data.expiresIn ?? 3600}s`
3944
4183
  ].join("\n")
3945
4184
  }
@@ -4870,7 +5109,7 @@ function registerScreenshotTools(server) {
4870
5109
  };
4871
5110
  } catch (err) {
4872
5111
  await closeBrowser();
4873
- const message = err instanceof Error ? err.message : String(err);
5112
+ const message = sanitizeError2(err);
4874
5113
  await logMcpToolInvocation({
4875
5114
  toolName: "capture_app_page",
4876
5115
  status: "error",
@@ -5026,7 +5265,7 @@ function registerScreenshotTools(server) {
5026
5265
  };
5027
5266
  } catch (err) {
5028
5267
  await closeBrowser();
5029
- const message = err instanceof Error ? err.message : String(err);
5268
+ const message = sanitizeError2(err);
5030
5269
  await logMcpToolInvocation({
5031
5270
  toolName: "capture_screenshot",
5032
5271
  status: "error",
@@ -5319,7 +5558,7 @@ function registerRemotionTools(server) {
5319
5558
  ]
5320
5559
  };
5321
5560
  } catch (err) {
5322
- const message = err instanceof Error ? err.message : String(err);
5561
+ const message = sanitizeError2(err);
5323
5562
  await logMcpToolInvocation({
5324
5563
  toolName: "render_demo_video",
5325
5564
  status: "error",
@@ -5447,7 +5686,7 @@ function registerRemotionTools(server) {
5447
5686
  ]
5448
5687
  };
5449
5688
  } catch (err) {
5450
- const message = err instanceof Error ? err.message : String(err);
5689
+ const message = sanitizeError2(err);
5451
5690
  await logMcpToolInvocation({
5452
5691
  toolName: "render_template_video",
5453
5692
  status: "error",
@@ -7098,7 +7337,7 @@ function registerExtractionTools(server) {
7098
7337
  };
7099
7338
  } catch (err) {
7100
7339
  const durationMs = Date.now() - startedAt;
7101
- const message = err instanceof Error ? err.message : String(err);
7340
+ const message = sanitizeError2(err);
7102
7341
  logMcpToolInvocation({
7103
7342
  toolName: "extract_url_content",
7104
7343
  status: "error",
@@ -7654,7 +7893,7 @@ ${rawText.slice(0, 1e3)}`
7654
7893
  }
7655
7894
  } catch (persistErr) {
7656
7895
  const durationMs2 = Date.now() - startedAt;
7657
- const message = persistErr instanceof Error ? persistErr.message : String(persistErr);
7896
+ const message = sanitizeError2(persistErr);
7658
7897
  logMcpToolInvocation({
7659
7898
  toolName: "plan_content_week",
7660
7899
  status: "error",
@@ -7686,7 +7925,7 @@ ${rawText.slice(0, 1e3)}`
7686
7925
  };
7687
7926
  } catch (err) {
7688
7927
  const durationMs = Date.now() - startedAt;
7689
- const message = err instanceof Error ? err.message : String(err);
7928
+ const message = sanitizeError2(err);
7690
7929
  logMcpToolInvocation({
7691
7930
  toolName: "plan_content_week",
7692
7931
  status: "error",
@@ -7778,7 +8017,7 @@ ${rawText.slice(0, 1e3)}`
7778
8017
  };
7779
8018
  } catch (err) {
7780
8019
  const durationMs = Date.now() - startedAt;
7781
- const message = err instanceof Error ? err.message : String(err);
8020
+ const message = sanitizeError2(err);
7782
8021
  logMcpToolInvocation({
7783
8022
  toolName: "save_content_plan",
7784
8023
  status: "error",
@@ -8823,7 +9062,7 @@ function registerPipelineTools(server) {
8823
9062
  return { content: [{ type: "text", text: lines.join("\n") }] };
8824
9063
  } catch (err) {
8825
9064
  const durationMs = Date.now() - startedAt;
8826
- const message = err instanceof Error ? err.message : String(err);
9065
+ const message = sanitizeError2(err);
8827
9066
  logMcpToolInvocation({
8828
9067
  toolName: "check_pipeline_readiness",
8829
9068
  status: "error",
@@ -8984,7 +9223,7 @@ function registerPipelineTools(server) {
8984
9223
  } catch (deductErr) {
8985
9224
  errors.push({
8986
9225
  stage: "planning",
8987
- message: `Credit deduction failed: ${deductErr instanceof Error ? deductErr.message : String(deductErr)}`
9226
+ message: `Credit deduction failed: ${sanitizeError2(deductErr)}`
8988
9227
  });
8989
9228
  }
8990
9229
  }
@@ -9155,7 +9394,7 @@ function registerPipelineTools(server) {
9155
9394
  } catch (schedErr) {
9156
9395
  errors.push({
9157
9396
  stage: "schedule",
9158
- message: `Failed to schedule ${post.id}: ${schedErr instanceof Error ? schedErr.message : String(schedErr)}`
9397
+ message: `Failed to schedule ${post.id}: ${sanitizeError2(schedErr)}`
9159
9398
  });
9160
9399
  }
9161
9400
  }
@@ -9244,7 +9483,7 @@ function registerPipelineTools(server) {
9244
9483
  return { content: [{ type: "text", text: lines.join("\n") }] };
9245
9484
  } catch (err) {
9246
9485
  const durationMs = Date.now() - startedAt;
9247
- const message = err instanceof Error ? err.message : String(err);
9486
+ const message = sanitizeError2(err);
9248
9487
  logMcpToolInvocation({
9249
9488
  toolName: "run_content_pipeline",
9250
9489
  status: "error",
@@ -9468,7 +9707,7 @@ function registerPipelineTools(server) {
9468
9707
  return { content: [{ type: "text", text: lines.join("\n") }] };
9469
9708
  } catch (err) {
9470
9709
  const durationMs = Date.now() - startedAt;
9471
- const message = err instanceof Error ? err.message : String(err);
9710
+ const message = sanitizeError2(err);
9472
9711
  logMcpToolInvocation({
9473
9712
  toolName: "auto_approve_plan",
9474
9713
  status: "error",
@@ -9646,7 +9885,7 @@ ${i + 1}. ${s.topic}`);
9646
9885
  return { content: [{ type: "text", text: lines.join("\n") }] };
9647
9886
  } catch (err) {
9648
9887
  const durationMs = Date.now() - startedAt;
9649
- const message = err instanceof Error ? err.message : String(err);
9888
+ const message = sanitizeError2(err);
9650
9889
  logMcpToolInvocation({
9651
9890
  toolName: "suggest_next_content",
9652
9891
  status: "error",
@@ -9985,7 +10224,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
9985
10224
  return { content: [{ type: "text", text: lines.join("\n") }] };
9986
10225
  } catch (err) {
9987
10226
  const durationMs = Date.now() - startedAt;
9988
- const message = err instanceof Error ? err.message : String(err);
10227
+ const message = sanitizeError2(err);
9989
10228
  logMcpToolInvocation({
9990
10229
  toolName: "generate_performance_digest",
9991
10230
  status: "error",
@@ -10082,7 +10321,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
10082
10321
  return { content: [{ type: "text", text: lines.join("\n") }] };
10083
10322
  } catch (err) {
10084
10323
  const durationMs = Date.now() - startedAt;
10085
- const message = err instanceof Error ? err.message : String(err);
10324
+ const message = sanitizeError2(err);
10086
10325
  logMcpToolInvocation({
10087
10326
  toolName: "detect_anomalies",
10088
10327
  status: "error",
@@ -10101,6 +10340,275 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
10101
10340
  // src/tools/brandRuntime.ts
10102
10341
  import { z as z25 } from "zod";
10103
10342
  init_supabase();
10343
+
10344
+ // src/lib/brandScoring.ts
10345
+ var WEIGHTS = {
10346
+ toneAlignment: 0.3,
10347
+ vocabularyAdherence: 0.25,
10348
+ avoidCompliance: 0.2,
10349
+ audienceRelevance: 0.15,
10350
+ brandMentions: 0.05,
10351
+ structuralPatterns: 0.05
10352
+ };
10353
+ function norm(content) {
10354
+ return content.toLowerCase().replace(/[^a-z0-9\s]/g, " ");
10355
+ }
10356
+ function findMatches(content, terms) {
10357
+ const n = norm(content);
10358
+ return terms.filter((t) => n.includes(t.toLowerCase()));
10359
+ }
10360
+ function findMissing(content, terms) {
10361
+ const n = norm(content);
10362
+ return terms.filter((t) => !n.includes(t.toLowerCase()));
10363
+ }
10364
+ var FABRICATION_PATTERNS = [
10365
+ { regex: /\b\d+[,.]?\d*\s*(%|percent)/gi, label: "unverified percentage" },
10366
+ { regex: /\b(award[- ]?winning|best[- ]selling|#\s*1)\b/gi, label: "unverified ranking" },
10367
+ {
10368
+ regex: /\b(guaranteed|proven to|studies show|scientifically proven)\b/gi,
10369
+ label: "unverified claim"
10370
+ },
10371
+ {
10372
+ regex: /\b(always works|100% effective|risk[- ]?free|no risk)\b/gi,
10373
+ label: "absolute claim"
10374
+ }
10375
+ ];
10376
+ function detectFabricationPatterns(content) {
10377
+ const matches = [];
10378
+ for (const { regex, label } of FABRICATION_PATTERNS) {
10379
+ const re = new RegExp(regex.source, regex.flags);
10380
+ let m;
10381
+ while ((m = re.exec(content)) !== null) {
10382
+ matches.push({ label, match: m[0] });
10383
+ }
10384
+ }
10385
+ return matches;
10386
+ }
10387
+ function scoreTone(content, profile) {
10388
+ const terms = profile.voiceProfile?.tone || [];
10389
+ if (!terms.length)
10390
+ return {
10391
+ score: 50,
10392
+ weight: WEIGHTS.toneAlignment,
10393
+ issues: [],
10394
+ suggestions: ["Define brand tone words for better consistency measurement"]
10395
+ };
10396
+ const matched = findMatches(content, terms);
10397
+ const missing = findMissing(content, terms);
10398
+ const score = Math.min(100, Math.round(matched.length / terms.length * 100));
10399
+ const issues = [];
10400
+ const suggestions = [];
10401
+ if (missing.length > 0) {
10402
+ issues.push(`Missing tone signals: ${missing.join(", ")}`);
10403
+ suggestions.push(`Try incorporating tone words: ${missing.slice(0, 3).join(", ")}`);
10404
+ }
10405
+ return { score, weight: WEIGHTS.toneAlignment, issues, suggestions };
10406
+ }
10407
+ function scoreVocab(content, profile) {
10408
+ const preferred = [
10409
+ ...profile.voiceProfile?.languagePatterns || [],
10410
+ ...profile.vocabularyRules?.preferredTerms || []
10411
+ ];
10412
+ if (!preferred.length)
10413
+ return {
10414
+ score: 50,
10415
+ weight: WEIGHTS.vocabularyAdherence,
10416
+ issues: [],
10417
+ suggestions: ["Add preferred terms to improve vocabulary scoring"]
10418
+ };
10419
+ const matched = findMatches(content, preferred);
10420
+ const missing = findMissing(content, preferred);
10421
+ const score = Math.min(100, Math.round(matched.length / preferred.length * 100));
10422
+ const issues = [];
10423
+ const suggestions = [];
10424
+ if (missing.length > 0 && score < 60) {
10425
+ issues.push(`Low preferred term usage (${matched.length}/${preferred.length})`);
10426
+ suggestions.push(`Consider using: ${missing.slice(0, 3).join(", ")}`);
10427
+ }
10428
+ return { score, weight: WEIGHTS.vocabularyAdherence, issues, suggestions };
10429
+ }
10430
+ function scoreAvoid(content, profile) {
10431
+ const banned = [
10432
+ ...profile.voiceProfile?.avoidPatterns || [],
10433
+ ...profile.vocabularyRules?.bannedTerms || []
10434
+ ];
10435
+ if (!banned.length)
10436
+ return {
10437
+ score: 100,
10438
+ weight: WEIGHTS.avoidCompliance,
10439
+ issues: [],
10440
+ suggestions: []
10441
+ };
10442
+ const violations = findMatches(content, banned);
10443
+ const score = violations.length === 0 ? 100 : Math.max(0, 100 - violations.length * 25);
10444
+ const issues = [];
10445
+ const suggestions = [];
10446
+ if (violations.length > 0) {
10447
+ issues.push(`Banned/avoided terms found: ${violations.join(", ")}`);
10448
+ suggestions.push(`Remove or replace: ${violations.join(", ")}`);
10449
+ }
10450
+ return { score, weight: WEIGHTS.avoidCompliance, issues, suggestions };
10451
+ }
10452
+ function scoreAudience(content, profile) {
10453
+ const terms = [];
10454
+ const d = profile.targetAudience?.demographics;
10455
+ const p = profile.targetAudience?.psychographics;
10456
+ if (d?.ageRange) terms.push(d.ageRange);
10457
+ if (d?.location) terms.push(d.location);
10458
+ if (p?.interests) terms.push(...p.interests);
10459
+ if (p?.painPoints) terms.push(...p.painPoints);
10460
+ if (p?.aspirations) terms.push(...p.aspirations);
10461
+ const valid = terms.filter(Boolean);
10462
+ if (!valid.length)
10463
+ return {
10464
+ score: 50,
10465
+ weight: WEIGHTS.audienceRelevance,
10466
+ issues: [],
10467
+ suggestions: ["Define target audience details for relevance scoring"]
10468
+ };
10469
+ const matched = findMatches(content, valid);
10470
+ const score = Math.min(100, Math.round(matched.length / valid.length * 100));
10471
+ const issues = [];
10472
+ const suggestions = [];
10473
+ if (score < 40) {
10474
+ issues.push("Content has low audience relevance");
10475
+ suggestions.push(
10476
+ `Reference audience pain points or interests: ${valid.slice(0, 3).join(", ")}`
10477
+ );
10478
+ }
10479
+ return { score, weight: WEIGHTS.audienceRelevance, issues, suggestions };
10480
+ }
10481
+ function scoreBrand(content, profile) {
10482
+ const name = profile.name?.toLowerCase();
10483
+ if (!name)
10484
+ return {
10485
+ score: 50,
10486
+ weight: WEIGHTS.brandMentions,
10487
+ issues: [],
10488
+ suggestions: []
10489
+ };
10490
+ const mentioned = norm(content).includes(name);
10491
+ const issues = [];
10492
+ const suggestions = [];
10493
+ if (!mentioned) {
10494
+ issues.push("Brand name not mentioned");
10495
+ suggestions.push(`Include "${profile.name}" in the content`);
10496
+ }
10497
+ return {
10498
+ score: mentioned ? 100 : 0,
10499
+ weight: WEIGHTS.brandMentions,
10500
+ issues,
10501
+ suggestions
10502
+ };
10503
+ }
10504
+ function scoreStructure(content, profile) {
10505
+ const rules = profile.writingStyleRules;
10506
+ if (!rules)
10507
+ return {
10508
+ score: 50,
10509
+ weight: WEIGHTS.structuralPatterns,
10510
+ issues: [],
10511
+ suggestions: []
10512
+ };
10513
+ let score = 100;
10514
+ const issues = [];
10515
+ const suggestions = [];
10516
+ if (rules.perspective) {
10517
+ const markers = {
10518
+ "first-singular": [/\bI\b/g, /\bmy\b/gi],
10519
+ "first-plural": [/\bwe\b/gi, /\bour\b/gi],
10520
+ second: [/\byou\b/gi, /\byour\b/gi],
10521
+ third: [/\bthey\b/gi, /\btheir\b/gi]
10522
+ };
10523
+ const expected = markers[rules.perspective];
10524
+ if (expected && !expected.some((r) => r.test(content))) {
10525
+ score -= 30;
10526
+ issues.push(`Expected ${rules.perspective} perspective not detected`);
10527
+ suggestions.push(`Use ${rules.perspective} perspective pronouns`);
10528
+ }
10529
+ }
10530
+ if (rules.useContractions === false) {
10531
+ const found = content.match(
10532
+ /\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
10533
+ );
10534
+ if (found && found.length > 0) {
10535
+ score -= Math.min(40, found.length * 10);
10536
+ issues.push(`Contractions found (${found.length}): ${found.slice(0, 3).join(", ")}`);
10537
+ suggestions.push("Expand contractions to full forms");
10538
+ }
10539
+ }
10540
+ if (rules.emojiPolicy === "none") {
10541
+ const emojis = content.match(
10542
+ /[\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
10543
+ );
10544
+ if (emojis && emojis.length > 0) {
10545
+ score -= 20;
10546
+ issues.push('Emojis found but emoji policy is "none"');
10547
+ suggestions.push("Remove emojis from content");
10548
+ }
10549
+ }
10550
+ return {
10551
+ score: Math.max(0, score),
10552
+ weight: WEIGHTS.structuralPatterns,
10553
+ issues,
10554
+ suggestions
10555
+ };
10556
+ }
10557
+ function computeBrandConsistency(content, profile, threshold = 60) {
10558
+ if (!content || !profile) {
10559
+ const neutral = {
10560
+ score: 50,
10561
+ weight: 0,
10562
+ issues: [],
10563
+ suggestions: []
10564
+ };
10565
+ return {
10566
+ overall: 50,
10567
+ passed: false,
10568
+ dimensions: {
10569
+ toneAlignment: { ...neutral, weight: WEIGHTS.toneAlignment },
10570
+ vocabularyAdherence: { ...neutral, weight: WEIGHTS.vocabularyAdherence },
10571
+ avoidCompliance: { ...neutral, weight: WEIGHTS.avoidCompliance },
10572
+ audienceRelevance: { ...neutral, weight: WEIGHTS.audienceRelevance },
10573
+ brandMentions: { ...neutral, weight: WEIGHTS.brandMentions },
10574
+ structuralPatterns: { ...neutral, weight: WEIGHTS.structuralPatterns }
10575
+ },
10576
+ preferredTermsUsed: [],
10577
+ bannedTermsFound: [],
10578
+ fabricationWarnings: []
10579
+ };
10580
+ }
10581
+ const dimensions = {
10582
+ toneAlignment: scoreTone(content, profile),
10583
+ vocabularyAdherence: scoreVocab(content, profile),
10584
+ avoidCompliance: scoreAvoid(content, profile),
10585
+ audienceRelevance: scoreAudience(content, profile),
10586
+ brandMentions: scoreBrand(content, profile),
10587
+ structuralPatterns: scoreStructure(content, profile)
10588
+ };
10589
+ const overall = Math.round(
10590
+ Object.values(dimensions).reduce((sum, d) => sum + d.score * d.weight, 0)
10591
+ );
10592
+ const preferred = [
10593
+ ...profile.voiceProfile?.languagePatterns || [],
10594
+ ...profile.vocabularyRules?.preferredTerms || []
10595
+ ];
10596
+ const banned = [
10597
+ ...profile.voiceProfile?.avoidPatterns || [],
10598
+ ...profile.vocabularyRules?.bannedTerms || []
10599
+ ];
10600
+ const fabrications = detectFabricationPatterns(content);
10601
+ return {
10602
+ overall: Math.max(0, Math.min(100, overall)),
10603
+ passed: overall >= threshold,
10604
+ dimensions,
10605
+ preferredTermsUsed: findMatches(content, preferred),
10606
+ bannedTermsFound: findMatches(content, banned),
10607
+ fabricationWarnings: fabrications.map((f) => `${f.label}: "${f.match}"`)
10608
+ };
10609
+ }
10610
+
10611
+ // src/tools/brandRuntime.ts
10104
10612
  function asEnvelope20(data) {
10105
10613
  return {
10106
10614
  _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
@@ -10313,44 +10821,7 @@ function registerBrandRuntimeTools(server) {
10313
10821
  };
10314
10822
  }
10315
10823
  const profile = row.profile_data;
10316
- const contentLower = content.toLowerCase();
10317
- const issues = [];
10318
- let score = 70;
10319
- const banned = profile.vocabularyRules?.bannedTerms || [];
10320
- const bannedFound = banned.filter((t) => contentLower.includes(t.toLowerCase()));
10321
- if (bannedFound.length > 0) {
10322
- score -= bannedFound.length * 15;
10323
- issues.push(`Banned terms found: ${bannedFound.join(", ")}`);
10324
- }
10325
- const avoid = profile.voiceProfile?.avoidPatterns || [];
10326
- const avoidFound = avoid.filter((p) => contentLower.includes(p.toLowerCase()));
10327
- if (avoidFound.length > 0) {
10328
- score -= avoidFound.length * 10;
10329
- issues.push(`Avoid patterns found: ${avoidFound.join(", ")}`);
10330
- }
10331
- const preferred = profile.vocabularyRules?.preferredTerms || [];
10332
- const prefUsed = preferred.filter((t) => contentLower.includes(t.toLowerCase()));
10333
- score += Math.min(15, prefUsed.length * 5);
10334
- const fabPatterns = [
10335
- { regex: /\b\d+[,.]?\d*\s*(%|percent)/gi, label: "unverified percentage" },
10336
- { regex: /\b(award[- ]?winning|best[- ]selling|#\s*1)\b/gi, label: "unverified ranking" },
10337
- { regex: /\b(guaranteed|proven to|studies show)\b/gi, label: "unverified claim" }
10338
- ];
10339
- for (const { regex, label } of fabPatterns) {
10340
- regex.lastIndex = 0;
10341
- if (regex.test(content)) {
10342
- score -= 10;
10343
- issues.push(`Potential ${label} detected`);
10344
- }
10345
- }
10346
- score = Math.max(0, Math.min(100, score));
10347
- const checkResult = {
10348
- score,
10349
- passed: score >= 60,
10350
- issues,
10351
- preferredTermsUsed: prefUsed,
10352
- bannedTermsFound: bannedFound
10353
- };
10824
+ const checkResult = computeBrandConsistency(content, profile);
10354
10825
  const envelope = asEnvelope20(checkResult);
10355
10826
  return {
10356
10827
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
@@ -11202,56 +11673,6 @@ function createOAuthProvider(options) {
11202
11673
 
11203
11674
  // src/http.ts
11204
11675
  init_posthog();
11205
-
11206
- // src/lib/sanitize-error.ts
11207
- var ERROR_PATTERNS = [
11208
- // Postgres / PostgREST
11209
- [/PGRST301|permission denied/i, "Access denied. Check your account permissions."],
11210
- [/42P01|does not exist/i, "Service temporarily unavailable. Please try again."],
11211
- [/23505|unique.*constraint|duplicate key/i, "A duplicate record already exists."],
11212
- [/23503|foreign key/i, "Referenced record not found."],
11213
- // Gemini / Google AI
11214
- [/google.*api.*key|googleapis\.com.*40[13]/i, "Content generation failed. Please try again."],
11215
- [
11216
- /RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
11217
- "AI service rate limit reached. Please wait and retry."
11218
- ],
11219
- [
11220
- /SAFETY|prompt.*blocked|content.*filter/i,
11221
- "Content was blocked by the AI safety filter. Try rephrasing."
11222
- ],
11223
- [/gemini.*error|generativelanguage/i, "Content generation failed. Please try again."],
11224
- // Kie.ai
11225
- [/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
11226
- // Stripe
11227
- [/stripe.*api|sk_live_|sk_test_/i, "Payment processing error. Please try again."],
11228
- // Network / fetch
11229
- [
11230
- /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
11231
- "External service unavailable. Please try again."
11232
- ],
11233
- [/fetch failed|network error|abort.*timeout/i, "Network request failed. Please try again."],
11234
- [/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
11235
- // Supabase Edge Function internals
11236
- [/FunctionsHttpError|non-2xx status/i, "Backend service error. Please try again."],
11237
- [/JWT|token.*expired|token.*invalid/i, "Authentication expired. Please re-authenticate."],
11238
- // Generic sensitive patterns (API keys, URLs with secrets)
11239
- [/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i, "An internal error occurred. Please try again."]
11240
- ];
11241
- function sanitizeError(error) {
11242
- const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
11243
- if (process.env.NODE_ENV !== "production") {
11244
- console.error("[Error]", msg);
11245
- }
11246
- for (const [pattern, userMessage] of ERROR_PATTERNS) {
11247
- if (pattern.test(msg)) {
11248
- return userMessage;
11249
- }
11250
- }
11251
- return "An unexpected error occurred. Please try again.";
11252
- }
11253
-
11254
- // src/http.ts
11255
11676
  var PORT = parseInt(process.env.PORT ?? "8080", 10);
11256
11677
  var SUPABASE_URL2 = process.env.SUPABASE_URL ?? "";
11257
11678
  var SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY ?? "";
@@ -11719,7 +12140,7 @@ app.post("/mcp", authenticateRequest, async (req, res) => {
11719
12140
  const rawMessage = err instanceof Error ? err.message : "Internal server error";
11720
12141
  console.error(`[MCP HTTP] POST /mcp error: ${rawMessage}`);
11721
12142
  if (!res.headersSent) {
11722
- res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: sanitizeError(err) } });
12143
+ res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: sanitizeError2(err) } });
11723
12144
  }
11724
12145
  }
11725
12146
  });
@@ -11761,7 +12182,7 @@ app.delete("/mcp", authenticateRequest, async (req, res) => {
11761
12182
  app.use((err, _req, res, _next) => {
11762
12183
  console.error("[MCP HTTP] Unhandled Express error:", err.stack || err.message || err);
11763
12184
  if (!res.headersSent) {
11764
- res.status(500).json({ error: "internal_error", error_description: err.message });
12185
+ res.status(500).json({ error: "internal_error", error_description: sanitizeError2(err) });
11765
12186
  }
11766
12187
  });
11767
12188
  var httpServer = app.listen(PORT, "0.0.0.0", () => {