@socialneuron/mcp-server 1.3.2 → 1.4.1

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/index.js CHANGED
@@ -14,22 +14,30 @@ var MCP_VERSION;
14
14
  var init_version = __esm({
15
15
  "src/lib/version.ts"() {
16
16
  "use strict";
17
- MCP_VERSION = "1.3.2";
17
+ MCP_VERSION = "1.4.1";
18
18
  }
19
19
  });
20
20
 
21
21
  // src/lib/posthog.ts
22
22
  import { createHash } from "node:crypto";
23
- import { PostHog } from "posthog-node";
24
23
  function hashUserId(userId) {
25
24
  return createHash("sha256").update(`${POSTHOG_SALT}:${userId}`).digest("hex").substring(0, 32);
26
25
  }
26
+ function isTelemetryOptedIn() {
27
+ if (process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true" || process.env.SOCIALNEURON_NO_TELEMETRY === "1") {
28
+ return false;
29
+ }
30
+ return process.env.SOCIALNEURON_TELEMETRY === "1";
31
+ }
27
32
  function initPostHog() {
28
- if (isTelemetryDisabled()) return;
33
+ if (!isTelemetryOptedIn()) return;
29
34
  const key = process.env.POSTHOG_KEY || process.env.VITE_POSTHOG_KEY;
30
35
  const host = process.env.POSTHOG_HOST || process.env.VITE_POSTHOG_HOST || "https://eu.i.posthog.com";
31
36
  if (!key) return;
32
- client = new PostHog(key, { host, flushAt: 5, flushInterval: 1e4 });
37
+ import("posthog-node").then(({ PostHog }) => {
38
+ client = new PostHog(key, { host, flushAt: 5, flushInterval: 1e4 });
39
+ }).catch(() => {
40
+ });
33
41
  }
34
42
  async function captureToolEvent(args) {
35
43
  if (!client) return;
@@ -875,7 +883,7 @@ var init_tool_catalog = __esm({
875
883
  name: "check_status",
876
884
  description: "Check status of async content generation job",
877
885
  module: "content",
878
- scope: "mcp:write"
886
+ scope: "mcp:read"
879
887
  },
880
888
  {
881
889
  name: "create_storyboard",
@@ -3738,7 +3746,7 @@ var TOOL_SCOPES = {
3738
3746
  adapt_content: "mcp:write",
3739
3747
  generate_video: "mcp:write",
3740
3748
  generate_image: "mcp:write",
3741
- check_status: "mcp:write",
3749
+ check_status: "mcp:read",
3742
3750
  render_demo_video: "mcp:write",
3743
3751
  save_brand_profile: "mcp:write",
3744
3752
  update_platform_voice: "mcp:write",
@@ -3792,6 +3800,77 @@ function hasScope(userScopes, required) {
3792
3800
  // src/tools/ideation.ts
3793
3801
  init_edge_function();
3794
3802
  import { z } from "zod";
3803
+
3804
+ // src/lib/rate-limit.ts
3805
+ var CATEGORY_CONFIGS = {
3806
+ posting: { maxTokens: 30, refillRate: 30 / 60 },
3807
+ // 30 req/min
3808
+ screenshot: { maxTokens: 10, refillRate: 10 / 60 },
3809
+ // 10 req/min
3810
+ read: { maxTokens: 60, refillRate: 60 / 60 }
3811
+ // 60 req/min
3812
+ };
3813
+ var RateLimiter = class {
3814
+ tokens;
3815
+ lastRefill;
3816
+ maxTokens;
3817
+ refillRate;
3818
+ // tokens per second
3819
+ constructor(config) {
3820
+ this.maxTokens = config.maxTokens;
3821
+ this.refillRate = config.refillRate;
3822
+ this.tokens = config.maxTokens;
3823
+ this.lastRefill = Date.now();
3824
+ }
3825
+ /**
3826
+ * Try to consume one token. Returns true if the request is allowed,
3827
+ * false if rate-limited.
3828
+ */
3829
+ consume() {
3830
+ this.refill();
3831
+ if (this.tokens >= 1) {
3832
+ this.tokens -= 1;
3833
+ return true;
3834
+ }
3835
+ return false;
3836
+ }
3837
+ /**
3838
+ * Seconds until at least one token is available.
3839
+ */
3840
+ retryAfter() {
3841
+ this.refill();
3842
+ if (this.tokens >= 1) return 0;
3843
+ return Math.ceil((1 - this.tokens) / this.refillRate);
3844
+ }
3845
+ refill() {
3846
+ const now = Date.now();
3847
+ const elapsed = (now - this.lastRefill) / 1e3;
3848
+ this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
3849
+ this.lastRefill = now;
3850
+ }
3851
+ };
3852
+ var limiters = /* @__PURE__ */ new Map();
3853
+ function getRateLimiter(category) {
3854
+ let limiter = limiters.get(category);
3855
+ if (!limiter) {
3856
+ const config = CATEGORY_CONFIGS[category] ?? CATEGORY_CONFIGS.read;
3857
+ limiter = new RateLimiter(config);
3858
+ limiters.set(category, limiter);
3859
+ }
3860
+ return limiter;
3861
+ }
3862
+ function checkRateLimit(category, key) {
3863
+ const bucketKey = key ? `${category}:${key}` : category;
3864
+ const limiter = getRateLimiter(bucketKey);
3865
+ const allowed = limiter.consume();
3866
+ return {
3867
+ allowed,
3868
+ retryAfter: allowed ? 0 : limiter.retryAfter()
3869
+ };
3870
+ }
3871
+
3872
+ // src/tools/ideation.ts
3873
+ init_supabase();
3795
3874
  function registerIdeationTools(server2) {
3796
3875
  server2.tool(
3797
3876
  "generate_content",
@@ -3812,7 +3891,9 @@ function registerIdeationTools(server2) {
3812
3891
  "facebook",
3813
3892
  "threads",
3814
3893
  "bluesky"
3815
- ]).optional().describe("Target social media platform. Helps tailor tone, length, and format."),
3894
+ ]).optional().describe(
3895
+ "Target social media platform. Helps tailor tone, length, and format."
3896
+ ),
3816
3897
  brand_voice: z.string().max(500).optional().describe(
3817
3898
  'Brand voice guidelines to follow (e.g. "professional and empathetic", "playful and Gen-Z"). Leave blank to use a neutral tone.'
3818
3899
  ),
@@ -3823,7 +3904,30 @@ function registerIdeationTools(server2) {
3823
3904
  "Project ID to auto-load brand profile and performance context for prompt enrichment."
3824
3905
  )
3825
3906
  },
3826
- async ({ prompt: prompt2, content_type, platform: platform3, brand_voice, model, project_id }) => {
3907
+ async ({
3908
+ prompt: prompt2,
3909
+ content_type,
3910
+ platform: platform3,
3911
+ brand_voice,
3912
+ model,
3913
+ project_id
3914
+ }) => {
3915
+ try {
3916
+ const userId = await getDefaultUserId();
3917
+ const rl = checkRateLimit("posting", userId);
3918
+ if (!rl.allowed) {
3919
+ return {
3920
+ content: [
3921
+ {
3922
+ type: "text",
3923
+ text: `Rate limited. Retry after ${rl.retryAfter}s.`
3924
+ }
3925
+ ],
3926
+ isError: true
3927
+ };
3928
+ }
3929
+ } catch {
3930
+ }
3827
3931
  let enrichedPrompt = prompt2;
3828
3932
  if (platform3) {
3829
3933
  enrichedPrompt += `
@@ -3955,8 +4059,12 @@ Content Type: ${content_type}`;
3955
4059
  category: z.string().optional().describe(
3956
4060
  "Category filter (for YouTube). Examples: general, entertainment, education, tech, music, gaming, sports, news."
3957
4061
  ),
3958
- niche: z.string().optional().describe("Niche keyword filter. Only return trends matching these keywords."),
3959
- url: z.string().optional().describe('Required when source is "rss" or "url". The feed or page URL to fetch.'),
4062
+ niche: z.string().optional().describe(
4063
+ "Niche keyword filter. Only return trends matching these keywords."
4064
+ ),
4065
+ url: z.string().optional().describe(
4066
+ 'Required when source is "rss" or "url". The feed or page URL to fetch.'
4067
+ ),
3960
4068
  force_refresh: z.boolean().optional().describe("Skip the server-side cache and fetch fresh data.")
3961
4069
  },
3962
4070
  async ({ source, category, niche, url, force_refresh }) => {
@@ -4031,7 +4139,9 @@ Content Type: ${content_type}`;
4031
4139
  "adapt_content",
4032
4140
  "Adapt existing content for a different social media platform. Rewrites content to match the target platform's norms including character limits, hashtag style, tone, and CTA conventions.",
4033
4141
  {
4034
- content: z.string().max(5e3).describe("The content to adapt. Can be a caption, script, blog excerpt, or any text."),
4142
+ content: z.string().max(5e3).describe(
4143
+ "The content to adapt. Can be a caption, script, blog excerpt, or any text."
4144
+ ),
4035
4145
  source_platform: z.enum([
4036
4146
  "youtube",
4037
4147
  "tiktok",
@@ -4041,7 +4151,9 @@ Content Type: ${content_type}`;
4041
4151
  "facebook",
4042
4152
  "threads",
4043
4153
  "bluesky"
4044
- ]).optional().describe("The platform the content was originally written for. Helps preserve intent."),
4154
+ ]).optional().describe(
4155
+ "The platform the content was originally written for. Helps preserve intent."
4156
+ ),
4045
4157
  target_platform: z.enum([
4046
4158
  "youtube",
4047
4159
  "tiktok",
@@ -4055,9 +4167,33 @@ Content Type: ${content_type}`;
4055
4167
  brand_voice: z.string().max(500).optional().describe(
4056
4168
  'Brand voice guidelines to maintain during adaptation (e.g. "professional", "playful").'
4057
4169
  ),
4058
- project_id: z.string().uuid().optional().describe("Optional project ID to load platform voice overrides from brand profile.")
4170
+ project_id: z.string().uuid().optional().describe(
4171
+ "Optional project ID to load platform voice overrides from brand profile."
4172
+ )
4059
4173
  },
4060
- async ({ content, source_platform, target_platform, brand_voice, project_id }) => {
4174
+ async ({
4175
+ content,
4176
+ source_platform,
4177
+ target_platform,
4178
+ brand_voice,
4179
+ project_id
4180
+ }) => {
4181
+ try {
4182
+ const userId = await getDefaultUserId();
4183
+ const rl = checkRateLimit("posting", userId);
4184
+ if (!rl.allowed) {
4185
+ return {
4186
+ content: [
4187
+ {
4188
+ type: "text",
4189
+ text: `Rate limited. Retry after ${rl.retryAfter}s.`
4190
+ }
4191
+ ],
4192
+ isError: true
4193
+ };
4194
+ }
4195
+ } catch {
4196
+ }
4061
4197
  const platformGuidelines = {
4062
4198
  twitter: "Max 280 characters. Concise, punchy. 1-3 hashtags max. Thread-friendly.",
4063
4199
  threads: "Max 500 characters. Conversational, opinion-driven. Minimal hashtags.",
@@ -4140,76 +4276,6 @@ ${content}`,
4140
4276
  // src/tools/content.ts
4141
4277
  init_edge_function();
4142
4278
  import { z as z2 } from "zod";
4143
-
4144
- // src/lib/rate-limit.ts
4145
- var CATEGORY_CONFIGS = {
4146
- posting: { maxTokens: 30, refillRate: 30 / 60 },
4147
- // 30 req/min
4148
- screenshot: { maxTokens: 10, refillRate: 10 / 60 },
4149
- // 10 req/min
4150
- read: { maxTokens: 60, refillRate: 60 / 60 }
4151
- // 60 req/min
4152
- };
4153
- var RateLimiter = class {
4154
- tokens;
4155
- lastRefill;
4156
- maxTokens;
4157
- refillRate;
4158
- // tokens per second
4159
- constructor(config) {
4160
- this.maxTokens = config.maxTokens;
4161
- this.refillRate = config.refillRate;
4162
- this.tokens = config.maxTokens;
4163
- this.lastRefill = Date.now();
4164
- }
4165
- /**
4166
- * Try to consume one token. Returns true if the request is allowed,
4167
- * false if rate-limited.
4168
- */
4169
- consume() {
4170
- this.refill();
4171
- if (this.tokens >= 1) {
4172
- this.tokens -= 1;
4173
- return true;
4174
- }
4175
- return false;
4176
- }
4177
- /**
4178
- * Seconds until at least one token is available.
4179
- */
4180
- retryAfter() {
4181
- this.refill();
4182
- if (this.tokens >= 1) return 0;
4183
- return Math.ceil((1 - this.tokens) / this.refillRate);
4184
- }
4185
- refill() {
4186
- const now = Date.now();
4187
- const elapsed = (now - this.lastRefill) / 1e3;
4188
- this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
4189
- this.lastRefill = now;
4190
- }
4191
- };
4192
- var limiters = /* @__PURE__ */ new Map();
4193
- function getRateLimiter(category) {
4194
- let limiter = limiters.get(category);
4195
- if (!limiter) {
4196
- const config = CATEGORY_CONFIGS[category] ?? CATEGORY_CONFIGS.read;
4197
- limiter = new RateLimiter(config);
4198
- limiters.set(category, limiter);
4199
- }
4200
- return limiter;
4201
- }
4202
- function checkRateLimit(category, key) {
4203
- const bucketKey = key ? `${category}:${key}` : category;
4204
- const limiter = getRateLimiter(bucketKey);
4205
- const allowed = limiter.consume();
4206
- return {
4207
- allowed,
4208
- retryAfter: allowed ? 0 : limiter.retryAfter()
4209
- };
4210
- }
4211
-
4212
- // src/tools/content.ts
4213
4279
  init_supabase();
4214
4280
 
4215
4281
  // src/lib/sanitize-error.ts
@@ -4263,8 +4329,15 @@ function sanitizeDbError(error) {
4263
4329
 
4264
4330
  // src/tools/content.ts
4265
4331
  init_request_context();
4266
- var MAX_CREDITS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
4267
- var MAX_ASSETS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0));
4332
+ init_version();
4333
+ var MAX_CREDITS_PER_RUN = Math.max(
4334
+ 0,
4335
+ Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0)
4336
+ );
4337
+ var MAX_ASSETS_PER_RUN = Math.max(
4338
+ 0,
4339
+ Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0)
4340
+ );
4268
4341
  var _globalCreditsUsed = 0;
4269
4342
  var _globalAssetsGenerated = 0;
4270
4343
  function getCreditsUsed() {
@@ -4306,7 +4379,7 @@ function getCurrentBudgetStatus() {
4306
4379
  function asEnvelope(data) {
4307
4380
  return {
4308
4381
  _meta: {
4309
- version: "0.2.0",
4382
+ version: MCP_VERSION,
4310
4383
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
4311
4384
  },
4312
4385
  data
@@ -4388,8 +4461,12 @@ function registerContentTools(server2) {
4388
4461
  enable_audio: z2.boolean().optional().describe(
4389
4462
  "Enable native audio generation. Kling 2.6: doubles cost. Kling 3.0: 50% more (std 30/sec, pro 40/sec). 5+ languages."
4390
4463
  ),
4391
- image_url: z2.string().optional().describe("Start frame image URL for image-to-video (Kling 3.0 frame control)."),
4392
- end_frame_url: z2.string().optional().describe("End frame image URL (Kling 3.0 only). Enables seamless loop transitions."),
4464
+ image_url: z2.string().optional().describe(
4465
+ "Start frame image URL for image-to-video (Kling 3.0 frame control)."
4466
+ ),
4467
+ end_frame_url: z2.string().optional().describe(
4468
+ "End frame image URL (Kling 3.0 only). Enables seamless loop transitions."
4469
+ ),
4393
4470
  response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
4394
4471
  },
4395
4472
  async ({
@@ -4827,10 +4904,13 @@ function registerContentTools(server2) {
4827
4904
  };
4828
4905
  }
4829
4906
  if (job.external_id && (job.status === "pending" || job.status === "processing")) {
4830
- const { data: liveStatus } = await callEdgeFunction("kie-task-status", {
4831
- taskId: job.external_id,
4832
- model: job.model
4833
- });
4907
+ const { data: liveStatus } = await callEdgeFunction(
4908
+ "kie-task-status",
4909
+ {
4910
+ taskId: job.external_id,
4911
+ model: job.model
4912
+ }
4913
+ );
4834
4914
  if (liveStatus) {
4835
4915
  const lines2 = [
4836
4916
  `Job: ${job.id}`,
@@ -4904,7 +4984,12 @@ function registerContentTools(server2) {
4904
4984
  });
4905
4985
  if (format === "json") {
4906
4986
  return {
4907
- content: [{ type: "text", text: JSON.stringify(asEnvelope(job), null, 2) }]
4987
+ content: [
4988
+ {
4989
+ type: "text",
4990
+ text: JSON.stringify(asEnvelope(job), null, 2)
4991
+ }
4992
+ ]
4908
4993
  };
4909
4994
  }
4910
4995
  return {
@@ -4922,7 +5007,15 @@ function registerContentTools(server2) {
4922
5007
  brand_context: z2.string().max(3e3).optional().describe(
4923
5008
  "Brand context JSON from extract_brand. Include colors, voice tone, visual style keywords for consistent branding across frames."
4924
5009
  ),
4925
- platform: z2.enum(["tiktok", "instagram-reels", "youtube-shorts", "youtube", "general"]).describe("Target platform. Determines aspect ratio, duration, and pacing."),
5010
+ platform: z2.enum([
5011
+ "tiktok",
5012
+ "instagram-reels",
5013
+ "youtube-shorts",
5014
+ "youtube",
5015
+ "general"
5016
+ ]).describe(
5017
+ "Target platform. Determines aspect ratio, duration, and pacing."
5018
+ ),
4926
5019
  target_duration: z2.number().min(5).max(120).optional().describe(
4927
5020
  "Target total duration in seconds. Defaults to 30s for short-form, 60s for YouTube."
4928
5021
  ),
@@ -4930,7 +5023,9 @@ function registerContentTools(server2) {
4930
5023
  style: z2.string().optional().describe(
4931
5024
  'Visual style direction (e.g., "cinematic", "anime", "documentary", "motion graphics").'
4932
5025
  ),
4933
- response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to json for structured storyboard data.")
5026
+ response_format: z2.enum(["text", "json"]).optional().describe(
5027
+ "Response format. Defaults to json for structured storyboard data."
5028
+ )
4934
5029
  },
4935
5030
  async ({
4936
5031
  concept,
@@ -4943,7 +5038,11 @@ function registerContentTools(server2) {
4943
5038
  }) => {
4944
5039
  const format = response_format ?? "json";
4945
5040
  const startedAt = Date.now();
4946
- const isShortForm = ["tiktok", "instagram-reels", "youtube-shorts"].includes(platform3);
5041
+ const isShortForm = [
5042
+ "tiktok",
5043
+ "instagram-reels",
5044
+ "youtube-shorts"
5045
+ ].includes(platform3);
4947
5046
  const duration = target_duration ?? (isShortForm ? 30 : 60);
4948
5047
  const scenes = num_scenes ?? (isShortForm ? 7 : 10);
4949
5048
  const aspectRatio = isShortForm ? "9:16" : "16:9";
@@ -5036,7 +5135,12 @@ Return ONLY valid JSON in this exact format:
5036
5135
  details: { error }
5037
5136
  });
5038
5137
  return {
5039
- content: [{ type: "text", text: `Storyboard generation failed: ${error}` }],
5138
+ content: [
5139
+ {
5140
+ type: "text",
5141
+ text: `Storyboard generation failed: ${error}`
5142
+ }
5143
+ ],
5040
5144
  isError: true
5041
5145
  };
5042
5146
  }
@@ -5052,7 +5156,12 @@ Return ONLY valid JSON in this exact format:
5052
5156
  try {
5053
5157
  const parsed = JSON.parse(rawContent);
5054
5158
  return {
5055
- content: [{ type: "text", text: JSON.stringify(asEnvelope(parsed), null, 2) }]
5159
+ content: [
5160
+ {
5161
+ type: "text",
5162
+ text: JSON.stringify(asEnvelope(parsed), null, 2)
5163
+ }
5164
+ ]
5056
5165
  };
5057
5166
  } catch {
5058
5167
  return {
@@ -5116,7 +5225,10 @@ Return ONLY valid JSON in this exact format:
5116
5225
  isError: true
5117
5226
  };
5118
5227
  }
5119
- const rateLimit = checkRateLimit("posting", `generate_voiceover:${userId}`);
5228
+ const rateLimit = checkRateLimit(
5229
+ "posting",
5230
+ `generate_voiceover:${userId}`
5231
+ );
5120
5232
  if (!rateLimit.allowed) {
5121
5233
  await logMcpToolInvocation({
5122
5234
  toolName: "generate_voiceover",
@@ -5151,7 +5263,12 @@ Return ONLY valid JSON in this exact format:
5151
5263
  details: { error }
5152
5264
  });
5153
5265
  return {
5154
- content: [{ type: "text", text: `Voiceover generation failed: ${error}` }],
5266
+ content: [
5267
+ {
5268
+ type: "text",
5269
+ text: `Voiceover generation failed: ${error}`
5270
+ }
5271
+ ],
5155
5272
  isError: true
5156
5273
  };
5157
5274
  }
@@ -5164,7 +5281,10 @@ Return ONLY valid JSON in this exact format:
5164
5281
  });
5165
5282
  return {
5166
5283
  content: [
5167
- { type: "text", text: "Voiceover generation failed: no audio URL returned." }
5284
+ {
5285
+ type: "text",
5286
+ text: "Voiceover generation failed: no audio URL returned."
5287
+ }
5168
5288
  ],
5169
5289
  isError: true
5170
5290
  };
@@ -5236,7 +5356,9 @@ Return ONLY valid JSON in this exact format:
5236
5356
  "Carousel template. hormozi-authority: bold typography, one idea per slide, dark backgrounds. educational-series: numbered tips. Default: hormozi-authority."
5237
5357
  ),
5238
5358
  slide_count: z2.number().min(3).max(10).optional().describe("Number of slides (3-10). Default: 7."),
5239
- aspect_ratio: z2.enum(["1:1", "4:5", "9:16"]).optional().describe("Aspect ratio. 1:1 square (default), 4:5 portrait, 9:16 story."),
5359
+ aspect_ratio: z2.enum(["1:1", "4:5", "9:16"]).optional().describe(
5360
+ "Aspect ratio. 1:1 square (default), 4:5 portrait, 9:16 story."
5361
+ ),
5240
5362
  style: z2.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
5241
5363
  "Visual style. hormozi: black bg, bold white text, gold accents. Default: hormozi (when using hormozi-authority template)."
5242
5364
  ),
@@ -5273,7 +5395,10 @@ Return ONLY valid JSON in this exact format:
5273
5395
  };
5274
5396
  }
5275
5397
  const userId = await getDefaultUserId();
5276
- const rateLimit = checkRateLimit("posting", `generate_carousel:${userId}`);
5398
+ const rateLimit = checkRateLimit(
5399
+ "posting",
5400
+ `generate_carousel:${userId}`
5401
+ );
5277
5402
  if (!rateLimit.allowed) {
5278
5403
  await logMcpToolInvocation({
5279
5404
  toolName: "generate_carousel",
@@ -5311,7 +5436,12 @@ Return ONLY valid JSON in this exact format:
5311
5436
  details: { error }
5312
5437
  });
5313
5438
  return {
5314
- content: [{ type: "text", text: `Carousel generation failed: ${error}` }],
5439
+ content: [
5440
+ {
5441
+ type: "text",
5442
+ text: `Carousel generation failed: ${error}`
5443
+ }
5444
+ ],
5315
5445
  isError: true
5316
5446
  };
5317
5447
  }
@@ -5323,7 +5453,12 @@ Return ONLY valid JSON in this exact format:
5323
5453
  details: { error: "No carousel data returned" }
5324
5454
  });
5325
5455
  return {
5326
- content: [{ type: "text", text: "Carousel generation returned no data." }],
5456
+ content: [
5457
+ {
5458
+ type: "text",
5459
+ text: "Carousel generation returned no data."
5460
+ }
5461
+ ],
5327
5462
  isError: true
5328
5463
  };
5329
5464
  }
@@ -5389,6 +5524,7 @@ import { z as z3 } from "zod";
5389
5524
  import { createHash as createHash2 } from "node:crypto";
5390
5525
  init_supabase();
5391
5526
  init_quality();
5527
+ init_version();
5392
5528
  var PLATFORM_CASE_MAP = {
5393
5529
  youtube: "YouTube",
5394
5530
  tiktok: "TikTok",
@@ -5402,7 +5538,7 @@ var PLATFORM_CASE_MAP = {
5402
5538
  function asEnvelope2(data) {
5403
5539
  return {
5404
5540
  _meta: {
5405
- version: "0.2.0",
5541
+ version: MCP_VERSION,
5406
5542
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5407
5543
  },
5408
5544
  data
@@ -5434,15 +5570,21 @@ function registerDistributionTools(server2) {
5434
5570
  "threads",
5435
5571
  "bluesky"
5436
5572
  ])
5437
- ).min(1).describe("Target platforms to post to. Each must have an active OAuth connection."),
5573
+ ).min(1).describe(
5574
+ "Target platforms to post to. Each must have an active OAuth connection."
5575
+ ),
5438
5576
  title: z3.string().optional().describe("Post title (used by YouTube and some other platforms)."),
5439
- hashtags: z3.array(z3.string()).optional().describe('Hashtags to append to the caption. Include or omit the "#" prefix.'),
5577
+ hashtags: z3.array(z3.string()).optional().describe(
5578
+ 'Hashtags to append to the caption. Include or omit the "#" prefix.'
5579
+ ),
5440
5580
  schedule_at: z3.string().optional().describe(
5441
5581
  'ISO 8601 datetime for scheduled posting (e.g. "2026-03-15T14:00:00Z"). Omit for immediate posting.'
5442
5582
  ),
5443
5583
  project_id: z3.string().optional().describe("Social Neuron project ID to associate this post with."),
5444
5584
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
5445
- attribution: z3.boolean().optional().describe('If true, appends "Created with Social Neuron" to the caption. Default: false.')
5585
+ attribution: z3.boolean().optional().describe(
5586
+ 'If true, appends "Created with Social Neuron" to the caption. Default: false.'
5587
+ )
5446
5588
  },
5447
5589
  async ({
5448
5590
  media_url,
@@ -5461,7 +5603,12 @@ function registerDistributionTools(server2) {
5461
5603
  const startedAt = Date.now();
5462
5604
  if ((!caption || caption.trim().length === 0) && (!title || title.trim().length === 0)) {
5463
5605
  return {
5464
- content: [{ type: "text", text: "Either caption or title is required." }],
5606
+ content: [
5607
+ {
5608
+ type: "text",
5609
+ text: "Either caption or title is required."
5610
+ }
5611
+ ],
5465
5612
  isError: true
5466
5613
  };
5467
5614
  }
@@ -5484,7 +5631,9 @@ function registerDistributionTools(server2) {
5484
5631
  isError: true
5485
5632
  };
5486
5633
  }
5487
- const normalizedPlatforms = platforms.map((p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p);
5634
+ const normalizedPlatforms = platforms.map(
5635
+ (p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
5636
+ );
5488
5637
  let finalCaption = caption;
5489
5638
  if (attribution && finalCaption) {
5490
5639
  finalCaption = `${finalCaption}
@@ -5548,7 +5697,9 @@ Created with Social Neuron`;
5548
5697
  ];
5549
5698
  for (const [platform3, result] of Object.entries(data.results)) {
5550
5699
  if (result.success) {
5551
- lines.push(` ${platform3}: OK (jobId=${result.jobId}, postId=${result.postId})`);
5700
+ lines.push(
5701
+ ` ${platform3}: OK (jobId=${result.jobId}, postId=${result.postId})`
5702
+ );
5552
5703
  } else {
5553
5704
  lines.push(` ${platform3}: FAILED - ${result.error}`);
5554
5705
  }
@@ -5565,7 +5716,12 @@ Created with Social Neuron`;
5565
5716
  });
5566
5717
  if (format === "json") {
5567
5718
  return {
5568
- content: [{ type: "text", text: JSON.stringify(asEnvelope2(data), null, 2) }],
5719
+ content: [
5720
+ {
5721
+ type: "text",
5722
+ text: JSON.stringify(asEnvelope2(data), null, 2)
5723
+ }
5724
+ ],
5569
5725
  isError: !data.success
5570
5726
  };
5571
5727
  }
@@ -5621,12 +5777,17 @@ Created with Social Neuron`;
5621
5777
  for (const account of accounts) {
5622
5778
  const name = account.username || "(unnamed)";
5623
5779
  const platformLower = account.platform.toLowerCase();
5624
- lines.push(` ${platformLower}: ${name} (connected ${account.created_at.split("T")[0]})`);
5780
+ lines.push(
5781
+ ` ${platformLower}: ${name} (connected ${account.created_at.split("T")[0]})`
5782
+ );
5625
5783
  }
5626
5784
  if (format === "json") {
5627
5785
  return {
5628
5786
  content: [
5629
- { type: "text", text: JSON.stringify(asEnvelope2({ accounts }), null, 2) }
5787
+ {
5788
+ type: "text",
5789
+ text: JSON.stringify(asEnvelope2({ accounts }), null, 2)
5790
+ }
5630
5791
  ]
5631
5792
  };
5632
5793
  }
@@ -5688,7 +5849,10 @@ Created with Social Neuron`;
5688
5849
  if (format === "json") {
5689
5850
  return {
5690
5851
  content: [
5691
- { type: "text", text: JSON.stringify(asEnvelope2({ posts: [] }), null, 2) }
5852
+ {
5853
+ type: "text",
5854
+ text: JSON.stringify(asEnvelope2({ posts: [] }), null, 2)
5855
+ }
5692
5856
  ]
5693
5857
  };
5694
5858
  }
@@ -5705,7 +5869,10 @@ Created with Social Neuron`;
5705
5869
  if (format === "json") {
5706
5870
  return {
5707
5871
  content: [
5708
- { type: "text", text: JSON.stringify(asEnvelope2({ posts }), null, 2) }
5872
+ {
5873
+ type: "text",
5874
+ text: JSON.stringify(asEnvelope2({ posts }), null, 2)
5875
+ }
5709
5876
  ]
5710
5877
  };
5711
5878
  }
@@ -5766,7 +5933,13 @@ Created with Social Neuron`;
5766
5933
  min_gap_hours: z3.number().min(1).max(24).default(4).describe("Minimum gap between posts on same platform"),
5767
5934
  response_format: z3.enum(["text", "json"]).default("text")
5768
5935
  },
5769
- async ({ platforms, count, start_after, min_gap_hours, response_format }) => {
5936
+ async ({
5937
+ platforms,
5938
+ count,
5939
+ start_after,
5940
+ min_gap_hours,
5941
+ response_format
5942
+ }) => {
5770
5943
  const startedAt = Date.now();
5771
5944
  try {
5772
5945
  const userId = await getDefaultUserId();
@@ -5777,7 +5950,9 @@ Created with Social Neuron`;
5777
5950
  const gapMs = min_gap_hours * 60 * 60 * 1e3;
5778
5951
  const candidates = [];
5779
5952
  for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
5780
- const date = new Date(startDate.getTime() + dayOffset * 24 * 60 * 60 * 1e3);
5953
+ const date = new Date(
5954
+ startDate.getTime() + dayOffset * 24 * 60 * 60 * 1e3
5955
+ );
5781
5956
  const dayOfWeek = date.getUTCDay();
5782
5957
  for (const platform3 of platforms) {
5783
5958
  const hours = PREFERRED_HOURS[platform3] ?? [12, 16];
@@ -5786,8 +5961,11 @@ Created with Social Neuron`;
5786
5961
  slotDate.setUTCHours(hours[hourIdx], 0, 0, 0);
5787
5962
  if (slotDate <= startDate) continue;
5788
5963
  const hasConflict = (existingPosts ?? []).some((post) => {
5789
- if (String(post.platform).toLowerCase() !== platform3) return false;
5790
- const postTime = new Date(post.scheduled_at ?? post.published_at).getTime();
5964
+ if (String(post.platform).toLowerCase() !== platform3)
5965
+ return false;
5966
+ const postTime = new Date(
5967
+ post.scheduled_at ?? post.published_at
5968
+ ).getTime();
5791
5969
  return Math.abs(postTime - slotDate.getTime()) < gapMs;
5792
5970
  });
5793
5971
  let engagementScore = hours.length - hourIdx;
@@ -5832,15 +6010,22 @@ Created with Social Neuron`;
5832
6010
  };
5833
6011
  }
5834
6012
  const lines = [];
5835
- lines.push(`Found ${slots.length} optimal slots (${conflictsAvoided} conflicts avoided):`);
6013
+ lines.push(
6014
+ `Found ${slots.length} optimal slots (${conflictsAvoided} conflicts avoided):`
6015
+ );
5836
6016
  lines.push("");
5837
6017
  lines.push("Datetime (UTC) | Platform | Score");
5838
6018
  lines.push("-------------------------+------------+------");
5839
6019
  for (const s of slots) {
5840
6020
  const dt = s.datetime.replace("T", " ").slice(0, 19);
5841
- lines.push(`${dt.padEnd(25)}| ${s.platform.padEnd(11)}| ${s.engagement_score}`);
6021
+ lines.push(
6022
+ `${dt.padEnd(25)}| ${s.platform.padEnd(11)}| ${s.engagement_score}`
6023
+ );
5842
6024
  }
5843
- return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
6025
+ return {
6026
+ content: [{ type: "text", text: lines.join("\n") }],
6027
+ isError: false
6028
+ };
5844
6029
  } catch (err) {
5845
6030
  const durationMs = Date.now() - startedAt;
5846
6031
  const message = err instanceof Error ? err.message : String(err);
@@ -5851,7 +6036,9 @@ Created with Social Neuron`;
5851
6036
  details: { error: message }
5852
6037
  });
5853
6038
  return {
5854
- content: [{ type: "text", text: `Failed to find slots: ${message}` }],
6039
+ content: [
6040
+ { type: "text", text: `Failed to find slots: ${message}` }
6041
+ ],
5855
6042
  isError: true
5856
6043
  };
5857
6044
  }
@@ -5878,8 +6065,12 @@ Created with Social Neuron`;
5878
6065
  auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
5879
6066
  dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
5880
6067
  response_format: z3.enum(["text", "json"]).default("text"),
5881
- enforce_quality: z3.boolean().default(true).describe("When true, block scheduling for posts that fail quality checks."),
5882
- quality_threshold: z3.number().int().min(0).max(35).optional().describe("Optional quality threshold override. Defaults to project setting or 26."),
6068
+ enforce_quality: z3.boolean().default(true).describe(
6069
+ "When true, block scheduling for posts that fail quality checks."
6070
+ ),
6071
+ quality_threshold: z3.number().int().min(0).max(35).optional().describe(
6072
+ "Optional quality threshold override. Defaults to project setting or 26."
6073
+ ),
5883
6074
  batch_size: z3.number().int().min(1).max(10).default(4).describe("Concurrent schedule calls per platform batch."),
5884
6075
  idempotency_seed: z3.string().max(128).optional().describe("Optional stable seed used when building idempotency keys.")
5885
6076
  },
@@ -5918,17 +6109,25 @@ Created with Social Neuron`;
5918
6109
  if (!stored?.plan_payload) {
5919
6110
  return {
5920
6111
  content: [
5921
- { type: "text", text: `No content plan found for plan_id=${plan_id}` }
6112
+ {
6113
+ type: "text",
6114
+ text: `No content plan found for plan_id=${plan_id}`
6115
+ }
5922
6116
  ],
5923
6117
  isError: true
5924
6118
  };
5925
6119
  }
5926
6120
  const payload = stored.plan_payload;
5927
- const postsFromPayload = Array.isArray(payload.posts) ? payload.posts : Array.isArray(payload.data?.posts) ? payload.data.posts : null;
6121
+ const postsFromPayload = Array.isArray(payload.posts) ? payload.posts : Array.isArray(
6122
+ payload.data?.posts
6123
+ ) ? payload.data.posts : null;
5928
6124
  if (!postsFromPayload) {
5929
6125
  return {
5930
6126
  content: [
5931
- { type: "text", text: `Stored plan ${plan_id} has no posts array.` }
6127
+ {
6128
+ type: "text",
6129
+ text: `Stored plan ${plan_id} has no posts array.`
6130
+ }
5932
6131
  ],
5933
6132
  isError: true
5934
6133
  };
@@ -6020,7 +6219,10 @@ Created with Social Neuron`;
6020
6219
  approvalSummary = {
6021
6220
  total: approvals.length,
6022
6221
  eligible: approvedPosts.length,
6023
- skipped: Math.max(0, workingPlan.posts.length - approvedPosts.length)
6222
+ skipped: Math.max(
6223
+ 0,
6224
+ workingPlan.posts.length - approvedPosts.length
6225
+ )
6024
6226
  };
6025
6227
  workingPlan = {
6026
6228
  ...workingPlan,
@@ -6054,9 +6256,14 @@ Created with Social Neuron`;
6054
6256
  try {
6055
6257
  const { data: settingsData } = await supabase.from("system_settings").select("value").eq("key", "content_safety").maybeSingle();
6056
6258
  if (settingsData?.value?.quality_threshold !== void 0) {
6057
- const parsedThreshold = Number(settingsData.value.quality_threshold);
6259
+ const parsedThreshold = Number(
6260
+ settingsData.value.quality_threshold
6261
+ );
6058
6262
  if (Number.isFinite(parsedThreshold)) {
6059
- effectiveQualityThreshold = Math.max(0, Math.min(35, Math.trunc(parsedThreshold)));
6263
+ effectiveQualityThreshold = Math.max(
6264
+ 0,
6265
+ Math.min(35, Math.trunc(parsedThreshold))
6266
+ );
6060
6267
  }
6061
6268
  }
6062
6269
  if (Array.isArray(settingsData?.value?.custom_banned_terms)) {
@@ -6092,13 +6299,18 @@ Created with Social Neuron`;
6092
6299
  }
6093
6300
  };
6094
6301
  });
6095
- const qualityPassed = postsWithResults.filter((post) => post.quality.passed).length;
6302
+ const qualityPassed = postsWithResults.filter(
6303
+ (post) => post.quality.passed
6304
+ ).length;
6096
6305
  const qualitySummary = {
6097
6306
  total_posts: postsWithResults.length,
6098
6307
  passed: qualityPassed,
6099
6308
  failed: postsWithResults.length - qualityPassed,
6100
6309
  avg_score: postsWithResults.length > 0 ? Number(
6101
- (postsWithResults.reduce((sum, post) => sum + post.quality.score, 0) / postsWithResults.length).toFixed(2)
6310
+ (postsWithResults.reduce(
6311
+ (sum, post) => sum + post.quality.score,
6312
+ 0
6313
+ ) / postsWithResults.length).toFixed(2)
6102
6314
  ) : 0
6103
6315
  };
6104
6316
  if (dry_run) {
@@ -6168,8 +6380,13 @@ Created with Social Neuron`;
6168
6380
  }
6169
6381
  }
6170
6382
  lines2.push("");
6171
- lines2.push(`Summary: ${passed}/${workingPlan.posts.length} passed quality check`);
6172
- return { content: [{ type: "text", text: lines2.join("\n") }], isError: false };
6383
+ lines2.push(
6384
+ `Summary: ${passed}/${workingPlan.posts.length} passed quality check`
6385
+ );
6386
+ return {
6387
+ content: [{ type: "text", text: lines2.join("\n") }],
6388
+ isError: false
6389
+ };
6173
6390
  }
6174
6391
  let scheduled = 0;
6175
6392
  let failed = 0;
@@ -6261,7 +6478,8 @@ Created with Social Neuron`;
6261
6478
  }
6262
6479
  const chunk = (arr, size) => {
6263
6480
  const out = [];
6264
- for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
6481
+ for (let i = 0; i < arr.length; i += size)
6482
+ out.push(arr.slice(i, i + size));
6265
6483
  return out;
6266
6484
  };
6267
6485
  const platformBatches = Array.from(grouped.entries()).map(
@@ -6269,7 +6487,9 @@ Created with Social Neuron`;
6269
6487
  const platformResults = [];
6270
6488
  const batches = chunk(platformPosts, batch_size);
6271
6489
  for (const batch of batches) {
6272
- const settled = await Promise.allSettled(batch.map((post) => scheduleOne(post)));
6490
+ const settled = await Promise.allSettled(
6491
+ batch.map((post) => scheduleOne(post))
6492
+ );
6273
6493
  for (const outcome of settled) {
6274
6494
  if (outcome.status === "fulfilled") {
6275
6495
  platformResults.push(outcome.value);
@@ -6335,7 +6555,11 @@ Created with Social Neuron`;
6335
6555
  plan_id: effectivePlanId,
6336
6556
  approvals: approvalSummary,
6337
6557
  posts: results,
6338
- summary: { total_posts: workingPlan.posts.length, scheduled, failed }
6558
+ summary: {
6559
+ total_posts: workingPlan.posts.length,
6560
+ scheduled,
6561
+ failed
6562
+ }
6339
6563
  }),
6340
6564
  null,
6341
6565
  2
@@ -6373,7 +6597,12 @@ Created with Social Neuron`;
6373
6597
  details: { error: message }
6374
6598
  });
6375
6599
  return {
6376
- content: [{ type: "text", text: `Batch scheduling failed: ${message}` }],
6600
+ content: [
6601
+ {
6602
+ type: "text",
6603
+ text: `Batch scheduling failed: ${message}`
6604
+ }
6605
+ ],
6377
6606
  isError: true
6378
6607
  };
6379
6608
  }
@@ -6385,10 +6614,11 @@ Created with Social Neuron`;
6385
6614
  init_supabase();
6386
6615
  init_edge_function();
6387
6616
  import { z as z4 } from "zod";
6617
+ init_version();
6388
6618
  function asEnvelope3(data) {
6389
6619
  return {
6390
6620
  _meta: {
6391
- version: "0.2.0",
6621
+ version: MCP_VERSION,
6392
6622
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6393
6623
  },
6394
6624
  data
@@ -6501,7 +6731,9 @@ function registerAnalyticsTools(server2) {
6501
6731
  ]
6502
6732
  };
6503
6733
  }
6504
- const { data: simpleRows, error: simpleError } = await supabase.from("post_analytics").select("id, post_id, platform, views, likes, comments, shares, captured_at").in("post_id", postIds).gte("captured_at", sinceIso).order("captured_at", { ascending: false }).limit(maxPosts);
6734
+ const { data: simpleRows, error: simpleError } = await supabase.from("post_analytics").select(
6735
+ "id, post_id, platform, views, likes, comments, shares, captured_at"
6736
+ ).in("post_id", postIds).gte("captured_at", sinceIso).order("captured_at", { ascending: false }).limit(maxPosts);
6505
6737
  if (simpleError) {
6506
6738
  return {
6507
6739
  content: [
@@ -6513,7 +6745,12 @@ function registerAnalyticsTools(server2) {
6513
6745
  isError: true
6514
6746
  };
6515
6747
  }
6516
- return formatSimpleAnalytics(simpleRows, platform3, lookbackDays, format);
6748
+ return formatSimpleAnalytics(
6749
+ simpleRows,
6750
+ platform3,
6751
+ lookbackDays,
6752
+ format
6753
+ );
6517
6754
  }
6518
6755
  if (!rows || rows.length === 0) {
6519
6756
  if (format === "json") {
@@ -6590,7 +6827,10 @@ function registerAnalyticsTools(server2) {
6590
6827
  const format = response_format ?? "text";
6591
6828
  const startedAt = Date.now();
6592
6829
  const userId = await getDefaultUserId();
6593
- const rateLimit = checkRateLimit("posting", `refresh_platform_analytics:${userId}`);
6830
+ const rateLimit = checkRateLimit(
6831
+ "posting",
6832
+ `refresh_platform_analytics:${userId}`
6833
+ );
6594
6834
  if (!rateLimit.allowed) {
6595
6835
  await logMcpToolInvocation({
6596
6836
  toolName: "refresh_platform_analytics",
@@ -6608,7 +6848,9 @@ function registerAnalyticsTools(server2) {
6608
6848
  isError: true
6609
6849
  };
6610
6850
  }
6611
- const { data, error } = await callEdgeFunction("fetch-analytics", { userId });
6851
+ const { data, error } = await callEdgeFunction("fetch-analytics", {
6852
+ userId
6853
+ });
6612
6854
  if (error) {
6613
6855
  await logMcpToolInvocation({
6614
6856
  toolName: "refresh_platform_analytics",
@@ -6617,7 +6859,12 @@ function registerAnalyticsTools(server2) {
6617
6859
  details: { error }
6618
6860
  });
6619
6861
  return {
6620
- content: [{ type: "text", text: `Error refreshing analytics: ${error}` }],
6862
+ content: [
6863
+ {
6864
+ type: "text",
6865
+ text: `Error refreshing analytics: ${error}`
6866
+ }
6867
+ ],
6621
6868
  isError: true
6622
6869
  };
6623
6870
  }
@@ -6630,12 +6877,18 @@ function registerAnalyticsTools(server2) {
6630
6877
  details: { error: "Edge function returned success=false" }
6631
6878
  });
6632
6879
  return {
6633
- content: [{ type: "text", text: "Analytics refresh failed." }],
6880
+ content: [
6881
+ { type: "text", text: "Analytics refresh failed." }
6882
+ ],
6634
6883
  isError: true
6635
6884
  };
6636
6885
  }
6637
- const queued = (result.results ?? []).filter((r) => r.status === "queued").length;
6638
- const errored = (result.results ?? []).filter((r) => r.status === "error").length;
6886
+ const queued = (result.results ?? []).filter(
6887
+ (r) => r.status === "queued"
6888
+ ).length;
6889
+ const errored = (result.results ?? []).filter(
6890
+ (r) => r.status === "error"
6891
+ ).length;
6639
6892
  const lines = [
6640
6893
  `Analytics refresh triggered successfully.`,
6641
6894
  ` Posts processed: ${result.postsProcessed}`,
@@ -6681,7 +6934,10 @@ function formatAnalytics(summary, days, format) {
6681
6934
  if (format === "json") {
6682
6935
  return {
6683
6936
  content: [
6684
- { type: "text", text: JSON.stringify(asEnvelope3({ ...summary, days }), null, 2) }
6937
+ {
6938
+ type: "text",
6939
+ text: JSON.stringify(asEnvelope3({ ...summary, days }), null, 2)
6940
+ }
6685
6941
  ]
6686
6942
  };
6687
6943
  }
@@ -6799,10 +7055,160 @@ function formatSimpleAnalytics(rows, platform3, days, format) {
6799
7055
  init_edge_function();
6800
7056
  init_supabase();
6801
7057
  import { z as z5 } from "zod";
7058
+
7059
+ // src/lib/ssrf.ts
7060
+ var BLOCKED_IP_PATTERNS = [
7061
+ // IPv4 localhost/loopback
7062
+ /^127\./,
7063
+ /^0\./,
7064
+ // IPv4 private ranges (RFC 1918)
7065
+ /^10\./,
7066
+ /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
7067
+ /^192\.168\./,
7068
+ // IPv4 link-local
7069
+ /^169\.254\./,
7070
+ // Cloud metadata endpoint (AWS, GCP, Azure)
7071
+ /^169\.254\.169\.254$/,
7072
+ // IPv4 broadcast
7073
+ /^255\./,
7074
+ // Shared address space (RFC 6598)
7075
+ /^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./
7076
+ ];
7077
+ var BLOCKED_IPV6_PATTERNS = [
7078
+ /^::1$/i,
7079
+ // loopback
7080
+ /^::$/i,
7081
+ // unspecified
7082
+ /^fe[89ab][0-9a-f]:/i,
7083
+ // link-local fe80::/10
7084
+ /^fc[0-9a-f]:/i,
7085
+ // unique local fc00::/7
7086
+ /^fd[0-9a-f]:/i,
7087
+ // unique local fc00::/7
7088
+ /^::ffff:127\./i,
7089
+ // IPv4-mapped localhost
7090
+ /^::ffff:(0|10|127|169\.254|172\.(1[6-9]|2[0-9]|3[0-1])|192\.168)\./i
7091
+ // IPv4-mapped private
7092
+ ];
7093
+ var BLOCKED_HOSTNAMES = [
7094
+ "localhost",
7095
+ "localhost.localdomain",
7096
+ "local",
7097
+ "127.0.0.1",
7098
+ "0.0.0.0",
7099
+ "[::1]",
7100
+ "[::ffff:127.0.0.1]",
7101
+ // Cloud metadata endpoints
7102
+ "metadata.google.internal",
7103
+ "metadata.goog",
7104
+ "instance-data",
7105
+ "instance-data.ec2.internal"
7106
+ ];
7107
+ var ALLOWED_PROTOCOLS = ["http:", "https:"];
7108
+ var BLOCKED_PORTS = [22, 23, 25, 110, 143, 445, 3306, 5432, 6379, 27017, 11211];
7109
+ function isBlockedIP(ip) {
7110
+ const normalized = ip.replace(/^\[/, "").replace(/\]$/, "");
7111
+ if (normalized.includes(":")) {
7112
+ return BLOCKED_IPV6_PATTERNS.some((pattern) => pattern.test(normalized));
7113
+ }
7114
+ return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(normalized));
7115
+ }
7116
+ function isBlockedHostname(hostname) {
7117
+ return BLOCKED_HOSTNAMES.includes(hostname.toLowerCase());
7118
+ }
7119
+ function isIPAddress(hostname) {
7120
+ const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
7121
+ const ipv6Pattern = /^\[?[a-fA-F0-9:]+\]?$/;
7122
+ return ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname);
7123
+ }
7124
+ async function validateUrlForSSRF(urlString) {
7125
+ try {
7126
+ const url = new URL(urlString);
7127
+ if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
7128
+ return {
7129
+ isValid: false,
7130
+ error: `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are allowed.`
7131
+ };
7132
+ }
7133
+ if (url.username || url.password) {
7134
+ return {
7135
+ isValid: false,
7136
+ error: "URLs with embedded credentials are not allowed."
7137
+ };
7138
+ }
7139
+ const hostname = url.hostname.toLowerCase();
7140
+ if (isBlockedHostname(hostname)) {
7141
+ return {
7142
+ isValid: false,
7143
+ error: "Access to internal/localhost addresses is not allowed."
7144
+ };
7145
+ }
7146
+ if (isIPAddress(hostname) && isBlockedIP(hostname)) {
7147
+ return {
7148
+ isValid: false,
7149
+ error: "Access to private/internal IP addresses is not allowed."
7150
+ };
7151
+ }
7152
+ const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80;
7153
+ if (BLOCKED_PORTS.includes(port)) {
7154
+ return {
7155
+ isValid: false,
7156
+ error: `Access to port ${port} is not allowed.`
7157
+ };
7158
+ }
7159
+ let resolvedIP;
7160
+ if (!isIPAddress(hostname)) {
7161
+ try {
7162
+ const dns = await import("node:dns");
7163
+ const resolver = new dns.promises.Resolver();
7164
+ const resolvedIPs = [];
7165
+ try {
7166
+ const aRecords = await resolver.resolve4(hostname);
7167
+ resolvedIPs.push(...aRecords);
7168
+ } catch {
7169
+ }
7170
+ try {
7171
+ const aaaaRecords = await resolver.resolve6(hostname);
7172
+ resolvedIPs.push(...aaaaRecords);
7173
+ } catch {
7174
+ }
7175
+ if (resolvedIPs.length === 0) {
7176
+ return {
7177
+ isValid: false,
7178
+ error: "DNS resolution failed: hostname did not resolve to any address."
7179
+ };
7180
+ }
7181
+ for (const ip of resolvedIPs) {
7182
+ if (isBlockedIP(ip)) {
7183
+ return {
7184
+ isValid: false,
7185
+ error: "Hostname resolves to a private/internal IP address."
7186
+ };
7187
+ }
7188
+ }
7189
+ resolvedIP = resolvedIPs[0];
7190
+ } catch {
7191
+ return {
7192
+ isValid: false,
7193
+ error: "DNS resolution failed. Cannot verify hostname safety."
7194
+ };
7195
+ }
7196
+ }
7197
+ return { isValid: true, sanitizedUrl: url.toString(), resolvedIP };
7198
+ } catch (error) {
7199
+ return {
7200
+ isValid: false,
7201
+ error: `Invalid URL format: ${error instanceof Error ? error.message : "Unknown error"}`
7202
+ };
7203
+ }
7204
+ }
7205
+
7206
+ // src/tools/brand.ts
7207
+ init_version();
6802
7208
  function asEnvelope4(data) {
6803
7209
  return {
6804
7210
  _meta: {
6805
- version: "0.2.0",
7211
+ version: MCP_VERSION,
6806
7212
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6807
7213
  },
6808
7214
  data
@@ -6819,6 +7225,15 @@ function registerBrandTools(server2) {
6819
7225
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6820
7226
  },
6821
7227
  async ({ url, response_format }) => {
7228
+ const ssrfCheck = await validateUrlForSSRF(url);
7229
+ if (!ssrfCheck.isValid) {
7230
+ return {
7231
+ content: [
7232
+ { type: "text", text: `URL blocked: ${ssrfCheck.error}` }
7233
+ ],
7234
+ isError: true
7235
+ };
7236
+ }
6822
7237
  const { data, error } = await callEdgeFunction(
6823
7238
  "brand-extract",
6824
7239
  { url },
@@ -6848,7 +7263,12 @@ function registerBrandTools(server2) {
6848
7263
  }
6849
7264
  if ((response_format || "text") === "json") {
6850
7265
  return {
6851
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(data), null, 2) }]
7266
+ content: [
7267
+ {
7268
+ type: "text",
7269
+ text: JSON.stringify(asEnvelope4(data), null, 2)
7270
+ }
7271
+ ]
6852
7272
  };
6853
7273
  }
6854
7274
  const lines = [
@@ -6912,7 +7332,12 @@ function registerBrandTools(server2) {
6912
7332
  const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
6913
7333
  if (!membership) {
6914
7334
  return {
6915
- content: [{ type: "text", text: "Project is not accessible to current user." }],
7335
+ content: [
7336
+ {
7337
+ type: "text",
7338
+ text: "Project is not accessible to current user."
7339
+ }
7340
+ ],
6916
7341
  isError: true
6917
7342
  };
6918
7343
  }
@@ -6931,13 +7356,21 @@ function registerBrandTools(server2) {
6931
7356
  if (!data) {
6932
7357
  return {
6933
7358
  content: [
6934
- { type: "text", text: "No active brand profile found for this project." }
7359
+ {
7360
+ type: "text",
7361
+ text: "No active brand profile found for this project."
7362
+ }
6935
7363
  ]
6936
7364
  };
6937
7365
  }
6938
7366
  if ((response_format || "text") === "json") {
6939
7367
  return {
6940
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(data), null, 2) }]
7368
+ content: [
7369
+ {
7370
+ type: "text",
7371
+ text: JSON.stringify(asEnvelope4(data), null, 2)
7372
+ }
7373
+ ]
6941
7374
  };
6942
7375
  }
6943
7376
  const lines = [
@@ -6958,11 +7391,18 @@ function registerBrandTools(server2) {
6958
7391
  "Persist a brand profile as the active profile for a project.",
6959
7392
  {
6960
7393
  project_id: z5.string().uuid().optional().describe("Project ID. Defaults to active project context."),
6961
- brand_context: z5.record(z5.string(), z5.unknown()).describe("Brand context payload to save to brand_profiles.brand_context."),
7394
+ brand_context: z5.record(z5.string(), z5.unknown()).describe(
7395
+ "Brand context payload to save to brand_profiles.brand_context."
7396
+ ),
6962
7397
  change_summary: z5.string().max(500).optional().describe("Optional summary of changes."),
6963
7398
  changed_paths: z5.array(z5.string()).optional().describe("Optional changed path list."),
6964
7399
  source_url: z5.string().url().optional().describe("Optional source URL for provenance."),
6965
- extraction_method: z5.enum(["manual", "url_extract", "business_profiler", "product_showcase"]).optional().describe("Extraction method metadata."),
7400
+ extraction_method: z5.enum([
7401
+ "manual",
7402
+ "url_extract",
7403
+ "business_profiler",
7404
+ "product_showcase"
7405
+ ]).optional().describe("Extraction method metadata."),
6966
7406
  overall_confidence: z5.number().min(0).max(1).optional().describe("Optional overall confidence score in range 0..1."),
6967
7407
  extraction_metadata: z5.record(z5.string(), z5.unknown()).optional(),
6968
7408
  response_format: z5.enum(["text", "json"]).optional()
@@ -7002,20 +7442,28 @@ function registerBrandTools(server2) {
7002
7442
  const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
7003
7443
  if (!membership) {
7004
7444
  return {
7005
- content: [{ type: "text", text: "Project is not accessible to current user." }],
7445
+ content: [
7446
+ {
7447
+ type: "text",
7448
+ text: "Project is not accessible to current user."
7449
+ }
7450
+ ],
7006
7451
  isError: true
7007
7452
  };
7008
7453
  }
7009
- const { data: profileId, error } = await supabase.rpc("set_active_brand_profile", {
7010
- p_project_id: projectId,
7011
- p_brand_context: brand_context,
7012
- p_change_summary: change_summary || null,
7013
- p_changed_paths: changed_paths || [],
7014
- p_source_url: source_url || null,
7015
- p_extraction_method: extraction_method || "manual",
7016
- p_overall_confidence: overall_confidence ?? null,
7017
- p_extraction_metadata: extraction_metadata || null
7018
- });
7454
+ const { data: profileId, error } = await supabase.rpc(
7455
+ "set_active_brand_profile",
7456
+ {
7457
+ p_project_id: projectId,
7458
+ p_brand_context: brand_context,
7459
+ p_change_summary: change_summary || null,
7460
+ p_changed_paths: changed_paths || [],
7461
+ p_source_url: source_url || null,
7462
+ p_extraction_method: extraction_method || "manual",
7463
+ p_overall_confidence: overall_confidence ?? null,
7464
+ p_extraction_metadata: extraction_metadata || null
7465
+ }
7466
+ );
7019
7467
  if (error) {
7020
7468
  return {
7021
7469
  content: [
@@ -7036,7 +7484,12 @@ function registerBrandTools(server2) {
7036
7484
  };
7037
7485
  if ((response_format || "text") === "json") {
7038
7486
  return {
7039
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(payload), null, 2) }]
7487
+ content: [
7488
+ {
7489
+ type: "text",
7490
+ text: JSON.stringify(asEnvelope4(payload), null, 2)
7491
+ }
7492
+ ]
7040
7493
  };
7041
7494
  }
7042
7495
  return {
@@ -7092,7 +7545,10 @@ Version: ${payload.version ?? "N/A"}`
7092
7545
  if (!projectId) {
7093
7546
  return {
7094
7547
  content: [
7095
- { type: "text", text: "No project_id provided and no default project found." }
7548
+ {
7549
+ type: "text",
7550
+ text: "No project_id provided and no default project found."
7551
+ }
7096
7552
  ],
7097
7553
  isError: true
7098
7554
  };
@@ -7107,7 +7563,12 @@ Version: ${payload.version ?? "N/A"}`
7107
7563
  const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
7108
7564
  if (!membership) {
7109
7565
  return {
7110
- content: [{ type: "text", text: "Project is not accessible to current user." }],
7566
+ content: [
7567
+ {
7568
+ type: "text",
7569
+ text: "Project is not accessible to current user."
7570
+ }
7571
+ ],
7111
7572
  isError: true
7112
7573
  };
7113
7574
  }
@@ -7123,7 +7584,9 @@ Version: ${payload.version ?? "N/A"}`
7123
7584
  isError: true
7124
7585
  };
7125
7586
  }
7126
- const brandContext = { ...existingProfile.brand_context };
7587
+ const brandContext = {
7588
+ ...existingProfile.brand_context
7589
+ };
7127
7590
  const voiceProfile = brandContext.voiceProfile ?? {};
7128
7591
  const platformOverrides = voiceProfile.platformOverrides ?? {};
7129
7592
  const existingOverride = platformOverrides[platform3] ?? {};
@@ -7147,16 +7610,19 @@ Version: ${payload.version ?? "N/A"}`
7147
7610
  ...brandContext,
7148
7611
  voiceProfile: updatedVoiceProfile
7149
7612
  };
7150
- const { data: profileId, error: saveError } = await supabase.rpc("set_active_brand_profile", {
7151
- p_project_id: projectId,
7152
- p_brand_context: updatedContext,
7153
- p_change_summary: `Updated platform voice override for ${platform3}`,
7154
- p_changed_paths: [`voiceProfile.platformOverrides.${platform3}`],
7155
- p_source_url: null,
7156
- p_extraction_method: "manual",
7157
- p_overall_confidence: null,
7158
- p_extraction_metadata: null
7159
- });
7613
+ const { data: profileId, error: saveError } = await supabase.rpc(
7614
+ "set_active_brand_profile",
7615
+ {
7616
+ p_project_id: projectId,
7617
+ p_brand_context: updatedContext,
7618
+ p_change_summary: `Updated platform voice override for ${platform3}`,
7619
+ p_changed_paths: [`voiceProfile.platformOverrides.${platform3}`],
7620
+ p_source_url: null,
7621
+ p_extraction_method: "manual",
7622
+ p_overall_confidence: null,
7623
+ p_extraction_metadata: null
7624
+ }
7625
+ );
7160
7626
  if (saveError) {
7161
7627
  return {
7162
7628
  content: [
@@ -7177,7 +7643,12 @@ Version: ${payload.version ?? "N/A"}`
7177
7643
  };
7178
7644
  if ((response_format || "text") === "json") {
7179
7645
  return {
7180
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(payload), null, 2) }],
7646
+ content: [
7647
+ {
7648
+ type: "text",
7649
+ text: JSON.stringify(asEnvelope4(payload), null, 2)
7650
+ }
7651
+ ],
7181
7652
  isError: false
7182
7653
  };
7183
7654
  }
@@ -7275,155 +7746,6 @@ async function capturePageScreenshot(page, outputPath, selector) {
7275
7746
  // src/tools/screenshot.ts
7276
7747
  import { resolve, relative } from "node:path";
7277
7748
  import { mkdir } from "node:fs/promises";
7278
-
7279
- // src/lib/ssrf.ts
7280
- var BLOCKED_IP_PATTERNS = [
7281
- // IPv4 localhost/loopback
7282
- /^127\./,
7283
- /^0\./,
7284
- // IPv4 private ranges (RFC 1918)
7285
- /^10\./,
7286
- /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
7287
- /^192\.168\./,
7288
- // IPv4 link-local
7289
- /^169\.254\./,
7290
- // Cloud metadata endpoint (AWS, GCP, Azure)
7291
- /^169\.254\.169\.254$/,
7292
- // IPv4 broadcast
7293
- /^255\./,
7294
- // Shared address space (RFC 6598)
7295
- /^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./
7296
- ];
7297
- var BLOCKED_IPV6_PATTERNS = [
7298
- /^::1$/i,
7299
- // loopback
7300
- /^::$/i,
7301
- // unspecified
7302
- /^fe[89ab][0-9a-f]:/i,
7303
- // link-local fe80::/10
7304
- /^fc[0-9a-f]:/i,
7305
- // unique local fc00::/7
7306
- /^fd[0-9a-f]:/i,
7307
- // unique local fc00::/7
7308
- /^::ffff:127\./i,
7309
- // IPv4-mapped localhost
7310
- /^::ffff:(0|10|127|169\.254|172\.(1[6-9]|2[0-9]|3[0-1])|192\.168)\./i
7311
- // IPv4-mapped private
7312
- ];
7313
- var BLOCKED_HOSTNAMES = [
7314
- "localhost",
7315
- "localhost.localdomain",
7316
- "local",
7317
- "127.0.0.1",
7318
- "0.0.0.0",
7319
- "[::1]",
7320
- "[::ffff:127.0.0.1]",
7321
- // Cloud metadata endpoints
7322
- "metadata.google.internal",
7323
- "metadata.goog",
7324
- "instance-data",
7325
- "instance-data.ec2.internal"
7326
- ];
7327
- var ALLOWED_PROTOCOLS = ["http:", "https:"];
7328
- var BLOCKED_PORTS = [22, 23, 25, 110, 143, 445, 3306, 5432, 6379, 27017, 11211];
7329
- function isBlockedIP(ip) {
7330
- const normalized = ip.replace(/^\[/, "").replace(/\]$/, "");
7331
- if (normalized.includes(":")) {
7332
- return BLOCKED_IPV6_PATTERNS.some((pattern) => pattern.test(normalized));
7333
- }
7334
- return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(normalized));
7335
- }
7336
- function isBlockedHostname(hostname) {
7337
- return BLOCKED_HOSTNAMES.includes(hostname.toLowerCase());
7338
- }
7339
- function isIPAddress(hostname) {
7340
- const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
7341
- const ipv6Pattern = /^\[?[a-fA-F0-9:]+\]?$/;
7342
- return ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname);
7343
- }
7344
- async function validateUrlForSSRF(urlString) {
7345
- try {
7346
- const url = new URL(urlString);
7347
- if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
7348
- return {
7349
- isValid: false,
7350
- error: `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are allowed.`
7351
- };
7352
- }
7353
- if (url.username || url.password) {
7354
- return {
7355
- isValid: false,
7356
- error: "URLs with embedded credentials are not allowed."
7357
- };
7358
- }
7359
- const hostname = url.hostname.toLowerCase();
7360
- if (isBlockedHostname(hostname)) {
7361
- return {
7362
- isValid: false,
7363
- error: "Access to internal/localhost addresses is not allowed."
7364
- };
7365
- }
7366
- if (isIPAddress(hostname) && isBlockedIP(hostname)) {
7367
- return {
7368
- isValid: false,
7369
- error: "Access to private/internal IP addresses is not allowed."
7370
- };
7371
- }
7372
- const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80;
7373
- if (BLOCKED_PORTS.includes(port)) {
7374
- return {
7375
- isValid: false,
7376
- error: `Access to port ${port} is not allowed.`
7377
- };
7378
- }
7379
- let resolvedIP;
7380
- if (!isIPAddress(hostname)) {
7381
- try {
7382
- const dns = await import("node:dns");
7383
- const resolver = new dns.promises.Resolver();
7384
- const resolvedIPs = [];
7385
- try {
7386
- const aRecords = await resolver.resolve4(hostname);
7387
- resolvedIPs.push(...aRecords);
7388
- } catch {
7389
- }
7390
- try {
7391
- const aaaaRecords = await resolver.resolve6(hostname);
7392
- resolvedIPs.push(...aaaaRecords);
7393
- } catch {
7394
- }
7395
- if (resolvedIPs.length === 0) {
7396
- return {
7397
- isValid: false,
7398
- error: "DNS resolution failed: hostname did not resolve to any address."
7399
- };
7400
- }
7401
- for (const ip of resolvedIPs) {
7402
- if (isBlockedIP(ip)) {
7403
- return {
7404
- isValid: false,
7405
- error: "Hostname resolves to a private/internal IP address."
7406
- };
7407
- }
7408
- }
7409
- resolvedIP = resolvedIPs[0];
7410
- } catch {
7411
- return {
7412
- isValid: false,
7413
- error: "DNS resolution failed. Cannot verify hostname safety."
7414
- };
7415
- }
7416
- }
7417
- return { isValid: true, sanitizedUrl: url.toString(), resolvedIP };
7418
- } catch (error) {
7419
- return {
7420
- isValid: false,
7421
- error: `Invalid URL format: ${error instanceof Error ? error.message : "Unknown error"}`
7422
- };
7423
- }
7424
- }
7425
-
7426
- // src/tools/screenshot.ts
7427
7749
  init_supabase();
7428
7750
  function registerScreenshotTools(server2) {
7429
7751
  server2.tool(
@@ -7999,6 +8321,8 @@ function registerRemotionTools(server2) {
7999
8321
  // src/tools/insights.ts
8000
8322
  init_supabase();
8001
8323
  import { z as z8 } from "zod";
8324
+ init_version();
8325
+ var MAX_INSIGHT_AGE_DAYS = 30;
8002
8326
  var PLATFORM_ENUM = [
8003
8327
  "youtube",
8004
8328
  "tiktok",
@@ -8009,11 +8333,19 @@ var PLATFORM_ENUM = [
8009
8333
  "threads",
8010
8334
  "bluesky"
8011
8335
  ];
8012
- var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
8336
+ var DAY_NAMES = [
8337
+ "Sunday",
8338
+ "Monday",
8339
+ "Tuesday",
8340
+ "Wednesday",
8341
+ "Thursday",
8342
+ "Friday",
8343
+ "Saturday"
8344
+ ];
8013
8345
  function asEnvelope5(data) {
8014
8346
  return {
8015
8347
  _meta: {
8016
- version: "0.2.0",
8348
+ version: MCP_VERSION,
8017
8349
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
8018
8350
  },
8019
8351
  data
@@ -8024,7 +8356,12 @@ function registerInsightsTools(server2) {
8024
8356
  "get_performance_insights",
8025
8357
  "Query performance insights derived from post analytics. Returns metrics like engagement rate, view velocity, and click rate aggregated over time. Use this to understand what content is performing well.",
8026
8358
  {
8027
- insight_type: z8.enum(["top_hooks", "optimal_timing", "best_models", "competitor_patterns"]).optional().describe("Filter to a specific insight type."),
8359
+ insight_type: z8.enum([
8360
+ "top_hooks",
8361
+ "optimal_timing",
8362
+ "best_models",
8363
+ "competitor_patterns"
8364
+ ]).optional().describe("Filter to a specific insight type."),
8028
8365
  days: z8.number().min(1).max(90).optional().describe("Number of days to look back. Defaults to 30. Max 90."),
8029
8366
  limit: z8.number().min(1).max(50).optional().describe("Maximum number of insights to return. Defaults to 10."),
8030
8367
  response_format: z8.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
@@ -8043,10 +8380,13 @@ function registerInsightsTools(server2) {
8043
8380
  projectIds.push(...projects.map((p) => p.id));
8044
8381
  }
8045
8382
  }
8383
+ const effectiveDays = Math.min(lookbackDays, MAX_INSIGHT_AGE_DAYS);
8046
8384
  const since = /* @__PURE__ */ new Date();
8047
- since.setDate(since.getDate() - lookbackDays);
8385
+ since.setDate(since.getDate() - effectiveDays);
8048
8386
  const sinceIso = since.toISOString();
8049
- let query = supabase.from("performance_insights").select("id, project_id, insight_type, insight_data, confidence_score, generated_at").gte("generated_at", sinceIso).order("generated_at", { ascending: false }).limit(maxRows);
8387
+ let query = supabase.from("performance_insights").select(
8388
+ "id, project_id, insight_type, insight_data, confidence_score, generated_at"
8389
+ ).gte("generated_at", sinceIso).order("generated_at", { ascending: false }).limit(maxRows);
8050
8390
  if (projectIds.length > 0) {
8051
8391
  query = query.in("project_id", projectIds);
8052
8392
  } else {
@@ -8402,10 +8742,11 @@ function registerYouTubeAnalyticsTools(server2) {
8402
8742
  init_edge_function();
8403
8743
  import { z as z10 } from "zod";
8404
8744
  init_supabase();
8745
+ init_version();
8405
8746
  function asEnvelope6(data) {
8406
8747
  return {
8407
8748
  _meta: {
8408
- version: "0.2.0",
8749
+ version: MCP_VERSION,
8409
8750
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
8410
8751
  },
8411
8752
  data
@@ -8416,7 +8757,9 @@ function registerCommentsTools(server2) {
8416
8757
  "list_comments",
8417
8758
  "List YouTube comments. Without a video_id, returns recent comments across all channel videos. With a video_id, returns comments for that specific video.",
8418
8759
  {
8419
- video_id: z10.string().optional().describe("YouTube video ID. If omitted, returns comments across all channel videos."),
8760
+ video_id: z10.string().optional().describe(
8761
+ "YouTube video ID. If omitted, returns comments across all channel videos."
8762
+ ),
8420
8763
  max_results: z10.number().min(1).max(100).optional().describe("Maximum number of comments to return. Defaults to 50."),
8421
8764
  page_token: z10.string().optional().describe("Pagination token from a previous list_comments call."),
8422
8765
  response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
@@ -8431,7 +8774,9 @@ function registerCommentsTools(server2) {
8431
8774
  });
8432
8775
  if (error) {
8433
8776
  return {
8434
- content: [{ type: "text", text: `Error listing comments: ${error}` }],
8777
+ content: [
8778
+ { type: "text", text: `Error listing comments: ${error}` }
8779
+ ],
8435
8780
  isError: true
8436
8781
  };
8437
8782
  }
@@ -8443,7 +8788,10 @@ function registerCommentsTools(server2) {
8443
8788
  {
8444
8789
  type: "text",
8445
8790
  text: JSON.stringify(
8446
- asEnvelope6({ comments, nextPageToken: result.nextPageToken ?? null }),
8791
+ asEnvelope6({
8792
+ comments,
8793
+ nextPageToken: result.nextPageToken ?? null
8794
+ }),
8447
8795
  null,
8448
8796
  2
8449
8797
  )
@@ -8480,7 +8828,9 @@ function registerCommentsTools(server2) {
8480
8828
  "reply_to_comment",
8481
8829
  "Reply to a YouTube comment. Requires the parent comment ID and reply text.",
8482
8830
  {
8483
- parent_id: z10.string().describe("The ID of the parent comment to reply to (from list_comments)."),
8831
+ parent_id: z10.string().describe(
8832
+ "The ID of the parent comment to reply to (from list_comments)."
8833
+ ),
8484
8834
  text: z10.string().min(1).describe("The reply text."),
8485
8835
  response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8486
8836
  },
@@ -8519,7 +8869,12 @@ function registerCommentsTools(server2) {
8519
8869
  details: { error }
8520
8870
  });
8521
8871
  return {
8522
- content: [{ type: "text", text: `Error replying to comment: ${error}` }],
8872
+ content: [
8873
+ {
8874
+ type: "text",
8875
+ text: `Error replying to comment: ${error}`
8876
+ }
8877
+ ],
8523
8878
  isError: true
8524
8879
  };
8525
8880
  }
@@ -8532,7 +8887,12 @@ function registerCommentsTools(server2) {
8532
8887
  });
8533
8888
  if (format === "json") {
8534
8889
  return {
8535
- content: [{ type: "text", text: JSON.stringify(asEnvelope6(result), null, 2) }]
8890
+ content: [
8891
+ {
8892
+ type: "text",
8893
+ text: JSON.stringify(asEnvelope6(result), null, 2)
8894
+ }
8895
+ ]
8536
8896
  };
8537
8897
  }
8538
8898
  return {
@@ -8590,7 +8950,9 @@ function registerCommentsTools(server2) {
8590
8950
  details: { error }
8591
8951
  });
8592
8952
  return {
8593
- content: [{ type: "text", text: `Error posting comment: ${error}` }],
8953
+ content: [
8954
+ { type: "text", text: `Error posting comment: ${error}` }
8955
+ ],
8594
8956
  isError: true
8595
8957
  };
8596
8958
  }
@@ -8603,7 +8965,12 @@ function registerCommentsTools(server2) {
8603
8965
  });
8604
8966
  if (format === "json") {
8605
8967
  return {
8606
- content: [{ type: "text", text: JSON.stringify(asEnvelope6(result), null, 2) }]
8968
+ content: [
8969
+ {
8970
+ type: "text",
8971
+ text: JSON.stringify(asEnvelope6(result), null, 2)
8972
+ }
8973
+ ]
8607
8974
  };
8608
8975
  }
8609
8976
  return {
@@ -8661,7 +9028,12 @@ function registerCommentsTools(server2) {
8661
9028
  details: { error }
8662
9029
  });
8663
9030
  return {
8664
- content: [{ type: "text", text: `Error moderating comment: ${error}` }],
9031
+ content: [
9032
+ {
9033
+ type: "text",
9034
+ text: `Error moderating comment: ${error}`
9035
+ }
9036
+ ],
8665
9037
  isError: true
8666
9038
  };
8667
9039
  }
@@ -8740,7 +9112,9 @@ function registerCommentsTools(server2) {
8740
9112
  details: { error }
8741
9113
  });
8742
9114
  return {
8743
- content: [{ type: "text", text: `Error deleting comment: ${error}` }],
9115
+ content: [
9116
+ { type: "text", text: `Error deleting comment: ${error}` }
9117
+ ],
8744
9118
  isError: true
8745
9119
  };
8746
9120
  }
@@ -8755,13 +9129,22 @@ function registerCommentsTools(server2) {
8755
9129
  content: [
8756
9130
  {
8757
9131
  type: "text",
8758
- text: JSON.stringify(asEnvelope6({ success: true, commentId: comment_id }), null, 2)
9132
+ text: JSON.stringify(
9133
+ asEnvelope6({ success: true, commentId: comment_id }),
9134
+ null,
9135
+ 2
9136
+ )
8759
9137
  }
8760
9138
  ]
8761
9139
  };
8762
9140
  }
8763
9141
  return {
8764
- content: [{ type: "text", text: `Comment ${comment_id} deleted successfully.` }]
9142
+ content: [
9143
+ {
9144
+ type: "text",
9145
+ text: `Comment ${comment_id} deleted successfully.`
9146
+ }
9147
+ ]
8765
9148
  };
8766
9149
  }
8767
9150
  );
@@ -8770,6 +9153,7 @@ function registerCommentsTools(server2) {
8770
9153
  // src/tools/ideation-context.ts
8771
9154
  init_supabase();
8772
9155
  import { z as z11 } from "zod";
9156
+ init_version();
8773
9157
  function transformInsightsToPerformanceContext(projectId, insights) {
8774
9158
  if (!insights.length) {
8775
9159
  return {
@@ -8789,8 +9173,12 @@ function transformInsightsToPerformanceContext(projectId, insights) {
8789
9173
  };
8790
9174
  }
8791
9175
  const topHooksInsight = insights.find((i) => i.insight_type === "top_hooks");
8792
- const optimalTimingInsight = insights.find((i) => i.insight_type === "optimal_timing");
8793
- const bestModelsInsight = insights.find((i) => i.insight_type === "best_models");
9176
+ const optimalTimingInsight = insights.find(
9177
+ (i) => i.insight_type === "optimal_timing"
9178
+ );
9179
+ const bestModelsInsight = insights.find(
9180
+ (i) => i.insight_type === "best_models"
9181
+ );
8794
9182
  const topHooks = topHooksInsight?.insight_data?.hooks || [];
8795
9183
  const hooksSummary = topHooksInsight?.insight_data?.summary || "";
8796
9184
  const timingSummary = optimalTimingInsight?.insight_data?.summary || "";
@@ -8801,7 +9189,10 @@ function transformInsightsToPerformanceContext(projectId, insights) {
8801
9189
  if (hooksSummary) promptParts.push(hooksSummary);
8802
9190
  if (timingSummary) promptParts.push(timingSummary);
8803
9191
  if (modelSummary) promptParts.push(modelSummary);
8804
- if (topHooks.length) promptParts.push(`Top performing hooks: ${topHooks.slice(0, 3).join(", ")}`);
9192
+ if (topHooks.length)
9193
+ promptParts.push(
9194
+ `Top performing hooks: ${topHooks.slice(0, 3).join(", ")}`
9195
+ );
8805
9196
  return {
8806
9197
  projectId,
8807
9198
  hasHistoricalData: true,
@@ -8827,7 +9218,7 @@ function transformInsightsToPerformanceContext(projectId, insights) {
8827
9218
  function asEnvelope7(data) {
8828
9219
  return {
8829
9220
  _meta: {
8830
- version: "0.2.0",
9221
+ version: MCP_VERSION,
8831
9222
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
8832
9223
  },
8833
9224
  data
@@ -8850,7 +9241,12 @@ function registerIdeationContextTools(server2) {
8850
9241
  const { data: member } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).limit(1).single();
8851
9242
  if (!member?.organization_id) {
8852
9243
  return {
8853
- content: [{ type: "text", text: "No organization found for current user." }],
9244
+ content: [
9245
+ {
9246
+ type: "text",
9247
+ text: "No organization found for current user."
9248
+ }
9249
+ ],
8854
9250
  isError: true
8855
9251
  };
8856
9252
  }
@@ -8858,7 +9254,10 @@ function registerIdeationContextTools(server2) {
8858
9254
  if (projectsError) {
8859
9255
  return {
8860
9256
  content: [
8861
- { type: "text", text: `Failed to resolve projects: ${projectsError.message}` }
9257
+ {
9258
+ type: "text",
9259
+ text: `Failed to resolve projects: ${projectsError.message}`
9260
+ }
8862
9261
  ],
8863
9262
  isError: true
8864
9263
  };
@@ -8866,7 +9265,12 @@ function registerIdeationContextTools(server2) {
8866
9265
  const projectIds = (projects || []).map((p) => p.id);
8867
9266
  if (projectIds.length === 0) {
8868
9267
  return {
8869
- content: [{ type: "text", text: "No projects found for current user." }],
9268
+ content: [
9269
+ {
9270
+ type: "text",
9271
+ text: "No projects found for current user."
9272
+ }
9273
+ ],
8870
9274
  isError: true
8871
9275
  };
8872
9276
  }
@@ -8875,7 +9279,10 @@ function registerIdeationContextTools(server2) {
8875
9279
  if (!selectedProjectId) {
8876
9280
  return {
8877
9281
  content: [
8878
- { type: "text", text: "No accessible project found for current user." }
9282
+ {
9283
+ type: "text",
9284
+ text: "No accessible project found for current user."
9285
+ }
8879
9286
  ],
8880
9287
  isError: true
8881
9288
  };
@@ -8893,7 +9300,9 @@ function registerIdeationContextTools(server2) {
8893
9300
  }
8894
9301
  const since = /* @__PURE__ */ new Date();
8895
9302
  since.setDate(since.getDate() - lookbackDays);
8896
- const { data: insights, error } = await supabase.from("performance_insights").select("id, project_id, insight_type, insight_data, generated_at, expires_at").eq("project_id", selectedProjectId).gte("generated_at", since.toISOString()).gt("expires_at", (/* @__PURE__ */ new Date()).toISOString()).order("generated_at", { ascending: false }).limit(30);
9303
+ const { data: insights, error } = await supabase.from("performance_insights").select(
9304
+ "id, project_id, insight_type, insight_data, generated_at, expires_at"
9305
+ ).eq("project_id", selectedProjectId).gte("generated_at", since.toISOString()).gt("expires_at", (/* @__PURE__ */ new Date()).toISOString()).order("generated_at", { ascending: false }).limit(30);
8897
9306
  if (error) {
8898
9307
  return {
8899
9308
  content: [
@@ -8911,7 +9320,12 @@ function registerIdeationContextTools(server2) {
8911
9320
  );
8912
9321
  if (format === "json") {
8913
9322
  return {
8914
- content: [{ type: "text", text: JSON.stringify(asEnvelope7(context), null, 2) }]
9323
+ content: [
9324
+ {
9325
+ type: "text",
9326
+ text: JSON.stringify(asEnvelope7(context), null, 2)
9327
+ }
9328
+ ]
8915
9329
  };
8916
9330
  }
8917
9331
  const lines = [
@@ -8932,10 +9346,11 @@ function registerIdeationContextTools(server2) {
8932
9346
  // src/tools/credits.ts
8933
9347
  init_supabase();
8934
9348
  import { z as z12 } from "zod";
9349
+ init_version();
8935
9350
  function asEnvelope8(data) {
8936
9351
  return {
8937
9352
  _meta: {
8938
- version: "0.2.0",
9353
+ version: MCP_VERSION,
8939
9354
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
8940
9355
  },
8941
9356
  data
@@ -8974,7 +9389,12 @@ function registerCreditsTools(server2) {
8974
9389
  };
8975
9390
  if ((response_format || "text") === "json") {
8976
9391
  return {
8977
- content: [{ type: "text", text: JSON.stringify(asEnvelope8(payload), null, 2) }]
9392
+ content: [
9393
+ {
9394
+ type: "text",
9395
+ text: JSON.stringify(asEnvelope8(payload), null, 2)
9396
+ }
9397
+ ]
8978
9398
  };
8979
9399
  }
8980
9400
  return {
@@ -9008,7 +9428,12 @@ Monthly used: ${payload.monthlyUsed}` + (payload.monthlyLimit ? ` / ${payload.mo
9008
9428
  };
9009
9429
  if ((response_format || "text") === "json") {
9010
9430
  return {
9011
- content: [{ type: "text", text: JSON.stringify(asEnvelope8(payload), null, 2) }]
9431
+ content: [
9432
+ {
9433
+ type: "text",
9434
+ text: JSON.stringify(asEnvelope8(payload), null, 2)
9435
+ }
9436
+ ]
9012
9437
  };
9013
9438
  }
9014
9439
  return {
@@ -9031,11 +9456,12 @@ Assets remaining: ${payload.remainingAssets ?? "unlimited"}`
9031
9456
 
9032
9457
  // src/tools/loop-summary.ts
9033
9458
  init_supabase();
9459
+ init_version();
9034
9460
  import { z as z13 } from "zod";
9035
9461
  function asEnvelope9(data) {
9036
9462
  return {
9037
9463
  _meta: {
9038
- version: "0.2.0",
9464
+ version: MCP_VERSION,
9039
9465
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
9040
9466
  },
9041
9467
  data
@@ -9074,7 +9500,12 @@ function registerLoopSummaryTools(server2) {
9074
9500
  const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
9075
9501
  if (!membership) {
9076
9502
  return {
9077
- content: [{ type: "text", text: "Project is not accessible to current user." }],
9503
+ content: [
9504
+ {
9505
+ type: "text",
9506
+ text: "Project is not accessible to current user."
9507
+ }
9508
+ ],
9078
9509
  isError: true
9079
9510
  };
9080
9511
  }
@@ -9097,7 +9528,12 @@ function registerLoopSummaryTools(server2) {
9097
9528
  };
9098
9529
  if ((response_format || "text") === "json") {
9099
9530
  return {
9100
- content: [{ type: "text", text: JSON.stringify(asEnvelope9(payload), null, 2) }]
9531
+ content: [
9532
+ {
9533
+ type: "text",
9534
+ text: JSON.stringify(asEnvelope9(payload), null, 2)
9535
+ }
9536
+ ]
9101
9537
  };
9102
9538
  }
9103
9539
  return {
@@ -9427,8 +9863,12 @@ ${"=".repeat(40)}
9427
9863
  init_edge_function();
9428
9864
  init_supabase();
9429
9865
  import { z as z16 } from "zod";
9866
+ init_version();
9430
9867
  function asEnvelope12(data) {
9431
- return { _meta: { version: "0.2.0", timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
9868
+ return {
9869
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
9870
+ data
9871
+ };
9432
9872
  }
9433
9873
  function isYouTubeUrl(url) {
9434
9874
  if (/youtube\.com\/watch|youtu\.be\//.test(url)) return "video";
@@ -9459,13 +9899,17 @@ Metadata:`);
9459
9899
  if (m.tags?.length) lines.push(` Tags: ${m.tags.join(", ")}`);
9460
9900
  }
9461
9901
  if (content.features?.length)
9462
- lines.push(`
9902
+ lines.push(
9903
+ `
9463
9904
  Features:
9464
- ${content.features.map((f) => ` - ${f}`).join("\n")}`);
9905
+ ${content.features.map((f) => ` - ${f}`).join("\n")}`
9906
+ );
9465
9907
  if (content.benefits?.length)
9466
- lines.push(`
9908
+ lines.push(
9909
+ `
9467
9910
  Benefits:
9468
- ${content.benefits.map((b) => ` - ${b}`).join("\n")}`);
9911
+ ${content.benefits.map((b) => ` - ${b}`).join("\n")}`
9912
+ );
9469
9913
  if (content.usp) lines.push(`
9470
9914
  USP: ${content.usp}`);
9471
9915
  if (content.suggested_hooks?.length)
@@ -9487,12 +9931,20 @@ function registerExtractionTools(server2) {
9487
9931
  max_results: z16.number().min(1).max(100).default(10).describe("Max comments to include"),
9488
9932
  response_format: z16.enum(["text", "json"]).default("text")
9489
9933
  },
9490
- async ({ url, extract_type, include_comments, max_results, response_format }) => {
9934
+ async ({
9935
+ url,
9936
+ extract_type,
9937
+ include_comments,
9938
+ max_results,
9939
+ response_format
9940
+ }) => {
9491
9941
  const startedAt = Date.now();
9492
9942
  const ssrfCheck = await validateUrlForSSRF(url);
9493
9943
  if (!ssrfCheck.isValid) {
9494
9944
  return {
9495
- content: [{ type: "text", text: `URL blocked: ${ssrfCheck.error}` }],
9945
+ content: [
9946
+ { type: "text", text: `URL blocked: ${ssrfCheck.error}` }
9947
+ ],
9496
9948
  isError: true
9497
9949
  };
9498
9950
  }
@@ -9620,13 +10072,21 @@ function registerExtractionTools(server2) {
9620
10072
  if (response_format === "json") {
9621
10073
  return {
9622
10074
  content: [
9623
- { type: "text", text: JSON.stringify(asEnvelope12(extracted), null, 2) }
10075
+ {
10076
+ type: "text",
10077
+ text: JSON.stringify(asEnvelope12(extracted), null, 2)
10078
+ }
9624
10079
  ],
9625
10080
  isError: false
9626
10081
  };
9627
10082
  }
9628
10083
  return {
9629
- content: [{ type: "text", text: formatExtractedContentAsText(extracted) }],
10084
+ content: [
10085
+ {
10086
+ type: "text",
10087
+ text: formatExtractedContentAsText(extracted)
10088
+ }
10089
+ ],
9630
10090
  isError: false
9631
10091
  };
9632
10092
  } catch (err) {
@@ -9639,7 +10099,9 @@ function registerExtractionTools(server2) {
9639
10099
  details: { url, error: message }
9640
10100
  });
9641
10101
  return {
9642
- content: [{ type: "text", text: `Extraction failed: ${message}` }],
10102
+ content: [
10103
+ { type: "text", text: `Extraction failed: ${message}` }
10104
+ ],
9643
10105
  isError: true
9644
10106
  };
9645
10107
  }
@@ -9650,9 +10112,13 @@ function registerExtractionTools(server2) {
9650
10112
  // src/tools/quality.ts
9651
10113
  init_quality();
9652
10114
  init_supabase();
10115
+ init_version();
9653
10116
  import { z as z17 } from "zod";
9654
10117
  function asEnvelope13(data) {
9655
- return { _meta: { version: "0.2.0", timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
10118
+ return {
10119
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
10120
+ data
10121
+ };
9656
10122
  }
9657
10123
  function registerQualityTools(server2) {
9658
10124
  server2.tool(
@@ -9708,7 +10174,12 @@ function registerQualityTools(server2) {
9708
10174
  });
9709
10175
  if (response_format === "json") {
9710
10176
  return {
9711
- content: [{ type: "text", text: JSON.stringify(asEnvelope13(result), null, 2) }],
10177
+ content: [
10178
+ {
10179
+ type: "text",
10180
+ text: JSON.stringify(asEnvelope13(result), null, 2)
10181
+ }
10182
+ ],
9712
10183
  isError: false
9713
10184
  };
9714
10185
  }
@@ -9718,7 +10189,9 @@ function registerQualityTools(server2) {
9718
10189
  );
9719
10190
  lines.push("");
9720
10191
  for (const cat of result.categories) {
9721
- lines.push(` ${cat.name}: ${cat.score}/${cat.maxScore} \u2014 ${cat.detail}`);
10192
+ lines.push(
10193
+ ` ${cat.name}: ${cat.score}/${cat.maxScore} \u2014 ${cat.detail}`
10194
+ );
9722
10195
  }
9723
10196
  if (result.blockers.length > 0) {
9724
10197
  lines.push("");
@@ -9729,7 +10202,10 @@ function registerQualityTools(server2) {
9729
10202
  }
9730
10203
  lines.push("");
9731
10204
  lines.push(`Threshold: ${result.threshold}/${result.maxTotal}`);
9732
- return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
10205
+ return {
10206
+ content: [{ type: "text", text: lines.join("\n") }],
10207
+ isError: false
10208
+ };
9733
10209
  }
9734
10210
  );
9735
10211
  server2.tool(
@@ -9770,7 +10246,9 @@ function registerQualityTools(server2) {
9770
10246
  });
9771
10247
  const scores = postsWithQuality.map((p) => p.quality.score);
9772
10248
  const passed = postsWithQuality.filter((p) => p.quality.passed).length;
9773
- const avgScore = scores.length > 0 ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length * 10) / 10 : 0;
10249
+ const avgScore = scores.length > 0 ? Math.round(
10250
+ scores.reduce((a, b) => a + b, 0) / scores.length * 10
10251
+ ) / 10 : 0;
9774
10252
  const summary = {
9775
10253
  total_posts: plan.posts.length,
9776
10254
  passed,
@@ -9789,25 +10267,36 @@ function registerQualityTools(server2) {
9789
10267
  content: [
9790
10268
  {
9791
10269
  type: "text",
9792
- text: JSON.stringify(asEnvelope13({ posts: postsWithQuality, summary }), null, 2)
10270
+ text: JSON.stringify(
10271
+ asEnvelope13({ posts: postsWithQuality, summary }),
10272
+ null,
10273
+ 2
10274
+ )
9793
10275
  }
9794
10276
  ],
9795
10277
  isError: false
9796
10278
  };
9797
10279
  }
9798
10280
  const lines = [];
9799
- lines.push(`PLAN QUALITY: ${passed}/${plan.posts.length} passed (avg: ${avgScore}/35)`);
10281
+ lines.push(
10282
+ `PLAN QUALITY: ${passed}/${plan.posts.length} passed (avg: ${avgScore}/35)`
10283
+ );
9800
10284
  lines.push("");
9801
10285
  for (const post of postsWithQuality) {
9802
10286
  const icon = post.quality.passed ? "[PASS]" : "[FAIL]";
9803
- lines.push(`${icon} ${post.id} | ${post.platform} | ${post.quality.score}/35`);
10287
+ lines.push(
10288
+ `${icon} ${post.id} | ${post.platform} | ${post.quality.score}/35`
10289
+ );
9804
10290
  if (post.quality.blockers.length > 0) {
9805
10291
  for (const b of post.quality.blockers) {
9806
10292
  lines.push(` - ${b}`);
9807
10293
  }
9808
10294
  }
9809
10295
  }
9810
- return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
10296
+ return {
10297
+ content: [{ type: "text", text: lines.join("\n") }],
10298
+ isError: false
10299
+ };
9811
10300
  }
9812
10301
  );
9813
10302
  }
@@ -9817,11 +10306,15 @@ init_edge_function();
9817
10306
  init_supabase();
9818
10307
  import { z as z18 } from "zod";
9819
10308
  import { randomUUID as randomUUID2 } from "node:crypto";
10309
+ init_version();
9820
10310
  function toRecord(value) {
9821
10311
  return value && typeof value === "object" ? value : void 0;
9822
10312
  }
9823
10313
  function asEnvelope14(data) {
9824
- return { _meta: { version: "0.2.0", timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
10314
+ return {
10315
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
10316
+ data
10317
+ };
9825
10318
  }
9826
10319
  function tomorrowIsoDate() {
9827
10320
  const d = /* @__PURE__ */ new Date();
@@ -9859,7 +10352,9 @@ function formatPlanAsText(plan) {
9859
10352
  lines.push(`WEEKLY CONTENT PLAN: "${plan.topic}"`);
9860
10353
  lines.push(`Period: ${plan.start_date} to ${plan.end_date}`);
9861
10354
  lines.push(`Platforms: ${plan.platforms.join(", ")}`);
9862
- lines.push(`Posts: ${plan.posts.length} | Estimated credits: ~${plan.estimated_credits}`);
10355
+ lines.push(
10356
+ `Posts: ${plan.posts.length} | Estimated credits: ~${plan.estimated_credits}`
10357
+ );
9863
10358
  if (plan.plan_id) lines.push(`Plan ID: ${plan.plan_id}`);
9864
10359
  if (plan.insights_applied?.has_historical_data) {
9865
10360
  lines.push("");
@@ -9874,7 +10369,9 @@ function formatPlanAsText(plan) {
9874
10369
  `- Best posting time: ${days[timing.dayOfWeek] ?? timing.dayOfWeek} ${timing.hourOfDay}:00`
9875
10370
  );
9876
10371
  }
9877
- lines.push(`- Recommended model: ${plan.insights_applied.recommended_model ?? "N/A"}`);
10372
+ lines.push(
10373
+ `- Recommended model: ${plan.insights_applied.recommended_model ?? "N/A"}`
10374
+ );
9878
10375
  lines.push(`- Insights count: ${plan.insights_applied.insights_count}`);
9879
10376
  }
9880
10377
  lines.push("");
@@ -9895,9 +10392,11 @@ function formatPlanAsText(plan) {
9895
10392
  ` Caption: ${post.caption.slice(0, 200)}${post.caption.length > 200 ? "..." : ""}`
9896
10393
  );
9897
10394
  if (post.title) lines.push(` Title: ${post.title}`);
9898
- if (post.visual_direction) lines.push(` Visual: ${post.visual_direction}`);
10395
+ if (post.visual_direction)
10396
+ lines.push(` Visual: ${post.visual_direction}`);
9899
10397
  if (post.media_type) lines.push(` Media: ${post.media_type}`);
9900
- if (post.hashtags?.length) lines.push(` Hashtags: ${post.hashtags.join(" ")}`);
10398
+ if (post.hashtags?.length)
10399
+ lines.push(` Hashtags: ${post.hashtags.join(" ")}`);
9901
10400
  lines.push("");
9902
10401
  }
9903
10402
  }
@@ -9980,7 +10479,10 @@ function registerPlanningTools(server2) {
9980
10479
  "mcp-data",
9981
10480
  {
9982
10481
  action: "brand-profile",
9983
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
10482
+ ...resolvedProjectId ? {
10483
+ projectId: resolvedProjectId,
10484
+ project_id: resolvedProjectId
10485
+ } : {}
9984
10486
  },
9985
10487
  { timeoutMs: 15e3 }
9986
10488
  );
@@ -10184,7 +10686,12 @@ ${rawText.slice(0, 1e3)}`
10184
10686
  details: { topic, error: `plan persistence failed: ${message}` }
10185
10687
  });
10186
10688
  return {
10187
- content: [{ type: "text", text: `Plan persistence failed: ${message}` }],
10689
+ content: [
10690
+ {
10691
+ type: "text",
10692
+ text: `Plan persistence failed: ${message}`
10693
+ }
10694
+ ],
10188
10695
  isError: true
10189
10696
  };
10190
10697
  }
@@ -10198,7 +10705,12 @@ ${rawText.slice(0, 1e3)}`
10198
10705
  });
10199
10706
  if (response_format === "json") {
10200
10707
  return {
10201
- content: [{ type: "text", text: JSON.stringify(asEnvelope14(plan), null, 2) }],
10708
+ content: [
10709
+ {
10710
+ type: "text",
10711
+ text: JSON.stringify(asEnvelope14(plan), null, 2)
10712
+ }
10713
+ ],
10202
10714
  isError: false
10203
10715
  };
10204
10716
  }
@@ -10216,7 +10728,12 @@ ${rawText.slice(0, 1e3)}`
10216
10728
  details: { topic, error: message }
10217
10729
  });
10218
10730
  return {
10219
- content: [{ type: "text", text: `Plan generation failed: ${message}` }],
10731
+ content: [
10732
+ {
10733
+ type: "text",
10734
+ text: `Plan generation failed: ${message}`
10735
+ }
10736
+ ],
10220
10737
  isError: true
10221
10738
  };
10222
10739
  }
@@ -10276,7 +10793,11 @@ ${rawText.slice(0, 1e3)}`
10276
10793
  toolName: "save_content_plan",
10277
10794
  status: "success",
10278
10795
  durationMs,
10279
- details: { plan_id: planId, project_id: resolvedProjectId, status: normalizedStatus }
10796
+ details: {
10797
+ plan_id: planId,
10798
+ project_id: resolvedProjectId,
10799
+ status: normalizedStatus
10800
+ }
10280
10801
  });
10281
10802
  const result = {
10282
10803
  plan_id: planId,
@@ -10285,13 +10806,21 @@ ${rawText.slice(0, 1e3)}`
10285
10806
  };
10286
10807
  if (response_format === "json") {
10287
10808
  return {
10288
- content: [{ type: "text", text: JSON.stringify(asEnvelope14(result), null, 2) }],
10809
+ content: [
10810
+ {
10811
+ type: "text",
10812
+ text: JSON.stringify(asEnvelope14(result), null, 2)
10813
+ }
10814
+ ],
10289
10815
  isError: false
10290
10816
  };
10291
10817
  }
10292
10818
  return {
10293
10819
  content: [
10294
- { type: "text", text: `Saved content plan ${planId} (${normalizedStatus}).` }
10820
+ {
10821
+ type: "text",
10822
+ text: `Saved content plan ${planId} (${normalizedStatus}).`
10823
+ }
10295
10824
  ],
10296
10825
  isError: false
10297
10826
  };
@@ -10305,7 +10834,12 @@ ${rawText.slice(0, 1e3)}`
10305
10834
  details: { error: message }
10306
10835
  });
10307
10836
  return {
10308
- content: [{ type: "text", text: `Failed to save content plan: ${message}` }],
10837
+ content: [
10838
+ {
10839
+ type: "text",
10840
+ text: `Failed to save content plan: ${message}`
10841
+ }
10842
+ ],
10309
10843
  isError: true
10310
10844
  };
10311
10845
  }
@@ -10321,7 +10855,9 @@ ${rawText.slice(0, 1e3)}`
10321
10855
  async ({ plan_id, response_format }) => {
10322
10856
  const supabase = getSupabaseClient();
10323
10857
  const userId = await getDefaultUserId();
10324
- const { data, error } = await supabase.from("content_plans").select("id, topic, status, plan_payload, insights_applied, created_at, updated_at").eq("id", plan_id).eq("user_id", userId).maybeSingle();
10858
+ const { data, error } = await supabase.from("content_plans").select(
10859
+ "id, topic, status, plan_payload, insights_applied, created_at, updated_at"
10860
+ ).eq("id", plan_id).eq("user_id", userId).maybeSingle();
10325
10861
  if (error) {
10326
10862
  return {
10327
10863
  content: [
@@ -10336,7 +10872,10 @@ ${rawText.slice(0, 1e3)}`
10336
10872
  if (!data) {
10337
10873
  return {
10338
10874
  content: [
10339
- { type: "text", text: `No content plan found for plan_id=${plan_id}` }
10875
+ {
10876
+ type: "text",
10877
+ text: `No content plan found for plan_id=${plan_id}`
10878
+ }
10340
10879
  ],
10341
10880
  isError: true
10342
10881
  };
@@ -10352,7 +10891,12 @@ ${rawText.slice(0, 1e3)}`
10352
10891
  };
10353
10892
  if (response_format === "json") {
10354
10893
  return {
10355
- content: [{ type: "text", text: JSON.stringify(asEnvelope14(payload), null, 2) }],
10894
+ content: [
10895
+ {
10896
+ type: "text",
10897
+ text: JSON.stringify(asEnvelope14(payload), null, 2)
10898
+ }
10899
+ ],
10356
10900
  isError: false
10357
10901
  };
10358
10902
  }
@@ -10363,7 +10907,10 @@ ${rawText.slice(0, 1e3)}`
10363
10907
  `Status: ${data.status}`,
10364
10908
  `Posts: ${Array.isArray(plan?.posts) ? plan.posts.length : 0}`
10365
10909
  ];
10366
- return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
10910
+ return {
10911
+ content: [{ type: "text", text: lines.join("\n") }],
10912
+ isError: false
10913
+ };
10367
10914
  }
10368
10915
  );
10369
10916
  server2.tool(
@@ -10406,14 +10953,19 @@ ${rawText.slice(0, 1e3)}`
10406
10953
  if (!stored?.plan_payload) {
10407
10954
  return {
10408
10955
  content: [
10409
- { type: "text", text: `No content plan found for plan_id=${plan_id}` }
10956
+ {
10957
+ type: "text",
10958
+ text: `No content plan found for plan_id=${plan_id}`
10959
+ }
10410
10960
  ],
10411
10961
  isError: true
10412
10962
  };
10413
10963
  }
10414
10964
  const plan = stored.plan_payload;
10415
10965
  const existingPosts = Array.isArray(plan.posts) ? plan.posts : [];
10416
- const updatesById = new Map(post_updates.map((update) => [update.post_id, update]));
10966
+ const updatesById = new Map(
10967
+ post_updates.map((update) => [update.post_id, update])
10968
+ );
10417
10969
  const updatedPosts = existingPosts.map((post) => {
10418
10970
  const update = updatesById.get(post.id);
10419
10971
  if (!update) return post;
@@ -10431,7 +10983,9 @@ ${rawText.slice(0, 1e3)}`
10431
10983
  ...update.status !== void 0 ? { status: update.status } : {}
10432
10984
  };
10433
10985
  });
10434
- const nextStatus = updatedPosts.length > 0 && updatedPosts.every((post) => post.status === "approved" || post.status === "edited") ? "approved" : stored.status === "scheduled" || stored.status === "completed" ? stored.status : "draft";
10986
+ const nextStatus = updatedPosts.length > 0 && updatedPosts.every(
10987
+ (post) => post.status === "approved" || post.status === "edited"
10988
+ ) ? "approved" : stored.status === "scheduled" || stored.status === "completed" ? stored.status : "draft";
10435
10989
  const updatedPlan = {
10436
10990
  ...plan,
10437
10991
  posts: updatedPosts
@@ -10458,7 +11012,12 @@ ${rawText.slice(0, 1e3)}`
10458
11012
  };
10459
11013
  if (response_format === "json") {
10460
11014
  return {
10461
- content: [{ type: "text", text: JSON.stringify(asEnvelope14(payload), null, 2) }],
11015
+ content: [
11016
+ {
11017
+ type: "text",
11018
+ text: JSON.stringify(asEnvelope14(payload), null, 2)
11019
+ }
11020
+ ],
10462
11021
  isError: false
10463
11022
  };
10464
11023
  }
@@ -10498,7 +11057,10 @@ ${rawText.slice(0, 1e3)}`
10498
11057
  if (!stored?.plan_payload || !stored.project_id) {
10499
11058
  return {
10500
11059
  content: [
10501
- { type: "text", text: `No content plan found for plan_id=${plan_id}` }
11060
+ {
11061
+ type: "text",
11062
+ text: `No content plan found for plan_id=${plan_id}`
11063
+ }
10502
11064
  ],
10503
11065
  isError: true
10504
11066
  };
@@ -10507,7 +11069,12 @@ ${rawText.slice(0, 1e3)}`
10507
11069
  const posts = Array.isArray(plan.posts) ? plan.posts : [];
10508
11070
  if (posts.length === 0) {
10509
11071
  return {
10510
- content: [{ type: "text", text: `Plan ${plan_id} has no posts to submit.` }],
11072
+ content: [
11073
+ {
11074
+ type: "text",
11075
+ text: `Plan ${plan_id} has no posts to submit.`
11076
+ }
11077
+ ],
10511
11078
  isError: true
10512
11079
  };
10513
11080
  }
@@ -10550,7 +11117,12 @@ ${rawText.slice(0, 1e3)}`
10550
11117
  };
10551
11118
  if (response_format === "json") {
10552
11119
  return {
10553
- content: [{ type: "text", text: JSON.stringify(asEnvelope14(payload), null, 2) }],
11120
+ content: [
11121
+ {
11122
+ type: "text",
11123
+ text: JSON.stringify(asEnvelope14(payload), null, 2)
11124
+ }
11125
+ ],
10554
11126
  isError: false
10555
11127
  };
10556
11128
  }
@@ -10570,10 +11142,11 @@ ${rawText.slice(0, 1e3)}`
10570
11142
  // src/tools/plan-approvals.ts
10571
11143
  init_supabase();
10572
11144
  import { z as z19 } from "zod";
11145
+ init_version();
10573
11146
  function asEnvelope15(data) {
10574
11147
  return {
10575
11148
  _meta: {
10576
- version: "0.2.0",
11149
+ version: MCP_VERSION,
10577
11150
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
10578
11151
  },
10579
11152
  data
@@ -10612,14 +11185,24 @@ function registerPlanApprovalTools(server2) {
10612
11185
  if (!projectId) {
10613
11186
  return {
10614
11187
  content: [
10615
- { type: "text", text: "No project_id provided and no default project found." }
11188
+ {
11189
+ type: "text",
11190
+ text: "No project_id provided and no default project found."
11191
+ }
10616
11192
  ],
10617
11193
  isError: true
10618
11194
  };
10619
11195
  }
10620
- const accessError = await assertProjectAccess(supabase, userId, projectId);
11196
+ const accessError = await assertProjectAccess(
11197
+ supabase,
11198
+ userId,
11199
+ projectId
11200
+ );
10621
11201
  if (accessError) {
10622
- return { content: [{ type: "text", text: accessError }], isError: true };
11202
+ return {
11203
+ content: [{ type: "text", text: accessError }],
11204
+ isError: true
11205
+ };
10623
11206
  }
10624
11207
  const rows = posts.map((post) => ({
10625
11208
  plan_id,
@@ -10648,7 +11231,12 @@ function registerPlanApprovalTools(server2) {
10648
11231
  };
10649
11232
  if ((response_format || "text") === "json") {
10650
11233
  return {
10651
- content: [{ type: "text", text: JSON.stringify(asEnvelope15(payload), null, 2) }],
11234
+ content: [
11235
+ {
11236
+ type: "text",
11237
+ text: JSON.stringify(asEnvelope15(payload), null, 2)
11238
+ }
11239
+ ],
10652
11240
  isError: false
10653
11241
  };
10654
11242
  }
@@ -10697,14 +11285,22 @@ function registerPlanApprovalTools(server2) {
10697
11285
  };
10698
11286
  if ((response_format || "text") === "json") {
10699
11287
  return {
10700
- content: [{ type: "text", text: JSON.stringify(asEnvelope15(payload), null, 2) }],
11288
+ content: [
11289
+ {
11290
+ type: "text",
11291
+ text: JSON.stringify(asEnvelope15(payload), null, 2)
11292
+ }
11293
+ ],
10701
11294
  isError: false
10702
11295
  };
10703
11296
  }
10704
11297
  if (!data || data.length === 0) {
10705
11298
  return {
10706
11299
  content: [
10707
- { type: "text", text: `No approval items found for plan ${plan_id}.` }
11300
+ {
11301
+ type: "text",
11302
+ text: `No approval items found for plan ${plan_id}.`
11303
+ }
10708
11304
  ],
10709
11305
  isError: false
10710
11306
  };
@@ -10717,7 +11313,10 @@ function registerPlanApprovalTools(server2) {
10717
11313
  }
10718
11314
  lines.push("");
10719
11315
  lines.push(`Total: ${data.length}`);
10720
- return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
11316
+ return {
11317
+ content: [{ type: "text", text: lines.join("\n") }],
11318
+ isError: false
11319
+ };
10721
11320
  }
10722
11321
  );
10723
11322
  server2.tool(
@@ -10736,7 +11335,10 @@ function registerPlanApprovalTools(server2) {
10736
11335
  if (decision === "edited" && !edited_post) {
10737
11336
  return {
10738
11337
  content: [
10739
- { type: "text", text: 'edited_post is required when decision is "edited".' }
11338
+ {
11339
+ type: "text",
11340
+ text: 'edited_post is required when decision is "edited".'
11341
+ }
10740
11342
  ],
10741
11343
  isError: true
10742
11344
  };
@@ -10749,7 +11351,9 @@ function registerPlanApprovalTools(server2) {
10749
11351
  if (decision === "edited") {
10750
11352
  updates.edited_post = edited_post;
10751
11353
  }
10752
- const { data, error } = await supabase.from("content_plan_approvals").update(updates).eq("id", approval_id).eq("user_id", userId).eq("status", "pending").select("id, plan_id, post_id, status, reason, decided_at, original_post, edited_post").maybeSingle();
11354
+ const { data, error } = await supabase.from("content_plan_approvals").update(updates).eq("id", approval_id).eq("user_id", userId).eq("status", "pending").select(
11355
+ "id, plan_id, post_id, status, reason, decided_at, original_post, edited_post"
11356
+ ).maybeSingle();
10753
11357
  if (error) {
10754
11358
  return {
10755
11359
  content: [
@@ -10774,7 +11378,12 @@ function registerPlanApprovalTools(server2) {
10774
11378
  }
10775
11379
  if ((response_format || "text") === "json") {
10776
11380
  return {
10777
- content: [{ type: "text", text: JSON.stringify(asEnvelope15(data), null, 2) }],
11381
+ content: [
11382
+ {
11383
+ type: "text",
11384
+ text: JSON.stringify(asEnvelope15(data), null, 2)
11385
+ }
11386
+ ],
10778
11387
  isError: false
10779
11388
  };
10780
11389
  }
@@ -10914,16 +11523,40 @@ function registerAllTools(server2, options) {
10914
11523
  init_posthog();
10915
11524
  init_supabase();
10916
11525
  init_sn();
11526
+ function flushAndExit(code) {
11527
+ const done = { out: false, err: false };
11528
+ const tryExit = () => {
11529
+ if (done.out && done.err) process.exit(code);
11530
+ };
11531
+ if (process.stdout.writableFinished) {
11532
+ done.out = true;
11533
+ } else {
11534
+ process.stdout.end(() => {
11535
+ done.out = true;
11536
+ tryExit();
11537
+ });
11538
+ }
11539
+ if (process.stderr.writableFinished) {
11540
+ done.err = true;
11541
+ } else {
11542
+ process.stderr.end(() => {
11543
+ done.err = true;
11544
+ tryExit();
11545
+ });
11546
+ }
11547
+ tryExit();
11548
+ setTimeout(() => process.exit(code), 2e3).unref();
11549
+ }
10917
11550
  process.on("uncaughtException", (err) => {
10918
11551
  process.stderr.write(`MCP server error: ${err.message}
10919
11552
  `);
10920
- process.exit(1);
11553
+ flushAndExit(1);
10921
11554
  });
10922
11555
  process.on("unhandledRejection", (reason) => {
10923
11556
  const message = reason instanceof Error ? reason.message : String(reason);
10924
11557
  process.stderr.write(`MCP server error: ${message}
10925
11558
  `);
10926
- process.exit(1);
11559
+ flushAndExit(1);
10927
11560
  });
10928
11561
  var command = process.argv[2];
10929
11562
  if (command === "--version" || command === "-v") {
@@ -10932,7 +11565,11 @@ if (command === "--version" || command === "-v") {
10932
11565
  const { fileURLToPath } = await import("node:url");
10933
11566
  let version = MCP_VERSION;
10934
11567
  try {
10935
- const pkgPath = resolve3(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
11568
+ const pkgPath = resolve3(
11569
+ dirname(fileURLToPath(import.meta.url)),
11570
+ "..",
11571
+ "package.json"
11572
+ );
10936
11573
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
10937
11574
  version = pkg.version;
10938
11575
  } catch {
@@ -10964,7 +11601,11 @@ if (command === "--help" || command === "-h") {
10964
11601
  ok: true,
10965
11602
  command: "help",
10966
11603
  commands: [
10967
- { name: "setup", aliases: ["login"], description: "Interactive OAuth setup" },
11604
+ {
11605
+ name: "setup",
11606
+ aliases: ["login"],
11607
+ description: "Interactive OAuth setup"
11608
+ },
10968
11609
  { name: "logout", description: "Remove credentials" },
10969
11610
  { name: "whoami", description: "Show auth info" },
10970
11611
  { name: "health", description: "Check connectivity" },
@@ -11053,10 +11694,14 @@ if (command === "sn") {
11053
11694
  await runSnCli(process.argv.slice(3));
11054
11695
  process.exit(0);
11055
11696
  }
11056
- if (command && !["setup", "login", "logout", "whoami", "health", "sn", "repl"].includes(command)) {
11057
- process.stderr.write(`Unknown command: ${command}
11697
+ if (command && !["setup", "login", "logout", "whoami", "health", "sn", "repl"].includes(
11698
+ command
11699
+ )) {
11700
+ process.stderr.write(
11701
+ `Unknown command: ${command}
11058
11702
  Run socialneuron-mcp --help for usage.
11059
- `);
11703
+ `
11704
+ );
11060
11705
  process.exit(1);
11061
11706
  }
11062
11707
  await initializeAuth();