@socialneuron/mcp-server 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/http.js +1223 -524
  2. package/dist/index.js +1073 -440
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var MCP_VERSION;
14
14
  var init_version = __esm({
15
15
  "src/lib/version.ts"() {
16
16
  "use strict";
17
- MCP_VERSION = "1.4.0";
17
+ MCP_VERSION = "1.4.2";
18
18
  }
19
19
  });
20
20
 
@@ -883,7 +883,7 @@ var init_tool_catalog = __esm({
883
883
  name: "check_status",
884
884
  description: "Check status of async content generation job",
885
885
  module: "content",
886
- scope: "mcp:write"
886
+ scope: "mcp:read"
887
887
  },
888
888
  {
889
889
  name: "create_storyboard",
@@ -3305,17 +3305,13 @@ async function runLoginDevice() {
3305
3305
  if (pollResponse.status === 200) {
3306
3306
  const pollData = await pollResponse.json();
3307
3307
  if (pollData.api_key) {
3308
- const validation = await validateApiKey(pollData.api_key);
3309
- if (validation.valid) {
3310
- await saveApiKey(pollData.api_key);
3311
- await saveSupabaseUrl(supabaseUrl);
3312
- console.error("");
3313
- console.error(" Authorized!");
3314
- console.error(` User: ${validation.email || "unknown"}`);
3315
- console.error(` Key prefix: ${pollData.api_key.substring(0, 12)}...`);
3316
- console.error("");
3317
- return;
3318
- }
3308
+ await saveApiKey(pollData.api_key);
3309
+ await saveSupabaseUrl(supabaseUrl);
3310
+ console.error("");
3311
+ console.error(" Authorized!");
3312
+ console.error(` Key prefix: ${pollData.api_key.substring(0, 12)}...`);
3313
+ console.error("");
3314
+ return;
3319
3315
  }
3320
3316
  }
3321
3317
  if (pollResponse.status === 410) {
@@ -3746,7 +3742,7 @@ var TOOL_SCOPES = {
3746
3742
  adapt_content: "mcp:write",
3747
3743
  generate_video: "mcp:write",
3748
3744
  generate_image: "mcp:write",
3749
- check_status: "mcp:write",
3745
+ check_status: "mcp:read",
3750
3746
  render_demo_video: "mcp:write",
3751
3747
  save_brand_profile: "mcp:write",
3752
3748
  update_platform_voice: "mcp:write",
@@ -3800,6 +3796,77 @@ function hasScope(userScopes, required) {
3800
3796
  // src/tools/ideation.ts
3801
3797
  init_edge_function();
3802
3798
  import { z } from "zod";
3799
+
3800
+ // src/lib/rate-limit.ts
3801
+ var CATEGORY_CONFIGS = {
3802
+ posting: { maxTokens: 30, refillRate: 30 / 60 },
3803
+ // 30 req/min
3804
+ screenshot: { maxTokens: 10, refillRate: 10 / 60 },
3805
+ // 10 req/min
3806
+ read: { maxTokens: 60, refillRate: 60 / 60 }
3807
+ // 60 req/min
3808
+ };
3809
+ var RateLimiter = class {
3810
+ tokens;
3811
+ lastRefill;
3812
+ maxTokens;
3813
+ refillRate;
3814
+ // tokens per second
3815
+ constructor(config) {
3816
+ this.maxTokens = config.maxTokens;
3817
+ this.refillRate = config.refillRate;
3818
+ this.tokens = config.maxTokens;
3819
+ this.lastRefill = Date.now();
3820
+ }
3821
+ /**
3822
+ * Try to consume one token. Returns true if the request is allowed,
3823
+ * false if rate-limited.
3824
+ */
3825
+ consume() {
3826
+ this.refill();
3827
+ if (this.tokens >= 1) {
3828
+ this.tokens -= 1;
3829
+ return true;
3830
+ }
3831
+ return false;
3832
+ }
3833
+ /**
3834
+ * Seconds until at least one token is available.
3835
+ */
3836
+ retryAfter() {
3837
+ this.refill();
3838
+ if (this.tokens >= 1) return 0;
3839
+ return Math.ceil((1 - this.tokens) / this.refillRate);
3840
+ }
3841
+ refill() {
3842
+ const now = Date.now();
3843
+ const elapsed = (now - this.lastRefill) / 1e3;
3844
+ this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
3845
+ this.lastRefill = now;
3846
+ }
3847
+ };
3848
+ var limiters = /* @__PURE__ */ new Map();
3849
+ function getRateLimiter(category) {
3850
+ let limiter = limiters.get(category);
3851
+ if (!limiter) {
3852
+ const config = CATEGORY_CONFIGS[category] ?? CATEGORY_CONFIGS.read;
3853
+ limiter = new RateLimiter(config);
3854
+ limiters.set(category, limiter);
3855
+ }
3856
+ return limiter;
3857
+ }
3858
+ function checkRateLimit(category, key) {
3859
+ const bucketKey = key ? `${category}:${key}` : category;
3860
+ const limiter = getRateLimiter(bucketKey);
3861
+ const allowed = limiter.consume();
3862
+ return {
3863
+ allowed,
3864
+ retryAfter: allowed ? 0 : limiter.retryAfter()
3865
+ };
3866
+ }
3867
+
3868
+ // src/tools/ideation.ts
3869
+ init_supabase();
3803
3870
  function registerIdeationTools(server2) {
3804
3871
  server2.tool(
3805
3872
  "generate_content",
@@ -3820,7 +3887,9 @@ function registerIdeationTools(server2) {
3820
3887
  "facebook",
3821
3888
  "threads",
3822
3889
  "bluesky"
3823
- ]).optional().describe("Target social media platform. Helps tailor tone, length, and format."),
3890
+ ]).optional().describe(
3891
+ "Target social media platform. Helps tailor tone, length, and format."
3892
+ ),
3824
3893
  brand_voice: z.string().max(500).optional().describe(
3825
3894
  'Brand voice guidelines to follow (e.g. "professional and empathetic", "playful and Gen-Z"). Leave blank to use a neutral tone.'
3826
3895
  ),
@@ -3831,7 +3900,30 @@ function registerIdeationTools(server2) {
3831
3900
  "Project ID to auto-load brand profile and performance context for prompt enrichment."
3832
3901
  )
3833
3902
  },
3834
- async ({ prompt: prompt2, content_type, platform: platform3, brand_voice, model, project_id }) => {
3903
+ async ({
3904
+ prompt: prompt2,
3905
+ content_type,
3906
+ platform: platform3,
3907
+ brand_voice,
3908
+ model,
3909
+ project_id
3910
+ }) => {
3911
+ try {
3912
+ const userId = await getDefaultUserId();
3913
+ const rl = checkRateLimit("posting", userId);
3914
+ if (!rl.allowed) {
3915
+ return {
3916
+ content: [
3917
+ {
3918
+ type: "text",
3919
+ text: `Rate limited. Retry after ${rl.retryAfter}s.`
3920
+ }
3921
+ ],
3922
+ isError: true
3923
+ };
3924
+ }
3925
+ } catch {
3926
+ }
3835
3927
  let enrichedPrompt = prompt2;
3836
3928
  if (platform3) {
3837
3929
  enrichedPrompt += `
@@ -3963,8 +4055,12 @@ Content Type: ${content_type}`;
3963
4055
  category: z.string().optional().describe(
3964
4056
  "Category filter (for YouTube). Examples: general, entertainment, education, tech, music, gaming, sports, news."
3965
4057
  ),
3966
- niche: z.string().optional().describe("Niche keyword filter. Only return trends matching these keywords."),
3967
- url: z.string().optional().describe('Required when source is "rss" or "url". The feed or page URL to fetch.'),
4058
+ niche: z.string().optional().describe(
4059
+ "Niche keyword filter. Only return trends matching these keywords."
4060
+ ),
4061
+ url: z.string().optional().describe(
4062
+ 'Required when source is "rss" or "url". The feed or page URL to fetch.'
4063
+ ),
3968
4064
  force_refresh: z.boolean().optional().describe("Skip the server-side cache and fetch fresh data.")
3969
4065
  },
3970
4066
  async ({ source, category, niche, url, force_refresh }) => {
@@ -4039,7 +4135,9 @@ Content Type: ${content_type}`;
4039
4135
  "adapt_content",
4040
4136
  "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.",
4041
4137
  {
4042
- content: z.string().max(5e3).describe("The content to adapt. Can be a caption, script, blog excerpt, or any text."),
4138
+ content: z.string().max(5e3).describe(
4139
+ "The content to adapt. Can be a caption, script, blog excerpt, or any text."
4140
+ ),
4043
4141
  source_platform: z.enum([
4044
4142
  "youtube",
4045
4143
  "tiktok",
@@ -4049,7 +4147,9 @@ Content Type: ${content_type}`;
4049
4147
  "facebook",
4050
4148
  "threads",
4051
4149
  "bluesky"
4052
- ]).optional().describe("The platform the content was originally written for. Helps preserve intent."),
4150
+ ]).optional().describe(
4151
+ "The platform the content was originally written for. Helps preserve intent."
4152
+ ),
4053
4153
  target_platform: z.enum([
4054
4154
  "youtube",
4055
4155
  "tiktok",
@@ -4063,9 +4163,33 @@ Content Type: ${content_type}`;
4063
4163
  brand_voice: z.string().max(500).optional().describe(
4064
4164
  'Brand voice guidelines to maintain during adaptation (e.g. "professional", "playful").'
4065
4165
  ),
4066
- project_id: z.string().uuid().optional().describe("Optional project ID to load platform voice overrides from brand profile.")
4166
+ project_id: z.string().uuid().optional().describe(
4167
+ "Optional project ID to load platform voice overrides from brand profile."
4168
+ )
4067
4169
  },
4068
- async ({ content, source_platform, target_platform, brand_voice, project_id }) => {
4170
+ async ({
4171
+ content,
4172
+ source_platform,
4173
+ target_platform,
4174
+ brand_voice,
4175
+ project_id
4176
+ }) => {
4177
+ try {
4178
+ const userId = await getDefaultUserId();
4179
+ const rl = checkRateLimit("posting", userId);
4180
+ if (!rl.allowed) {
4181
+ return {
4182
+ content: [
4183
+ {
4184
+ type: "text",
4185
+ text: `Rate limited. Retry after ${rl.retryAfter}s.`
4186
+ }
4187
+ ],
4188
+ isError: true
4189
+ };
4190
+ }
4191
+ } catch {
4192
+ }
4069
4193
  const platformGuidelines = {
4070
4194
  twitter: "Max 280 characters. Concise, punchy. 1-3 hashtags max. Thread-friendly.",
4071
4195
  threads: "Max 500 characters. Conversational, opinion-driven. Minimal hashtags.",
@@ -4148,76 +4272,6 @@ ${content}`,
4148
4272
  // src/tools/content.ts
4149
4273
  init_edge_function();
4150
4274
  import { z as z2 } from "zod";
4151
-
4152
- // src/lib/rate-limit.ts
4153
- var CATEGORY_CONFIGS = {
4154
- posting: { maxTokens: 30, refillRate: 30 / 60 },
4155
- // 30 req/min
4156
- screenshot: { maxTokens: 10, refillRate: 10 / 60 },
4157
- // 10 req/min
4158
- read: { maxTokens: 60, refillRate: 60 / 60 }
4159
- // 60 req/min
4160
- };
4161
- var RateLimiter = class {
4162
- tokens;
4163
- lastRefill;
4164
- maxTokens;
4165
- refillRate;
4166
- // tokens per second
4167
- constructor(config) {
4168
- this.maxTokens = config.maxTokens;
4169
- this.refillRate = config.refillRate;
4170
- this.tokens = config.maxTokens;
4171
- this.lastRefill = Date.now();
4172
- }
4173
- /**
4174
- * Try to consume one token. Returns true if the request is allowed,
4175
- * false if rate-limited.
4176
- */
4177
- consume() {
4178
- this.refill();
4179
- if (this.tokens >= 1) {
4180
- this.tokens -= 1;
4181
- return true;
4182
- }
4183
- return false;
4184
- }
4185
- /**
4186
- * Seconds until at least one token is available.
4187
- */
4188
- retryAfter() {
4189
- this.refill();
4190
- if (this.tokens >= 1) return 0;
4191
- return Math.ceil((1 - this.tokens) / this.refillRate);
4192
- }
4193
- refill() {
4194
- const now = Date.now();
4195
- const elapsed = (now - this.lastRefill) / 1e3;
4196
- this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
4197
- this.lastRefill = now;
4198
- }
4199
- };
4200
- var limiters = /* @__PURE__ */ new Map();
4201
- function getRateLimiter(category) {
4202
- let limiter = limiters.get(category);
4203
- if (!limiter) {
4204
- const config = CATEGORY_CONFIGS[category] ?? CATEGORY_CONFIGS.read;
4205
- limiter = new RateLimiter(config);
4206
- limiters.set(category, limiter);
4207
- }
4208
- return limiter;
4209
- }
4210
- function checkRateLimit(category, key) {
4211
- const bucketKey = key ? `${category}:${key}` : category;
4212
- const limiter = getRateLimiter(bucketKey);
4213
- const allowed = limiter.consume();
4214
- return {
4215
- allowed,
4216
- retryAfter: allowed ? 0 : limiter.retryAfter()
4217
- };
4218
- }
4219
-
4220
- // src/tools/content.ts
4221
4275
  init_supabase();
4222
4276
 
4223
4277
  // src/lib/sanitize-error.ts
@@ -4271,8 +4325,15 @@ function sanitizeDbError(error) {
4271
4325
 
4272
4326
  // src/tools/content.ts
4273
4327
  init_request_context();
4274
- var MAX_CREDITS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
4275
- var MAX_ASSETS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0));
4328
+ init_version();
4329
+ var MAX_CREDITS_PER_RUN = Math.max(
4330
+ 0,
4331
+ Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0)
4332
+ );
4333
+ var MAX_ASSETS_PER_RUN = Math.max(
4334
+ 0,
4335
+ Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0)
4336
+ );
4276
4337
  var _globalCreditsUsed = 0;
4277
4338
  var _globalAssetsGenerated = 0;
4278
4339
  function getCreditsUsed() {
@@ -4314,7 +4375,7 @@ function getCurrentBudgetStatus() {
4314
4375
  function asEnvelope(data) {
4315
4376
  return {
4316
4377
  _meta: {
4317
- version: "0.2.0",
4378
+ version: MCP_VERSION,
4318
4379
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
4319
4380
  },
4320
4381
  data
@@ -4396,8 +4457,12 @@ function registerContentTools(server2) {
4396
4457
  enable_audio: z2.boolean().optional().describe(
4397
4458
  "Enable native audio generation. Kling 2.6: doubles cost. Kling 3.0: 50% more (std 30/sec, pro 40/sec). 5+ languages."
4398
4459
  ),
4399
- image_url: z2.string().optional().describe("Start frame image URL for image-to-video (Kling 3.0 frame control)."),
4400
- end_frame_url: z2.string().optional().describe("End frame image URL (Kling 3.0 only). Enables seamless loop transitions."),
4460
+ image_url: z2.string().optional().describe(
4461
+ "Start frame image URL for image-to-video (Kling 3.0 frame control)."
4462
+ ),
4463
+ end_frame_url: z2.string().optional().describe(
4464
+ "End frame image URL (Kling 3.0 only). Enables seamless loop transitions."
4465
+ ),
4401
4466
  response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
4402
4467
  },
4403
4468
  async ({
@@ -4835,10 +4900,13 @@ function registerContentTools(server2) {
4835
4900
  };
4836
4901
  }
4837
4902
  if (job.external_id && (job.status === "pending" || job.status === "processing")) {
4838
- const { data: liveStatus } = await callEdgeFunction("kie-task-status", {
4839
- taskId: job.external_id,
4840
- model: job.model
4841
- });
4903
+ const { data: liveStatus } = await callEdgeFunction(
4904
+ "kie-task-status",
4905
+ {
4906
+ taskId: job.external_id,
4907
+ model: job.model
4908
+ }
4909
+ );
4842
4910
  if (liveStatus) {
4843
4911
  const lines2 = [
4844
4912
  `Job: ${job.id}`,
@@ -4912,7 +4980,12 @@ function registerContentTools(server2) {
4912
4980
  });
4913
4981
  if (format === "json") {
4914
4982
  return {
4915
- content: [{ type: "text", text: JSON.stringify(asEnvelope(job), null, 2) }]
4983
+ content: [
4984
+ {
4985
+ type: "text",
4986
+ text: JSON.stringify(asEnvelope(job), null, 2)
4987
+ }
4988
+ ]
4916
4989
  };
4917
4990
  }
4918
4991
  return {
@@ -4930,7 +5003,15 @@ function registerContentTools(server2) {
4930
5003
  brand_context: z2.string().max(3e3).optional().describe(
4931
5004
  "Brand context JSON from extract_brand. Include colors, voice tone, visual style keywords for consistent branding across frames."
4932
5005
  ),
4933
- platform: z2.enum(["tiktok", "instagram-reels", "youtube-shorts", "youtube", "general"]).describe("Target platform. Determines aspect ratio, duration, and pacing."),
5006
+ platform: z2.enum([
5007
+ "tiktok",
5008
+ "instagram-reels",
5009
+ "youtube-shorts",
5010
+ "youtube",
5011
+ "general"
5012
+ ]).describe(
5013
+ "Target platform. Determines aspect ratio, duration, and pacing."
5014
+ ),
4934
5015
  target_duration: z2.number().min(5).max(120).optional().describe(
4935
5016
  "Target total duration in seconds. Defaults to 30s for short-form, 60s for YouTube."
4936
5017
  ),
@@ -4938,7 +5019,9 @@ function registerContentTools(server2) {
4938
5019
  style: z2.string().optional().describe(
4939
5020
  'Visual style direction (e.g., "cinematic", "anime", "documentary", "motion graphics").'
4940
5021
  ),
4941
- response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to json for structured storyboard data.")
5022
+ response_format: z2.enum(["text", "json"]).optional().describe(
5023
+ "Response format. Defaults to json for structured storyboard data."
5024
+ )
4942
5025
  },
4943
5026
  async ({
4944
5027
  concept,
@@ -4951,7 +5034,11 @@ function registerContentTools(server2) {
4951
5034
  }) => {
4952
5035
  const format = response_format ?? "json";
4953
5036
  const startedAt = Date.now();
4954
- const isShortForm = ["tiktok", "instagram-reels", "youtube-shorts"].includes(platform3);
5037
+ const isShortForm = [
5038
+ "tiktok",
5039
+ "instagram-reels",
5040
+ "youtube-shorts"
5041
+ ].includes(platform3);
4955
5042
  const duration = target_duration ?? (isShortForm ? 30 : 60);
4956
5043
  const scenes = num_scenes ?? (isShortForm ? 7 : 10);
4957
5044
  const aspectRatio = isShortForm ? "9:16" : "16:9";
@@ -5044,7 +5131,12 @@ Return ONLY valid JSON in this exact format:
5044
5131
  details: { error }
5045
5132
  });
5046
5133
  return {
5047
- content: [{ type: "text", text: `Storyboard generation failed: ${error}` }],
5134
+ content: [
5135
+ {
5136
+ type: "text",
5137
+ text: `Storyboard generation failed: ${error}`
5138
+ }
5139
+ ],
5048
5140
  isError: true
5049
5141
  };
5050
5142
  }
@@ -5060,7 +5152,12 @@ Return ONLY valid JSON in this exact format:
5060
5152
  try {
5061
5153
  const parsed = JSON.parse(rawContent);
5062
5154
  return {
5063
- content: [{ type: "text", text: JSON.stringify(asEnvelope(parsed), null, 2) }]
5155
+ content: [
5156
+ {
5157
+ type: "text",
5158
+ text: JSON.stringify(asEnvelope(parsed), null, 2)
5159
+ }
5160
+ ]
5064
5161
  };
5065
5162
  } catch {
5066
5163
  return {
@@ -5124,7 +5221,10 @@ Return ONLY valid JSON in this exact format:
5124
5221
  isError: true
5125
5222
  };
5126
5223
  }
5127
- const rateLimit = checkRateLimit("posting", `generate_voiceover:${userId}`);
5224
+ const rateLimit = checkRateLimit(
5225
+ "posting",
5226
+ `generate_voiceover:${userId}`
5227
+ );
5128
5228
  if (!rateLimit.allowed) {
5129
5229
  await logMcpToolInvocation({
5130
5230
  toolName: "generate_voiceover",
@@ -5159,7 +5259,12 @@ Return ONLY valid JSON in this exact format:
5159
5259
  details: { error }
5160
5260
  });
5161
5261
  return {
5162
- content: [{ type: "text", text: `Voiceover generation failed: ${error}` }],
5262
+ content: [
5263
+ {
5264
+ type: "text",
5265
+ text: `Voiceover generation failed: ${error}`
5266
+ }
5267
+ ],
5163
5268
  isError: true
5164
5269
  };
5165
5270
  }
@@ -5172,7 +5277,10 @@ Return ONLY valid JSON in this exact format:
5172
5277
  });
5173
5278
  return {
5174
5279
  content: [
5175
- { type: "text", text: "Voiceover generation failed: no audio URL returned." }
5280
+ {
5281
+ type: "text",
5282
+ text: "Voiceover generation failed: no audio URL returned."
5283
+ }
5176
5284
  ],
5177
5285
  isError: true
5178
5286
  };
@@ -5244,7 +5352,9 @@ Return ONLY valid JSON in this exact format:
5244
5352
  "Carousel template. hormozi-authority: bold typography, one idea per slide, dark backgrounds. educational-series: numbered tips. Default: hormozi-authority."
5245
5353
  ),
5246
5354
  slide_count: z2.number().min(3).max(10).optional().describe("Number of slides (3-10). Default: 7."),
5247
- aspect_ratio: z2.enum(["1:1", "4:5", "9:16"]).optional().describe("Aspect ratio. 1:1 square (default), 4:5 portrait, 9:16 story."),
5355
+ aspect_ratio: z2.enum(["1:1", "4:5", "9:16"]).optional().describe(
5356
+ "Aspect ratio. 1:1 square (default), 4:5 portrait, 9:16 story."
5357
+ ),
5248
5358
  style: z2.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
5249
5359
  "Visual style. hormozi: black bg, bold white text, gold accents. Default: hormozi (when using hormozi-authority template)."
5250
5360
  ),
@@ -5281,7 +5391,10 @@ Return ONLY valid JSON in this exact format:
5281
5391
  };
5282
5392
  }
5283
5393
  const userId = await getDefaultUserId();
5284
- const rateLimit = checkRateLimit("posting", `generate_carousel:${userId}`);
5394
+ const rateLimit = checkRateLimit(
5395
+ "posting",
5396
+ `generate_carousel:${userId}`
5397
+ );
5285
5398
  if (!rateLimit.allowed) {
5286
5399
  await logMcpToolInvocation({
5287
5400
  toolName: "generate_carousel",
@@ -5319,7 +5432,12 @@ Return ONLY valid JSON in this exact format:
5319
5432
  details: { error }
5320
5433
  });
5321
5434
  return {
5322
- content: [{ type: "text", text: `Carousel generation failed: ${error}` }],
5435
+ content: [
5436
+ {
5437
+ type: "text",
5438
+ text: `Carousel generation failed: ${error}`
5439
+ }
5440
+ ],
5323
5441
  isError: true
5324
5442
  };
5325
5443
  }
@@ -5331,7 +5449,12 @@ Return ONLY valid JSON in this exact format:
5331
5449
  details: { error: "No carousel data returned" }
5332
5450
  });
5333
5451
  return {
5334
- content: [{ type: "text", text: "Carousel generation returned no data." }],
5452
+ content: [
5453
+ {
5454
+ type: "text",
5455
+ text: "Carousel generation returned no data."
5456
+ }
5457
+ ],
5335
5458
  isError: true
5336
5459
  };
5337
5460
  }
@@ -5397,6 +5520,7 @@ import { z as z3 } from "zod";
5397
5520
  import { createHash as createHash2 } from "node:crypto";
5398
5521
  init_supabase();
5399
5522
  init_quality();
5523
+ init_version();
5400
5524
  var PLATFORM_CASE_MAP = {
5401
5525
  youtube: "YouTube",
5402
5526
  tiktok: "TikTok",
@@ -5410,7 +5534,7 @@ var PLATFORM_CASE_MAP = {
5410
5534
  function asEnvelope2(data) {
5411
5535
  return {
5412
5536
  _meta: {
5413
- version: "0.2.0",
5537
+ version: MCP_VERSION,
5414
5538
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5415
5539
  },
5416
5540
  data
@@ -5442,15 +5566,21 @@ function registerDistributionTools(server2) {
5442
5566
  "threads",
5443
5567
  "bluesky"
5444
5568
  ])
5445
- ).min(1).describe("Target platforms to post to. Each must have an active OAuth connection."),
5569
+ ).min(1).describe(
5570
+ "Target platforms to post to. Each must have an active OAuth connection."
5571
+ ),
5446
5572
  title: z3.string().optional().describe("Post title (used by YouTube and some other platforms)."),
5447
- hashtags: z3.array(z3.string()).optional().describe('Hashtags to append to the caption. Include or omit the "#" prefix.'),
5573
+ hashtags: z3.array(z3.string()).optional().describe(
5574
+ 'Hashtags to append to the caption. Include or omit the "#" prefix.'
5575
+ ),
5448
5576
  schedule_at: z3.string().optional().describe(
5449
5577
  'ISO 8601 datetime for scheduled posting (e.g. "2026-03-15T14:00:00Z"). Omit for immediate posting.'
5450
5578
  ),
5451
5579
  project_id: z3.string().optional().describe("Social Neuron project ID to associate this post with."),
5452
5580
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
5453
- attribution: z3.boolean().optional().describe('If true, appends "Created with Social Neuron" to the caption. Default: false.')
5581
+ attribution: z3.boolean().optional().describe(
5582
+ 'If true, appends "Created with Social Neuron" to the caption. Default: false.'
5583
+ )
5454
5584
  },
5455
5585
  async ({
5456
5586
  media_url,
@@ -5469,7 +5599,12 @@ function registerDistributionTools(server2) {
5469
5599
  const startedAt = Date.now();
5470
5600
  if ((!caption || caption.trim().length === 0) && (!title || title.trim().length === 0)) {
5471
5601
  return {
5472
- content: [{ type: "text", text: "Either caption or title is required." }],
5602
+ content: [
5603
+ {
5604
+ type: "text",
5605
+ text: "Either caption or title is required."
5606
+ }
5607
+ ],
5473
5608
  isError: true
5474
5609
  };
5475
5610
  }
@@ -5492,7 +5627,9 @@ function registerDistributionTools(server2) {
5492
5627
  isError: true
5493
5628
  };
5494
5629
  }
5495
- const normalizedPlatforms = platforms.map((p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p);
5630
+ const normalizedPlatforms = platforms.map(
5631
+ (p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
5632
+ );
5496
5633
  let finalCaption = caption;
5497
5634
  if (attribution && finalCaption) {
5498
5635
  finalCaption = `${finalCaption}
@@ -5556,7 +5693,9 @@ Created with Social Neuron`;
5556
5693
  ];
5557
5694
  for (const [platform3, result] of Object.entries(data.results)) {
5558
5695
  if (result.success) {
5559
- lines.push(` ${platform3}: OK (jobId=${result.jobId}, postId=${result.postId})`);
5696
+ lines.push(
5697
+ ` ${platform3}: OK (jobId=${result.jobId}, postId=${result.postId})`
5698
+ );
5560
5699
  } else {
5561
5700
  lines.push(` ${platform3}: FAILED - ${result.error}`);
5562
5701
  }
@@ -5573,10 +5712,15 @@ Created with Social Neuron`;
5573
5712
  });
5574
5713
  if (format === "json") {
5575
5714
  return {
5576
- content: [{ type: "text", text: JSON.stringify(asEnvelope2(data), null, 2) }],
5577
- isError: !data.success
5578
- };
5579
- }
5715
+ content: [
5716
+ {
5717
+ type: "text",
5718
+ text: JSON.stringify(asEnvelope2(data), null, 2)
5719
+ }
5720
+ ],
5721
+ isError: !data.success
5722
+ };
5723
+ }
5580
5724
  return {
5581
5725
  content: [{ type: "text", text: lines.join("\n") }],
5582
5726
  isError: !data.success
@@ -5629,12 +5773,17 @@ Created with Social Neuron`;
5629
5773
  for (const account of accounts) {
5630
5774
  const name = account.username || "(unnamed)";
5631
5775
  const platformLower = account.platform.toLowerCase();
5632
- lines.push(` ${platformLower}: ${name} (connected ${account.created_at.split("T")[0]})`);
5776
+ lines.push(
5777
+ ` ${platformLower}: ${name} (connected ${account.created_at.split("T")[0]})`
5778
+ );
5633
5779
  }
5634
5780
  if (format === "json") {
5635
5781
  return {
5636
5782
  content: [
5637
- { type: "text", text: JSON.stringify(asEnvelope2({ accounts }), null, 2) }
5783
+ {
5784
+ type: "text",
5785
+ text: JSON.stringify(asEnvelope2({ accounts }), null, 2)
5786
+ }
5638
5787
  ]
5639
5788
  };
5640
5789
  }
@@ -5696,7 +5845,10 @@ Created with Social Neuron`;
5696
5845
  if (format === "json") {
5697
5846
  return {
5698
5847
  content: [
5699
- { type: "text", text: JSON.stringify(asEnvelope2({ posts: [] }), null, 2) }
5848
+ {
5849
+ type: "text",
5850
+ text: JSON.stringify(asEnvelope2({ posts: [] }), null, 2)
5851
+ }
5700
5852
  ]
5701
5853
  };
5702
5854
  }
@@ -5713,7 +5865,10 @@ Created with Social Neuron`;
5713
5865
  if (format === "json") {
5714
5866
  return {
5715
5867
  content: [
5716
- { type: "text", text: JSON.stringify(asEnvelope2({ posts }), null, 2) }
5868
+ {
5869
+ type: "text",
5870
+ text: JSON.stringify(asEnvelope2({ posts }), null, 2)
5871
+ }
5717
5872
  ]
5718
5873
  };
5719
5874
  }
@@ -5774,7 +5929,13 @@ Created with Social Neuron`;
5774
5929
  min_gap_hours: z3.number().min(1).max(24).default(4).describe("Minimum gap between posts on same platform"),
5775
5930
  response_format: z3.enum(["text", "json"]).default("text")
5776
5931
  },
5777
- async ({ platforms, count, start_after, min_gap_hours, response_format }) => {
5932
+ async ({
5933
+ platforms,
5934
+ count,
5935
+ start_after,
5936
+ min_gap_hours,
5937
+ response_format
5938
+ }) => {
5778
5939
  const startedAt = Date.now();
5779
5940
  try {
5780
5941
  const userId = await getDefaultUserId();
@@ -5785,7 +5946,9 @@ Created with Social Neuron`;
5785
5946
  const gapMs = min_gap_hours * 60 * 60 * 1e3;
5786
5947
  const candidates = [];
5787
5948
  for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
5788
- const date = new Date(startDate.getTime() + dayOffset * 24 * 60 * 60 * 1e3);
5949
+ const date = new Date(
5950
+ startDate.getTime() + dayOffset * 24 * 60 * 60 * 1e3
5951
+ );
5789
5952
  const dayOfWeek = date.getUTCDay();
5790
5953
  for (const platform3 of platforms) {
5791
5954
  const hours = PREFERRED_HOURS[platform3] ?? [12, 16];
@@ -5794,8 +5957,11 @@ Created with Social Neuron`;
5794
5957
  slotDate.setUTCHours(hours[hourIdx], 0, 0, 0);
5795
5958
  if (slotDate <= startDate) continue;
5796
5959
  const hasConflict = (existingPosts ?? []).some((post) => {
5797
- if (String(post.platform).toLowerCase() !== platform3) return false;
5798
- const postTime = new Date(post.scheduled_at ?? post.published_at).getTime();
5960
+ if (String(post.platform).toLowerCase() !== platform3)
5961
+ return false;
5962
+ const postTime = new Date(
5963
+ post.scheduled_at ?? post.published_at
5964
+ ).getTime();
5799
5965
  return Math.abs(postTime - slotDate.getTime()) < gapMs;
5800
5966
  });
5801
5967
  let engagementScore = hours.length - hourIdx;
@@ -5840,15 +6006,22 @@ Created with Social Neuron`;
5840
6006
  };
5841
6007
  }
5842
6008
  const lines = [];
5843
- lines.push(`Found ${slots.length} optimal slots (${conflictsAvoided} conflicts avoided):`);
6009
+ lines.push(
6010
+ `Found ${slots.length} optimal slots (${conflictsAvoided} conflicts avoided):`
6011
+ );
5844
6012
  lines.push("");
5845
6013
  lines.push("Datetime (UTC) | Platform | Score");
5846
6014
  lines.push("-------------------------+------------+------");
5847
6015
  for (const s of slots) {
5848
6016
  const dt = s.datetime.replace("T", " ").slice(0, 19);
5849
- lines.push(`${dt.padEnd(25)}| ${s.platform.padEnd(11)}| ${s.engagement_score}`);
6017
+ lines.push(
6018
+ `${dt.padEnd(25)}| ${s.platform.padEnd(11)}| ${s.engagement_score}`
6019
+ );
5850
6020
  }
5851
- return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
6021
+ return {
6022
+ content: [{ type: "text", text: lines.join("\n") }],
6023
+ isError: false
6024
+ };
5852
6025
  } catch (err) {
5853
6026
  const durationMs = Date.now() - startedAt;
5854
6027
  const message = err instanceof Error ? err.message : String(err);
@@ -5859,7 +6032,9 @@ Created with Social Neuron`;
5859
6032
  details: { error: message }
5860
6033
  });
5861
6034
  return {
5862
- content: [{ type: "text", text: `Failed to find slots: ${message}` }],
6035
+ content: [
6036
+ { type: "text", text: `Failed to find slots: ${message}` }
6037
+ ],
5863
6038
  isError: true
5864
6039
  };
5865
6040
  }
@@ -5886,8 +6061,12 @@ Created with Social Neuron`;
5886
6061
  auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
5887
6062
  dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
5888
6063
  response_format: z3.enum(["text", "json"]).default("text"),
5889
- enforce_quality: z3.boolean().default(true).describe("When true, block scheduling for posts that fail quality checks."),
5890
- quality_threshold: z3.number().int().min(0).max(35).optional().describe("Optional quality threshold override. Defaults to project setting or 26."),
6064
+ enforce_quality: z3.boolean().default(true).describe(
6065
+ "When true, block scheduling for posts that fail quality checks."
6066
+ ),
6067
+ quality_threshold: z3.number().int().min(0).max(35).optional().describe(
6068
+ "Optional quality threshold override. Defaults to project setting or 26."
6069
+ ),
5891
6070
  batch_size: z3.number().int().min(1).max(10).default(4).describe("Concurrent schedule calls per platform batch."),
5892
6071
  idempotency_seed: z3.string().max(128).optional().describe("Optional stable seed used when building idempotency keys.")
5893
6072
  },
@@ -5926,17 +6105,25 @@ Created with Social Neuron`;
5926
6105
  if (!stored?.plan_payload) {
5927
6106
  return {
5928
6107
  content: [
5929
- { type: "text", text: `No content plan found for plan_id=${plan_id}` }
6108
+ {
6109
+ type: "text",
6110
+ text: `No content plan found for plan_id=${plan_id}`
6111
+ }
5930
6112
  ],
5931
6113
  isError: true
5932
6114
  };
5933
6115
  }
5934
6116
  const payload = stored.plan_payload;
5935
- const postsFromPayload = Array.isArray(payload.posts) ? payload.posts : Array.isArray(payload.data?.posts) ? payload.data.posts : null;
6117
+ const postsFromPayload = Array.isArray(payload.posts) ? payload.posts : Array.isArray(
6118
+ payload.data?.posts
6119
+ ) ? payload.data.posts : null;
5936
6120
  if (!postsFromPayload) {
5937
6121
  return {
5938
6122
  content: [
5939
- { type: "text", text: `Stored plan ${plan_id} has no posts array.` }
6123
+ {
6124
+ type: "text",
6125
+ text: `Stored plan ${plan_id} has no posts array.`
6126
+ }
5940
6127
  ],
5941
6128
  isError: true
5942
6129
  };
@@ -6028,7 +6215,10 @@ Created with Social Neuron`;
6028
6215
  approvalSummary = {
6029
6216
  total: approvals.length,
6030
6217
  eligible: approvedPosts.length,
6031
- skipped: Math.max(0, workingPlan.posts.length - approvedPosts.length)
6218
+ skipped: Math.max(
6219
+ 0,
6220
+ workingPlan.posts.length - approvedPosts.length
6221
+ )
6032
6222
  };
6033
6223
  workingPlan = {
6034
6224
  ...workingPlan,
@@ -6062,9 +6252,14 @@ Created with Social Neuron`;
6062
6252
  try {
6063
6253
  const { data: settingsData } = await supabase.from("system_settings").select("value").eq("key", "content_safety").maybeSingle();
6064
6254
  if (settingsData?.value?.quality_threshold !== void 0) {
6065
- const parsedThreshold = Number(settingsData.value.quality_threshold);
6255
+ const parsedThreshold = Number(
6256
+ settingsData.value.quality_threshold
6257
+ );
6066
6258
  if (Number.isFinite(parsedThreshold)) {
6067
- effectiveQualityThreshold = Math.max(0, Math.min(35, Math.trunc(parsedThreshold)));
6259
+ effectiveQualityThreshold = Math.max(
6260
+ 0,
6261
+ Math.min(35, Math.trunc(parsedThreshold))
6262
+ );
6068
6263
  }
6069
6264
  }
6070
6265
  if (Array.isArray(settingsData?.value?.custom_banned_terms)) {
@@ -6100,13 +6295,18 @@ Created with Social Neuron`;
6100
6295
  }
6101
6296
  };
6102
6297
  });
6103
- const qualityPassed = postsWithResults.filter((post) => post.quality.passed).length;
6298
+ const qualityPassed = postsWithResults.filter(
6299
+ (post) => post.quality.passed
6300
+ ).length;
6104
6301
  const qualitySummary = {
6105
6302
  total_posts: postsWithResults.length,
6106
6303
  passed: qualityPassed,
6107
6304
  failed: postsWithResults.length - qualityPassed,
6108
6305
  avg_score: postsWithResults.length > 0 ? Number(
6109
- (postsWithResults.reduce((sum, post) => sum + post.quality.score, 0) / postsWithResults.length).toFixed(2)
6306
+ (postsWithResults.reduce(
6307
+ (sum, post) => sum + post.quality.score,
6308
+ 0
6309
+ ) / postsWithResults.length).toFixed(2)
6110
6310
  ) : 0
6111
6311
  };
6112
6312
  if (dry_run) {
@@ -6176,8 +6376,13 @@ Created with Social Neuron`;
6176
6376
  }
6177
6377
  }
6178
6378
  lines2.push("");
6179
- lines2.push(`Summary: ${passed}/${workingPlan.posts.length} passed quality check`);
6180
- return { content: [{ type: "text", text: lines2.join("\n") }], isError: false };
6379
+ lines2.push(
6380
+ `Summary: ${passed}/${workingPlan.posts.length} passed quality check`
6381
+ );
6382
+ return {
6383
+ content: [{ type: "text", text: lines2.join("\n") }],
6384
+ isError: false
6385
+ };
6181
6386
  }
6182
6387
  let scheduled = 0;
6183
6388
  let failed = 0;
@@ -6269,7 +6474,8 @@ Created with Social Neuron`;
6269
6474
  }
6270
6475
  const chunk = (arr, size) => {
6271
6476
  const out = [];
6272
- for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
6477
+ for (let i = 0; i < arr.length; i += size)
6478
+ out.push(arr.slice(i, i + size));
6273
6479
  return out;
6274
6480
  };
6275
6481
  const platformBatches = Array.from(grouped.entries()).map(
@@ -6277,7 +6483,9 @@ Created with Social Neuron`;
6277
6483
  const platformResults = [];
6278
6484
  const batches = chunk(platformPosts, batch_size);
6279
6485
  for (const batch of batches) {
6280
- const settled = await Promise.allSettled(batch.map((post) => scheduleOne(post)));
6486
+ const settled = await Promise.allSettled(
6487
+ batch.map((post) => scheduleOne(post))
6488
+ );
6281
6489
  for (const outcome of settled) {
6282
6490
  if (outcome.status === "fulfilled") {
6283
6491
  platformResults.push(outcome.value);
@@ -6343,7 +6551,11 @@ Created with Social Neuron`;
6343
6551
  plan_id: effectivePlanId,
6344
6552
  approvals: approvalSummary,
6345
6553
  posts: results,
6346
- summary: { total_posts: workingPlan.posts.length, scheduled, failed }
6554
+ summary: {
6555
+ total_posts: workingPlan.posts.length,
6556
+ scheduled,
6557
+ failed
6558
+ }
6347
6559
  }),
6348
6560
  null,
6349
6561
  2
@@ -6381,7 +6593,12 @@ Created with Social Neuron`;
6381
6593
  details: { error: message }
6382
6594
  });
6383
6595
  return {
6384
- content: [{ type: "text", text: `Batch scheduling failed: ${message}` }],
6596
+ content: [
6597
+ {
6598
+ type: "text",
6599
+ text: `Batch scheduling failed: ${message}`
6600
+ }
6601
+ ],
6385
6602
  isError: true
6386
6603
  };
6387
6604
  }
@@ -6393,10 +6610,11 @@ Created with Social Neuron`;
6393
6610
  init_supabase();
6394
6611
  init_edge_function();
6395
6612
  import { z as z4 } from "zod";
6613
+ init_version();
6396
6614
  function asEnvelope3(data) {
6397
6615
  return {
6398
6616
  _meta: {
6399
- version: "0.2.0",
6617
+ version: MCP_VERSION,
6400
6618
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6401
6619
  },
6402
6620
  data
@@ -6509,7 +6727,9 @@ function registerAnalyticsTools(server2) {
6509
6727
  ]
6510
6728
  };
6511
6729
  }
6512
- 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);
6730
+ const { data: simpleRows, error: simpleError } = await supabase.from("post_analytics").select(
6731
+ "id, post_id, platform, views, likes, comments, shares, captured_at"
6732
+ ).in("post_id", postIds).gte("captured_at", sinceIso).order("captured_at", { ascending: false }).limit(maxPosts);
6513
6733
  if (simpleError) {
6514
6734
  return {
6515
6735
  content: [
@@ -6521,7 +6741,12 @@ function registerAnalyticsTools(server2) {
6521
6741
  isError: true
6522
6742
  };
6523
6743
  }
6524
- return formatSimpleAnalytics(simpleRows, platform3, lookbackDays, format);
6744
+ return formatSimpleAnalytics(
6745
+ simpleRows,
6746
+ platform3,
6747
+ lookbackDays,
6748
+ format
6749
+ );
6525
6750
  }
6526
6751
  if (!rows || rows.length === 0) {
6527
6752
  if (format === "json") {
@@ -6598,7 +6823,10 @@ function registerAnalyticsTools(server2) {
6598
6823
  const format = response_format ?? "text";
6599
6824
  const startedAt = Date.now();
6600
6825
  const userId = await getDefaultUserId();
6601
- const rateLimit = checkRateLimit("posting", `refresh_platform_analytics:${userId}`);
6826
+ const rateLimit = checkRateLimit(
6827
+ "posting",
6828
+ `refresh_platform_analytics:${userId}`
6829
+ );
6602
6830
  if (!rateLimit.allowed) {
6603
6831
  await logMcpToolInvocation({
6604
6832
  toolName: "refresh_platform_analytics",
@@ -6616,7 +6844,9 @@ function registerAnalyticsTools(server2) {
6616
6844
  isError: true
6617
6845
  };
6618
6846
  }
6619
- const { data, error } = await callEdgeFunction("fetch-analytics", { userId });
6847
+ const { data, error } = await callEdgeFunction("fetch-analytics", {
6848
+ userId
6849
+ });
6620
6850
  if (error) {
6621
6851
  await logMcpToolInvocation({
6622
6852
  toolName: "refresh_platform_analytics",
@@ -6625,7 +6855,12 @@ function registerAnalyticsTools(server2) {
6625
6855
  details: { error }
6626
6856
  });
6627
6857
  return {
6628
- content: [{ type: "text", text: `Error refreshing analytics: ${error}` }],
6858
+ content: [
6859
+ {
6860
+ type: "text",
6861
+ text: `Error refreshing analytics: ${error}`
6862
+ }
6863
+ ],
6629
6864
  isError: true
6630
6865
  };
6631
6866
  }
@@ -6638,12 +6873,18 @@ function registerAnalyticsTools(server2) {
6638
6873
  details: { error: "Edge function returned success=false" }
6639
6874
  });
6640
6875
  return {
6641
- content: [{ type: "text", text: "Analytics refresh failed." }],
6876
+ content: [
6877
+ { type: "text", text: "Analytics refresh failed." }
6878
+ ],
6642
6879
  isError: true
6643
6880
  };
6644
6881
  }
6645
- const queued = (result.results ?? []).filter((r) => r.status === "queued").length;
6646
- const errored = (result.results ?? []).filter((r) => r.status === "error").length;
6882
+ const queued = (result.results ?? []).filter(
6883
+ (r) => r.status === "queued"
6884
+ ).length;
6885
+ const errored = (result.results ?? []).filter(
6886
+ (r) => r.status === "error"
6887
+ ).length;
6647
6888
  const lines = [
6648
6889
  `Analytics refresh triggered successfully.`,
6649
6890
  ` Posts processed: ${result.postsProcessed}`,
@@ -6689,7 +6930,10 @@ function formatAnalytics(summary, days, format) {
6689
6930
  if (format === "json") {
6690
6931
  return {
6691
6932
  content: [
6692
- { type: "text", text: JSON.stringify(asEnvelope3({ ...summary, days }), null, 2) }
6933
+ {
6934
+ type: "text",
6935
+ text: JSON.stringify(asEnvelope3({ ...summary, days }), null, 2)
6936
+ }
6693
6937
  ]
6694
6938
  };
6695
6939
  }
@@ -6807,10 +7051,160 @@ function formatSimpleAnalytics(rows, platform3, days, format) {
6807
7051
  init_edge_function();
6808
7052
  init_supabase();
6809
7053
  import { z as z5 } from "zod";
7054
+
7055
+ // src/lib/ssrf.ts
7056
+ var BLOCKED_IP_PATTERNS = [
7057
+ // IPv4 localhost/loopback
7058
+ /^127\./,
7059
+ /^0\./,
7060
+ // IPv4 private ranges (RFC 1918)
7061
+ /^10\./,
7062
+ /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
7063
+ /^192\.168\./,
7064
+ // IPv4 link-local
7065
+ /^169\.254\./,
7066
+ // Cloud metadata endpoint (AWS, GCP, Azure)
7067
+ /^169\.254\.169\.254$/,
7068
+ // IPv4 broadcast
7069
+ /^255\./,
7070
+ // Shared address space (RFC 6598)
7071
+ /^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./
7072
+ ];
7073
+ var BLOCKED_IPV6_PATTERNS = [
7074
+ /^::1$/i,
7075
+ // loopback
7076
+ /^::$/i,
7077
+ // unspecified
7078
+ /^fe[89ab][0-9a-f]:/i,
7079
+ // link-local fe80::/10
7080
+ /^fc[0-9a-f]:/i,
7081
+ // unique local fc00::/7
7082
+ /^fd[0-9a-f]:/i,
7083
+ // unique local fc00::/7
7084
+ /^::ffff:127\./i,
7085
+ // IPv4-mapped localhost
7086
+ /^::ffff:(0|10|127|169\.254|172\.(1[6-9]|2[0-9]|3[0-1])|192\.168)\./i
7087
+ // IPv4-mapped private
7088
+ ];
7089
+ var BLOCKED_HOSTNAMES = [
7090
+ "localhost",
7091
+ "localhost.localdomain",
7092
+ "local",
7093
+ "127.0.0.1",
7094
+ "0.0.0.0",
7095
+ "[::1]",
7096
+ "[::ffff:127.0.0.1]",
7097
+ // Cloud metadata endpoints
7098
+ "metadata.google.internal",
7099
+ "metadata.goog",
7100
+ "instance-data",
7101
+ "instance-data.ec2.internal"
7102
+ ];
7103
+ var ALLOWED_PROTOCOLS = ["http:", "https:"];
7104
+ var BLOCKED_PORTS = [22, 23, 25, 110, 143, 445, 3306, 5432, 6379, 27017, 11211];
7105
+ function isBlockedIP(ip) {
7106
+ const normalized = ip.replace(/^\[/, "").replace(/\]$/, "");
7107
+ if (normalized.includes(":")) {
7108
+ return BLOCKED_IPV6_PATTERNS.some((pattern) => pattern.test(normalized));
7109
+ }
7110
+ return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(normalized));
7111
+ }
7112
+ function isBlockedHostname(hostname) {
7113
+ return BLOCKED_HOSTNAMES.includes(hostname.toLowerCase());
7114
+ }
7115
+ function isIPAddress(hostname) {
7116
+ const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
7117
+ const ipv6Pattern = /^\[?[a-fA-F0-9:]+\]?$/;
7118
+ return ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname);
7119
+ }
7120
+ async function validateUrlForSSRF(urlString) {
7121
+ try {
7122
+ const url = new URL(urlString);
7123
+ if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
7124
+ return {
7125
+ isValid: false,
7126
+ error: `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are allowed.`
7127
+ };
7128
+ }
7129
+ if (url.username || url.password) {
7130
+ return {
7131
+ isValid: false,
7132
+ error: "URLs with embedded credentials are not allowed."
7133
+ };
7134
+ }
7135
+ const hostname = url.hostname.toLowerCase();
7136
+ if (isBlockedHostname(hostname)) {
7137
+ return {
7138
+ isValid: false,
7139
+ error: "Access to internal/localhost addresses is not allowed."
7140
+ };
7141
+ }
7142
+ if (isIPAddress(hostname) && isBlockedIP(hostname)) {
7143
+ return {
7144
+ isValid: false,
7145
+ error: "Access to private/internal IP addresses is not allowed."
7146
+ };
7147
+ }
7148
+ const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80;
7149
+ if (BLOCKED_PORTS.includes(port)) {
7150
+ return {
7151
+ isValid: false,
7152
+ error: `Access to port ${port} is not allowed.`
7153
+ };
7154
+ }
7155
+ let resolvedIP;
7156
+ if (!isIPAddress(hostname)) {
7157
+ try {
7158
+ const dns = await import("node:dns");
7159
+ const resolver = new dns.promises.Resolver();
7160
+ const resolvedIPs = [];
7161
+ try {
7162
+ const aRecords = await resolver.resolve4(hostname);
7163
+ resolvedIPs.push(...aRecords);
7164
+ } catch {
7165
+ }
7166
+ try {
7167
+ const aaaaRecords = await resolver.resolve6(hostname);
7168
+ resolvedIPs.push(...aaaaRecords);
7169
+ } catch {
7170
+ }
7171
+ if (resolvedIPs.length === 0) {
7172
+ return {
7173
+ isValid: false,
7174
+ error: "DNS resolution failed: hostname did not resolve to any address."
7175
+ };
7176
+ }
7177
+ for (const ip of resolvedIPs) {
7178
+ if (isBlockedIP(ip)) {
7179
+ return {
7180
+ isValid: false,
7181
+ error: "Hostname resolves to a private/internal IP address."
7182
+ };
7183
+ }
7184
+ }
7185
+ resolvedIP = resolvedIPs[0];
7186
+ } catch {
7187
+ return {
7188
+ isValid: false,
7189
+ error: "DNS resolution failed. Cannot verify hostname safety."
7190
+ };
7191
+ }
7192
+ }
7193
+ return { isValid: true, sanitizedUrl: url.toString(), resolvedIP };
7194
+ } catch (error) {
7195
+ return {
7196
+ isValid: false,
7197
+ error: `Invalid URL format: ${error instanceof Error ? error.message : "Unknown error"}`
7198
+ };
7199
+ }
7200
+ }
7201
+
7202
+ // src/tools/brand.ts
7203
+ init_version();
6810
7204
  function asEnvelope4(data) {
6811
7205
  return {
6812
7206
  _meta: {
6813
- version: "0.2.0",
7207
+ version: MCP_VERSION,
6814
7208
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6815
7209
  },
6816
7210
  data
@@ -6827,6 +7221,15 @@ function registerBrandTools(server2) {
6827
7221
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6828
7222
  },
6829
7223
  async ({ url, response_format }) => {
7224
+ const ssrfCheck = await validateUrlForSSRF(url);
7225
+ if (!ssrfCheck.isValid) {
7226
+ return {
7227
+ content: [
7228
+ { type: "text", text: `URL blocked: ${ssrfCheck.error}` }
7229
+ ],
7230
+ isError: true
7231
+ };
7232
+ }
6830
7233
  const { data, error } = await callEdgeFunction(
6831
7234
  "brand-extract",
6832
7235
  { url },
@@ -6856,7 +7259,12 @@ function registerBrandTools(server2) {
6856
7259
  }
6857
7260
  if ((response_format || "text") === "json") {
6858
7261
  return {
6859
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(data), null, 2) }]
7262
+ content: [
7263
+ {
7264
+ type: "text",
7265
+ text: JSON.stringify(asEnvelope4(data), null, 2)
7266
+ }
7267
+ ]
6860
7268
  };
6861
7269
  }
6862
7270
  const lines = [
@@ -6920,7 +7328,12 @@ function registerBrandTools(server2) {
6920
7328
  const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
6921
7329
  if (!membership) {
6922
7330
  return {
6923
- content: [{ type: "text", text: "Project is not accessible to current user." }],
7331
+ content: [
7332
+ {
7333
+ type: "text",
7334
+ text: "Project is not accessible to current user."
7335
+ }
7336
+ ],
6924
7337
  isError: true
6925
7338
  };
6926
7339
  }
@@ -6939,13 +7352,21 @@ function registerBrandTools(server2) {
6939
7352
  if (!data) {
6940
7353
  return {
6941
7354
  content: [
6942
- { type: "text", text: "No active brand profile found for this project." }
7355
+ {
7356
+ type: "text",
7357
+ text: "No active brand profile found for this project."
7358
+ }
6943
7359
  ]
6944
7360
  };
6945
7361
  }
6946
7362
  if ((response_format || "text") === "json") {
6947
7363
  return {
6948
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(data), null, 2) }]
7364
+ content: [
7365
+ {
7366
+ type: "text",
7367
+ text: JSON.stringify(asEnvelope4(data), null, 2)
7368
+ }
7369
+ ]
6949
7370
  };
6950
7371
  }
6951
7372
  const lines = [
@@ -6966,11 +7387,18 @@ function registerBrandTools(server2) {
6966
7387
  "Persist a brand profile as the active profile for a project.",
6967
7388
  {
6968
7389
  project_id: z5.string().uuid().optional().describe("Project ID. Defaults to active project context."),
6969
- brand_context: z5.record(z5.string(), z5.unknown()).describe("Brand context payload to save to brand_profiles.brand_context."),
7390
+ brand_context: z5.record(z5.string(), z5.unknown()).describe(
7391
+ "Brand context payload to save to brand_profiles.brand_context."
7392
+ ),
6970
7393
  change_summary: z5.string().max(500).optional().describe("Optional summary of changes."),
6971
7394
  changed_paths: z5.array(z5.string()).optional().describe("Optional changed path list."),
6972
7395
  source_url: z5.string().url().optional().describe("Optional source URL for provenance."),
6973
- extraction_method: z5.enum(["manual", "url_extract", "business_profiler", "product_showcase"]).optional().describe("Extraction method metadata."),
7396
+ extraction_method: z5.enum([
7397
+ "manual",
7398
+ "url_extract",
7399
+ "business_profiler",
7400
+ "product_showcase"
7401
+ ]).optional().describe("Extraction method metadata."),
6974
7402
  overall_confidence: z5.number().min(0).max(1).optional().describe("Optional overall confidence score in range 0..1."),
6975
7403
  extraction_metadata: z5.record(z5.string(), z5.unknown()).optional(),
6976
7404
  response_format: z5.enum(["text", "json"]).optional()
@@ -7010,20 +7438,28 @@ function registerBrandTools(server2) {
7010
7438
  const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
7011
7439
  if (!membership) {
7012
7440
  return {
7013
- content: [{ type: "text", text: "Project is not accessible to current user." }],
7441
+ content: [
7442
+ {
7443
+ type: "text",
7444
+ text: "Project is not accessible to current user."
7445
+ }
7446
+ ],
7014
7447
  isError: true
7015
7448
  };
7016
7449
  }
7017
- const { data: profileId, error } = await supabase.rpc("set_active_brand_profile", {
7018
- p_project_id: projectId,
7019
- p_brand_context: brand_context,
7020
- p_change_summary: change_summary || null,
7021
- p_changed_paths: changed_paths || [],
7022
- p_source_url: source_url || null,
7023
- p_extraction_method: extraction_method || "manual",
7024
- p_overall_confidence: overall_confidence ?? null,
7025
- p_extraction_metadata: extraction_metadata || null
7026
- });
7450
+ const { data: profileId, error } = await supabase.rpc(
7451
+ "set_active_brand_profile",
7452
+ {
7453
+ p_project_id: projectId,
7454
+ p_brand_context: brand_context,
7455
+ p_change_summary: change_summary || null,
7456
+ p_changed_paths: changed_paths || [],
7457
+ p_source_url: source_url || null,
7458
+ p_extraction_method: extraction_method || "manual",
7459
+ p_overall_confidence: overall_confidence ?? null,
7460
+ p_extraction_metadata: extraction_metadata || null
7461
+ }
7462
+ );
7027
7463
  if (error) {
7028
7464
  return {
7029
7465
  content: [
@@ -7044,7 +7480,12 @@ function registerBrandTools(server2) {
7044
7480
  };
7045
7481
  if ((response_format || "text") === "json") {
7046
7482
  return {
7047
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(payload), null, 2) }]
7483
+ content: [
7484
+ {
7485
+ type: "text",
7486
+ text: JSON.stringify(asEnvelope4(payload), null, 2)
7487
+ }
7488
+ ]
7048
7489
  };
7049
7490
  }
7050
7491
  return {
@@ -7100,7 +7541,10 @@ Version: ${payload.version ?? "N/A"}`
7100
7541
  if (!projectId) {
7101
7542
  return {
7102
7543
  content: [
7103
- { type: "text", text: "No project_id provided and no default project found." }
7544
+ {
7545
+ type: "text",
7546
+ text: "No project_id provided and no default project found."
7547
+ }
7104
7548
  ],
7105
7549
  isError: true
7106
7550
  };
@@ -7115,7 +7559,12 @@ Version: ${payload.version ?? "N/A"}`
7115
7559
  const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
7116
7560
  if (!membership) {
7117
7561
  return {
7118
- content: [{ type: "text", text: "Project is not accessible to current user." }],
7562
+ content: [
7563
+ {
7564
+ type: "text",
7565
+ text: "Project is not accessible to current user."
7566
+ }
7567
+ ],
7119
7568
  isError: true
7120
7569
  };
7121
7570
  }
@@ -7131,7 +7580,9 @@ Version: ${payload.version ?? "N/A"}`
7131
7580
  isError: true
7132
7581
  };
7133
7582
  }
7134
- const brandContext = { ...existingProfile.brand_context };
7583
+ const brandContext = {
7584
+ ...existingProfile.brand_context
7585
+ };
7135
7586
  const voiceProfile = brandContext.voiceProfile ?? {};
7136
7587
  const platformOverrides = voiceProfile.platformOverrides ?? {};
7137
7588
  const existingOverride = platformOverrides[platform3] ?? {};
@@ -7155,16 +7606,19 @@ Version: ${payload.version ?? "N/A"}`
7155
7606
  ...brandContext,
7156
7607
  voiceProfile: updatedVoiceProfile
7157
7608
  };
7158
- const { data: profileId, error: saveError } = await supabase.rpc("set_active_brand_profile", {
7159
- p_project_id: projectId,
7160
- p_brand_context: updatedContext,
7161
- p_change_summary: `Updated platform voice override for ${platform3}`,
7162
- p_changed_paths: [`voiceProfile.platformOverrides.${platform3}`],
7163
- p_source_url: null,
7164
- p_extraction_method: "manual",
7165
- p_overall_confidence: null,
7166
- p_extraction_metadata: null
7167
- });
7609
+ const { data: profileId, error: saveError } = await supabase.rpc(
7610
+ "set_active_brand_profile",
7611
+ {
7612
+ p_project_id: projectId,
7613
+ p_brand_context: updatedContext,
7614
+ p_change_summary: `Updated platform voice override for ${platform3}`,
7615
+ p_changed_paths: [`voiceProfile.platformOverrides.${platform3}`],
7616
+ p_source_url: null,
7617
+ p_extraction_method: "manual",
7618
+ p_overall_confidence: null,
7619
+ p_extraction_metadata: null
7620
+ }
7621
+ );
7168
7622
  if (saveError) {
7169
7623
  return {
7170
7624
  content: [
@@ -7185,7 +7639,12 @@ Version: ${payload.version ?? "N/A"}`
7185
7639
  };
7186
7640
  if ((response_format || "text") === "json") {
7187
7641
  return {
7188
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(payload), null, 2) }],
7642
+ content: [
7643
+ {
7644
+ type: "text",
7645
+ text: JSON.stringify(asEnvelope4(payload), null, 2)
7646
+ }
7647
+ ],
7189
7648
  isError: false
7190
7649
  };
7191
7650
  }
@@ -7283,155 +7742,6 @@ async function capturePageScreenshot(page, outputPath, selector) {
7283
7742
  // src/tools/screenshot.ts
7284
7743
  import { resolve, relative } from "node:path";
7285
7744
  import { mkdir } from "node:fs/promises";
7286
-
7287
- // src/lib/ssrf.ts
7288
- var BLOCKED_IP_PATTERNS = [
7289
- // IPv4 localhost/loopback
7290
- /^127\./,
7291
- /^0\./,
7292
- // IPv4 private ranges (RFC 1918)
7293
- /^10\./,
7294
- /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
7295
- /^192\.168\./,
7296
- // IPv4 link-local
7297
- /^169\.254\./,
7298
- // Cloud metadata endpoint (AWS, GCP, Azure)
7299
- /^169\.254\.169\.254$/,
7300
- // IPv4 broadcast
7301
- /^255\./,
7302
- // Shared address space (RFC 6598)
7303
- /^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./
7304
- ];
7305
- var BLOCKED_IPV6_PATTERNS = [
7306
- /^::1$/i,
7307
- // loopback
7308
- /^::$/i,
7309
- // unspecified
7310
- /^fe[89ab][0-9a-f]:/i,
7311
- // link-local fe80::/10
7312
- /^fc[0-9a-f]:/i,
7313
- // unique local fc00::/7
7314
- /^fd[0-9a-f]:/i,
7315
- // unique local fc00::/7
7316
- /^::ffff:127\./i,
7317
- // IPv4-mapped localhost
7318
- /^::ffff:(0|10|127|169\.254|172\.(1[6-9]|2[0-9]|3[0-1])|192\.168)\./i
7319
- // IPv4-mapped private
7320
- ];
7321
- var BLOCKED_HOSTNAMES = [
7322
- "localhost",
7323
- "localhost.localdomain",
7324
- "local",
7325
- "127.0.0.1",
7326
- "0.0.0.0",
7327
- "[::1]",
7328
- "[::ffff:127.0.0.1]",
7329
- // Cloud metadata endpoints
7330
- "metadata.google.internal",
7331
- "metadata.goog",
7332
- "instance-data",
7333
- "instance-data.ec2.internal"
7334
- ];
7335
- var ALLOWED_PROTOCOLS = ["http:", "https:"];
7336
- var BLOCKED_PORTS = [22, 23, 25, 110, 143, 445, 3306, 5432, 6379, 27017, 11211];
7337
- function isBlockedIP(ip) {
7338
- const normalized = ip.replace(/^\[/, "").replace(/\]$/, "");
7339
- if (normalized.includes(":")) {
7340
- return BLOCKED_IPV6_PATTERNS.some((pattern) => pattern.test(normalized));
7341
- }
7342
- return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(normalized));
7343
- }
7344
- function isBlockedHostname(hostname) {
7345
- return BLOCKED_HOSTNAMES.includes(hostname.toLowerCase());
7346
- }
7347
- function isIPAddress(hostname) {
7348
- const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
7349
- const ipv6Pattern = /^\[?[a-fA-F0-9:]+\]?$/;
7350
- return ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname);
7351
- }
7352
- async function validateUrlForSSRF(urlString) {
7353
- try {
7354
- const url = new URL(urlString);
7355
- if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
7356
- return {
7357
- isValid: false,
7358
- error: `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are allowed.`
7359
- };
7360
- }
7361
- if (url.username || url.password) {
7362
- return {
7363
- isValid: false,
7364
- error: "URLs with embedded credentials are not allowed."
7365
- };
7366
- }
7367
- const hostname = url.hostname.toLowerCase();
7368
- if (isBlockedHostname(hostname)) {
7369
- return {
7370
- isValid: false,
7371
- error: "Access to internal/localhost addresses is not allowed."
7372
- };
7373
- }
7374
- if (isIPAddress(hostname) && isBlockedIP(hostname)) {
7375
- return {
7376
- isValid: false,
7377
- error: "Access to private/internal IP addresses is not allowed."
7378
- };
7379
- }
7380
- const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80;
7381
- if (BLOCKED_PORTS.includes(port)) {
7382
- return {
7383
- isValid: false,
7384
- error: `Access to port ${port} is not allowed.`
7385
- };
7386
- }
7387
- let resolvedIP;
7388
- if (!isIPAddress(hostname)) {
7389
- try {
7390
- const dns = await import("node:dns");
7391
- const resolver = new dns.promises.Resolver();
7392
- const resolvedIPs = [];
7393
- try {
7394
- const aRecords = await resolver.resolve4(hostname);
7395
- resolvedIPs.push(...aRecords);
7396
- } catch {
7397
- }
7398
- try {
7399
- const aaaaRecords = await resolver.resolve6(hostname);
7400
- resolvedIPs.push(...aaaaRecords);
7401
- } catch {
7402
- }
7403
- if (resolvedIPs.length === 0) {
7404
- return {
7405
- isValid: false,
7406
- error: "DNS resolution failed: hostname did not resolve to any address."
7407
- };
7408
- }
7409
- for (const ip of resolvedIPs) {
7410
- if (isBlockedIP(ip)) {
7411
- return {
7412
- isValid: false,
7413
- error: "Hostname resolves to a private/internal IP address."
7414
- };
7415
- }
7416
- }
7417
- resolvedIP = resolvedIPs[0];
7418
- } catch {
7419
- return {
7420
- isValid: false,
7421
- error: "DNS resolution failed. Cannot verify hostname safety."
7422
- };
7423
- }
7424
- }
7425
- return { isValid: true, sanitizedUrl: url.toString(), resolvedIP };
7426
- } catch (error) {
7427
- return {
7428
- isValid: false,
7429
- error: `Invalid URL format: ${error instanceof Error ? error.message : "Unknown error"}`
7430
- };
7431
- }
7432
- }
7433
-
7434
- // src/tools/screenshot.ts
7435
7745
  init_supabase();
7436
7746
  function registerScreenshotTools(server2) {
7437
7747
  server2.tool(
@@ -8007,6 +8317,8 @@ function registerRemotionTools(server2) {
8007
8317
  // src/tools/insights.ts
8008
8318
  init_supabase();
8009
8319
  import { z as z8 } from "zod";
8320
+ init_version();
8321
+ var MAX_INSIGHT_AGE_DAYS = 30;
8010
8322
  var PLATFORM_ENUM = [
8011
8323
  "youtube",
8012
8324
  "tiktok",
@@ -8017,11 +8329,19 @@ var PLATFORM_ENUM = [
8017
8329
  "threads",
8018
8330
  "bluesky"
8019
8331
  ];
8020
- var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
8332
+ var DAY_NAMES = [
8333
+ "Sunday",
8334
+ "Monday",
8335
+ "Tuesday",
8336
+ "Wednesday",
8337
+ "Thursday",
8338
+ "Friday",
8339
+ "Saturday"
8340
+ ];
8021
8341
  function asEnvelope5(data) {
8022
8342
  return {
8023
8343
  _meta: {
8024
- version: "0.2.0",
8344
+ version: MCP_VERSION,
8025
8345
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
8026
8346
  },
8027
8347
  data
@@ -8032,7 +8352,12 @@ function registerInsightsTools(server2) {
8032
8352
  "get_performance_insights",
8033
8353
  "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.",
8034
8354
  {
8035
- insight_type: z8.enum(["top_hooks", "optimal_timing", "best_models", "competitor_patterns"]).optional().describe("Filter to a specific insight type."),
8355
+ insight_type: z8.enum([
8356
+ "top_hooks",
8357
+ "optimal_timing",
8358
+ "best_models",
8359
+ "competitor_patterns"
8360
+ ]).optional().describe("Filter to a specific insight type."),
8036
8361
  days: z8.number().min(1).max(90).optional().describe("Number of days to look back. Defaults to 30. Max 90."),
8037
8362
  limit: z8.number().min(1).max(50).optional().describe("Maximum number of insights to return. Defaults to 10."),
8038
8363
  response_format: z8.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
@@ -8051,10 +8376,13 @@ function registerInsightsTools(server2) {
8051
8376
  projectIds.push(...projects.map((p) => p.id));
8052
8377
  }
8053
8378
  }
8379
+ const effectiveDays = Math.min(lookbackDays, MAX_INSIGHT_AGE_DAYS);
8054
8380
  const since = /* @__PURE__ */ new Date();
8055
- since.setDate(since.getDate() - lookbackDays);
8381
+ since.setDate(since.getDate() - effectiveDays);
8056
8382
  const sinceIso = since.toISOString();
8057
- 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);
8383
+ let query = supabase.from("performance_insights").select(
8384
+ "id, project_id, insight_type, insight_data, confidence_score, generated_at"
8385
+ ).gte("generated_at", sinceIso).order("generated_at", { ascending: false }).limit(maxRows);
8058
8386
  if (projectIds.length > 0) {
8059
8387
  query = query.in("project_id", projectIds);
8060
8388
  } else {
@@ -8410,10 +8738,11 @@ function registerYouTubeAnalyticsTools(server2) {
8410
8738
  init_edge_function();
8411
8739
  import { z as z10 } from "zod";
8412
8740
  init_supabase();
8741
+ init_version();
8413
8742
  function asEnvelope6(data) {
8414
8743
  return {
8415
8744
  _meta: {
8416
- version: "0.2.0",
8745
+ version: MCP_VERSION,
8417
8746
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
8418
8747
  },
8419
8748
  data
@@ -8424,7 +8753,9 @@ function registerCommentsTools(server2) {
8424
8753
  "list_comments",
8425
8754
  "List YouTube comments. Without a video_id, returns recent comments across all channel videos. With a video_id, returns comments for that specific video.",
8426
8755
  {
8427
- video_id: z10.string().optional().describe("YouTube video ID. If omitted, returns comments across all channel videos."),
8756
+ video_id: z10.string().optional().describe(
8757
+ "YouTube video ID. If omitted, returns comments across all channel videos."
8758
+ ),
8428
8759
  max_results: z10.number().min(1).max(100).optional().describe("Maximum number of comments to return. Defaults to 50."),
8429
8760
  page_token: z10.string().optional().describe("Pagination token from a previous list_comments call."),
8430
8761
  response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
@@ -8439,7 +8770,9 @@ function registerCommentsTools(server2) {
8439
8770
  });
8440
8771
  if (error) {
8441
8772
  return {
8442
- content: [{ type: "text", text: `Error listing comments: ${error}` }],
8773
+ content: [
8774
+ { type: "text", text: `Error listing comments: ${error}` }
8775
+ ],
8443
8776
  isError: true
8444
8777
  };
8445
8778
  }
@@ -8451,7 +8784,10 @@ function registerCommentsTools(server2) {
8451
8784
  {
8452
8785
  type: "text",
8453
8786
  text: JSON.stringify(
8454
- asEnvelope6({ comments, nextPageToken: result.nextPageToken ?? null }),
8787
+ asEnvelope6({
8788
+ comments,
8789
+ nextPageToken: result.nextPageToken ?? null
8790
+ }),
8455
8791
  null,
8456
8792
  2
8457
8793
  )
@@ -8488,7 +8824,9 @@ function registerCommentsTools(server2) {
8488
8824
  "reply_to_comment",
8489
8825
  "Reply to a YouTube comment. Requires the parent comment ID and reply text.",
8490
8826
  {
8491
- parent_id: z10.string().describe("The ID of the parent comment to reply to (from list_comments)."),
8827
+ parent_id: z10.string().describe(
8828
+ "The ID of the parent comment to reply to (from list_comments)."
8829
+ ),
8492
8830
  text: z10.string().min(1).describe("The reply text."),
8493
8831
  response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8494
8832
  },
@@ -8527,7 +8865,12 @@ function registerCommentsTools(server2) {
8527
8865
  details: { error }
8528
8866
  });
8529
8867
  return {
8530
- content: [{ type: "text", text: `Error replying to comment: ${error}` }],
8868
+ content: [
8869
+ {
8870
+ type: "text",
8871
+ text: `Error replying to comment: ${error}`
8872
+ }
8873
+ ],
8531
8874
  isError: true
8532
8875
  };
8533
8876
  }
@@ -8540,7 +8883,12 @@ function registerCommentsTools(server2) {
8540
8883
  });
8541
8884
  if (format === "json") {
8542
8885
  return {
8543
- content: [{ type: "text", text: JSON.stringify(asEnvelope6(result), null, 2) }]
8886
+ content: [
8887
+ {
8888
+ type: "text",
8889
+ text: JSON.stringify(asEnvelope6(result), null, 2)
8890
+ }
8891
+ ]
8544
8892
  };
8545
8893
  }
8546
8894
  return {
@@ -8598,7 +8946,9 @@ function registerCommentsTools(server2) {
8598
8946
  details: { error }
8599
8947
  });
8600
8948
  return {
8601
- content: [{ type: "text", text: `Error posting comment: ${error}` }],
8949
+ content: [
8950
+ { type: "text", text: `Error posting comment: ${error}` }
8951
+ ],
8602
8952
  isError: true
8603
8953
  };
8604
8954
  }
@@ -8611,7 +8961,12 @@ function registerCommentsTools(server2) {
8611
8961
  });
8612
8962
  if (format === "json") {
8613
8963
  return {
8614
- content: [{ type: "text", text: JSON.stringify(asEnvelope6(result), null, 2) }]
8964
+ content: [
8965
+ {
8966
+ type: "text",
8967
+ text: JSON.stringify(asEnvelope6(result), null, 2)
8968
+ }
8969
+ ]
8615
8970
  };
8616
8971
  }
8617
8972
  return {
@@ -8669,7 +9024,12 @@ function registerCommentsTools(server2) {
8669
9024
  details: { error }
8670
9025
  });
8671
9026
  return {
8672
- content: [{ type: "text", text: `Error moderating comment: ${error}` }],
9027
+ content: [
9028
+ {
9029
+ type: "text",
9030
+ text: `Error moderating comment: ${error}`
9031
+ }
9032
+ ],
8673
9033
  isError: true
8674
9034
  };
8675
9035
  }
@@ -8748,7 +9108,9 @@ function registerCommentsTools(server2) {
8748
9108
  details: { error }
8749
9109
  });
8750
9110
  return {
8751
- content: [{ type: "text", text: `Error deleting comment: ${error}` }],
9111
+ content: [
9112
+ { type: "text", text: `Error deleting comment: ${error}` }
9113
+ ],
8752
9114
  isError: true
8753
9115
  };
8754
9116
  }
@@ -8763,13 +9125,22 @@ function registerCommentsTools(server2) {
8763
9125
  content: [
8764
9126
  {
8765
9127
  type: "text",
8766
- text: JSON.stringify(asEnvelope6({ success: true, commentId: comment_id }), null, 2)
9128
+ text: JSON.stringify(
9129
+ asEnvelope6({ success: true, commentId: comment_id }),
9130
+ null,
9131
+ 2
9132
+ )
8767
9133
  }
8768
9134
  ]
8769
9135
  };
8770
9136
  }
8771
9137
  return {
8772
- content: [{ type: "text", text: `Comment ${comment_id} deleted successfully.` }]
9138
+ content: [
9139
+ {
9140
+ type: "text",
9141
+ text: `Comment ${comment_id} deleted successfully.`
9142
+ }
9143
+ ]
8773
9144
  };
8774
9145
  }
8775
9146
  );
@@ -8778,6 +9149,7 @@ function registerCommentsTools(server2) {
8778
9149
  // src/tools/ideation-context.ts
8779
9150
  init_supabase();
8780
9151
  import { z as z11 } from "zod";
9152
+ init_version();
8781
9153
  function transformInsightsToPerformanceContext(projectId, insights) {
8782
9154
  if (!insights.length) {
8783
9155
  return {
@@ -8797,8 +9169,12 @@ function transformInsightsToPerformanceContext(projectId, insights) {
8797
9169
  };
8798
9170
  }
8799
9171
  const topHooksInsight = insights.find((i) => i.insight_type === "top_hooks");
8800
- const optimalTimingInsight = insights.find((i) => i.insight_type === "optimal_timing");
8801
- const bestModelsInsight = insights.find((i) => i.insight_type === "best_models");
9172
+ const optimalTimingInsight = insights.find(
9173
+ (i) => i.insight_type === "optimal_timing"
9174
+ );
9175
+ const bestModelsInsight = insights.find(
9176
+ (i) => i.insight_type === "best_models"
9177
+ );
8802
9178
  const topHooks = topHooksInsight?.insight_data?.hooks || [];
8803
9179
  const hooksSummary = topHooksInsight?.insight_data?.summary || "";
8804
9180
  const timingSummary = optimalTimingInsight?.insight_data?.summary || "";
@@ -8809,7 +9185,10 @@ function transformInsightsToPerformanceContext(projectId, insights) {
8809
9185
  if (hooksSummary) promptParts.push(hooksSummary);
8810
9186
  if (timingSummary) promptParts.push(timingSummary);
8811
9187
  if (modelSummary) promptParts.push(modelSummary);
8812
- if (topHooks.length) promptParts.push(`Top performing hooks: ${topHooks.slice(0, 3).join(", ")}`);
9188
+ if (topHooks.length)
9189
+ promptParts.push(
9190
+ `Top performing hooks: ${topHooks.slice(0, 3).join(", ")}`
9191
+ );
8813
9192
  return {
8814
9193
  projectId,
8815
9194
  hasHistoricalData: true,
@@ -8835,7 +9214,7 @@ function transformInsightsToPerformanceContext(projectId, insights) {
8835
9214
  function asEnvelope7(data) {
8836
9215
  return {
8837
9216
  _meta: {
8838
- version: "0.2.0",
9217
+ version: MCP_VERSION,
8839
9218
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
8840
9219
  },
8841
9220
  data
@@ -8858,7 +9237,12 @@ function registerIdeationContextTools(server2) {
8858
9237
  const { data: member } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).limit(1).single();
8859
9238
  if (!member?.organization_id) {
8860
9239
  return {
8861
- content: [{ type: "text", text: "No organization found for current user." }],
9240
+ content: [
9241
+ {
9242
+ type: "text",
9243
+ text: "No organization found for current user."
9244
+ }
9245
+ ],
8862
9246
  isError: true
8863
9247
  };
8864
9248
  }
@@ -8866,7 +9250,10 @@ function registerIdeationContextTools(server2) {
8866
9250
  if (projectsError) {
8867
9251
  return {
8868
9252
  content: [
8869
- { type: "text", text: `Failed to resolve projects: ${projectsError.message}` }
9253
+ {
9254
+ type: "text",
9255
+ text: `Failed to resolve projects: ${projectsError.message}`
9256
+ }
8870
9257
  ],
8871
9258
  isError: true
8872
9259
  };
@@ -8874,7 +9261,12 @@ function registerIdeationContextTools(server2) {
8874
9261
  const projectIds = (projects || []).map((p) => p.id);
8875
9262
  if (projectIds.length === 0) {
8876
9263
  return {
8877
- content: [{ type: "text", text: "No projects found for current user." }],
9264
+ content: [
9265
+ {
9266
+ type: "text",
9267
+ text: "No projects found for current user."
9268
+ }
9269
+ ],
8878
9270
  isError: true
8879
9271
  };
8880
9272
  }
@@ -8883,7 +9275,10 @@ function registerIdeationContextTools(server2) {
8883
9275
  if (!selectedProjectId) {
8884
9276
  return {
8885
9277
  content: [
8886
- { type: "text", text: "No accessible project found for current user." }
9278
+ {
9279
+ type: "text",
9280
+ text: "No accessible project found for current user."
9281
+ }
8887
9282
  ],
8888
9283
  isError: true
8889
9284
  };
@@ -8901,7 +9296,9 @@ function registerIdeationContextTools(server2) {
8901
9296
  }
8902
9297
  const since = /* @__PURE__ */ new Date();
8903
9298
  since.setDate(since.getDate() - lookbackDays);
8904
- 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);
9299
+ const { data: insights, error } = await supabase.from("performance_insights").select(
9300
+ "id, project_id, insight_type, insight_data, generated_at, expires_at"
9301
+ ).eq("project_id", selectedProjectId).gte("generated_at", since.toISOString()).gt("expires_at", (/* @__PURE__ */ new Date()).toISOString()).order("generated_at", { ascending: false }).limit(30);
8905
9302
  if (error) {
8906
9303
  return {
8907
9304
  content: [
@@ -8919,7 +9316,12 @@ function registerIdeationContextTools(server2) {
8919
9316
  );
8920
9317
  if (format === "json") {
8921
9318
  return {
8922
- content: [{ type: "text", text: JSON.stringify(asEnvelope7(context), null, 2) }]
9319
+ content: [
9320
+ {
9321
+ type: "text",
9322
+ text: JSON.stringify(asEnvelope7(context), null, 2)
9323
+ }
9324
+ ]
8923
9325
  };
8924
9326
  }
8925
9327
  const lines = [
@@ -8940,10 +9342,11 @@ function registerIdeationContextTools(server2) {
8940
9342
  // src/tools/credits.ts
8941
9343
  init_supabase();
8942
9344
  import { z as z12 } from "zod";
9345
+ init_version();
8943
9346
  function asEnvelope8(data) {
8944
9347
  return {
8945
9348
  _meta: {
8946
- version: "0.2.0",
9349
+ version: MCP_VERSION,
8947
9350
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
8948
9351
  },
8949
9352
  data
@@ -8982,7 +9385,12 @@ function registerCreditsTools(server2) {
8982
9385
  };
8983
9386
  if ((response_format || "text") === "json") {
8984
9387
  return {
8985
- content: [{ type: "text", text: JSON.stringify(asEnvelope8(payload), null, 2) }]
9388
+ content: [
9389
+ {
9390
+ type: "text",
9391
+ text: JSON.stringify(asEnvelope8(payload), null, 2)
9392
+ }
9393
+ ]
8986
9394
  };
8987
9395
  }
8988
9396
  return {
@@ -9016,7 +9424,12 @@ Monthly used: ${payload.monthlyUsed}` + (payload.monthlyLimit ? ` / ${payload.mo
9016
9424
  };
9017
9425
  if ((response_format || "text") === "json") {
9018
9426
  return {
9019
- content: [{ type: "text", text: JSON.stringify(asEnvelope8(payload), null, 2) }]
9427
+ content: [
9428
+ {
9429
+ type: "text",
9430
+ text: JSON.stringify(asEnvelope8(payload), null, 2)
9431
+ }
9432
+ ]
9020
9433
  };
9021
9434
  }
9022
9435
  return {
@@ -9039,11 +9452,12 @@ Assets remaining: ${payload.remainingAssets ?? "unlimited"}`
9039
9452
 
9040
9453
  // src/tools/loop-summary.ts
9041
9454
  init_supabase();
9455
+ init_version();
9042
9456
  import { z as z13 } from "zod";
9043
9457
  function asEnvelope9(data) {
9044
9458
  return {
9045
9459
  _meta: {
9046
- version: "0.2.0",
9460
+ version: MCP_VERSION,
9047
9461
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
9048
9462
  },
9049
9463
  data
@@ -9082,7 +9496,12 @@ function registerLoopSummaryTools(server2) {
9082
9496
  const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
9083
9497
  if (!membership) {
9084
9498
  return {
9085
- content: [{ type: "text", text: "Project is not accessible to current user." }],
9499
+ content: [
9500
+ {
9501
+ type: "text",
9502
+ text: "Project is not accessible to current user."
9503
+ }
9504
+ ],
9086
9505
  isError: true
9087
9506
  };
9088
9507
  }
@@ -9105,7 +9524,12 @@ function registerLoopSummaryTools(server2) {
9105
9524
  };
9106
9525
  if ((response_format || "text") === "json") {
9107
9526
  return {
9108
- content: [{ type: "text", text: JSON.stringify(asEnvelope9(payload), null, 2) }]
9527
+ content: [
9528
+ {
9529
+ type: "text",
9530
+ text: JSON.stringify(asEnvelope9(payload), null, 2)
9531
+ }
9532
+ ]
9109
9533
  };
9110
9534
  }
9111
9535
  return {
@@ -9435,8 +9859,12 @@ ${"=".repeat(40)}
9435
9859
  init_edge_function();
9436
9860
  init_supabase();
9437
9861
  import { z as z16 } from "zod";
9862
+ init_version();
9438
9863
  function asEnvelope12(data) {
9439
- return { _meta: { version: "0.2.0", timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
9864
+ return {
9865
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
9866
+ data
9867
+ };
9440
9868
  }
9441
9869
  function isYouTubeUrl(url) {
9442
9870
  if (/youtube\.com\/watch|youtu\.be\//.test(url)) return "video";
@@ -9467,13 +9895,17 @@ Metadata:`);
9467
9895
  if (m.tags?.length) lines.push(` Tags: ${m.tags.join(", ")}`);
9468
9896
  }
9469
9897
  if (content.features?.length)
9470
- lines.push(`
9898
+ lines.push(
9899
+ `
9471
9900
  Features:
9472
- ${content.features.map((f) => ` - ${f}`).join("\n")}`);
9901
+ ${content.features.map((f) => ` - ${f}`).join("\n")}`
9902
+ );
9473
9903
  if (content.benefits?.length)
9474
- lines.push(`
9904
+ lines.push(
9905
+ `
9475
9906
  Benefits:
9476
- ${content.benefits.map((b) => ` - ${b}`).join("\n")}`);
9907
+ ${content.benefits.map((b) => ` - ${b}`).join("\n")}`
9908
+ );
9477
9909
  if (content.usp) lines.push(`
9478
9910
  USP: ${content.usp}`);
9479
9911
  if (content.suggested_hooks?.length)
@@ -9495,12 +9927,20 @@ function registerExtractionTools(server2) {
9495
9927
  max_results: z16.number().min(1).max(100).default(10).describe("Max comments to include"),
9496
9928
  response_format: z16.enum(["text", "json"]).default("text")
9497
9929
  },
9498
- async ({ url, extract_type, include_comments, max_results, response_format }) => {
9930
+ async ({
9931
+ url,
9932
+ extract_type,
9933
+ include_comments,
9934
+ max_results,
9935
+ response_format
9936
+ }) => {
9499
9937
  const startedAt = Date.now();
9500
9938
  const ssrfCheck = await validateUrlForSSRF(url);
9501
9939
  if (!ssrfCheck.isValid) {
9502
9940
  return {
9503
- content: [{ type: "text", text: `URL blocked: ${ssrfCheck.error}` }],
9941
+ content: [
9942
+ { type: "text", text: `URL blocked: ${ssrfCheck.error}` }
9943
+ ],
9504
9944
  isError: true
9505
9945
  };
9506
9946
  }
@@ -9628,13 +10068,21 @@ function registerExtractionTools(server2) {
9628
10068
  if (response_format === "json") {
9629
10069
  return {
9630
10070
  content: [
9631
- { type: "text", text: JSON.stringify(asEnvelope12(extracted), null, 2) }
10071
+ {
10072
+ type: "text",
10073
+ text: JSON.stringify(asEnvelope12(extracted), null, 2)
10074
+ }
9632
10075
  ],
9633
10076
  isError: false
9634
10077
  };
9635
10078
  }
9636
10079
  return {
9637
- content: [{ type: "text", text: formatExtractedContentAsText(extracted) }],
10080
+ content: [
10081
+ {
10082
+ type: "text",
10083
+ text: formatExtractedContentAsText(extracted)
10084
+ }
10085
+ ],
9638
10086
  isError: false
9639
10087
  };
9640
10088
  } catch (err) {
@@ -9647,7 +10095,9 @@ function registerExtractionTools(server2) {
9647
10095
  details: { url, error: message }
9648
10096
  });
9649
10097
  return {
9650
- content: [{ type: "text", text: `Extraction failed: ${message}` }],
10098
+ content: [
10099
+ { type: "text", text: `Extraction failed: ${message}` }
10100
+ ],
9651
10101
  isError: true
9652
10102
  };
9653
10103
  }
@@ -9658,9 +10108,13 @@ function registerExtractionTools(server2) {
9658
10108
  // src/tools/quality.ts
9659
10109
  init_quality();
9660
10110
  init_supabase();
10111
+ init_version();
9661
10112
  import { z as z17 } from "zod";
9662
10113
  function asEnvelope13(data) {
9663
- return { _meta: { version: "0.2.0", timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
10114
+ return {
10115
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
10116
+ data
10117
+ };
9664
10118
  }
9665
10119
  function registerQualityTools(server2) {
9666
10120
  server2.tool(
@@ -9716,7 +10170,12 @@ function registerQualityTools(server2) {
9716
10170
  });
9717
10171
  if (response_format === "json") {
9718
10172
  return {
9719
- content: [{ type: "text", text: JSON.stringify(asEnvelope13(result), null, 2) }],
10173
+ content: [
10174
+ {
10175
+ type: "text",
10176
+ text: JSON.stringify(asEnvelope13(result), null, 2)
10177
+ }
10178
+ ],
9720
10179
  isError: false
9721
10180
  };
9722
10181
  }
@@ -9726,7 +10185,9 @@ function registerQualityTools(server2) {
9726
10185
  );
9727
10186
  lines.push("");
9728
10187
  for (const cat of result.categories) {
9729
- lines.push(` ${cat.name}: ${cat.score}/${cat.maxScore} \u2014 ${cat.detail}`);
10188
+ lines.push(
10189
+ ` ${cat.name}: ${cat.score}/${cat.maxScore} \u2014 ${cat.detail}`
10190
+ );
9730
10191
  }
9731
10192
  if (result.blockers.length > 0) {
9732
10193
  lines.push("");
@@ -9737,7 +10198,10 @@ function registerQualityTools(server2) {
9737
10198
  }
9738
10199
  lines.push("");
9739
10200
  lines.push(`Threshold: ${result.threshold}/${result.maxTotal}`);
9740
- return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
10201
+ return {
10202
+ content: [{ type: "text", text: lines.join("\n") }],
10203
+ isError: false
10204
+ };
9741
10205
  }
9742
10206
  );
9743
10207
  server2.tool(
@@ -9778,7 +10242,9 @@ function registerQualityTools(server2) {
9778
10242
  });
9779
10243
  const scores = postsWithQuality.map((p) => p.quality.score);
9780
10244
  const passed = postsWithQuality.filter((p) => p.quality.passed).length;
9781
- const avgScore = scores.length > 0 ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length * 10) / 10 : 0;
10245
+ const avgScore = scores.length > 0 ? Math.round(
10246
+ scores.reduce((a, b) => a + b, 0) / scores.length * 10
10247
+ ) / 10 : 0;
9782
10248
  const summary = {
9783
10249
  total_posts: plan.posts.length,
9784
10250
  passed,
@@ -9797,25 +10263,36 @@ function registerQualityTools(server2) {
9797
10263
  content: [
9798
10264
  {
9799
10265
  type: "text",
9800
- text: JSON.stringify(asEnvelope13({ posts: postsWithQuality, summary }), null, 2)
10266
+ text: JSON.stringify(
10267
+ asEnvelope13({ posts: postsWithQuality, summary }),
10268
+ null,
10269
+ 2
10270
+ )
9801
10271
  }
9802
10272
  ],
9803
10273
  isError: false
9804
10274
  };
9805
10275
  }
9806
10276
  const lines = [];
9807
- lines.push(`PLAN QUALITY: ${passed}/${plan.posts.length} passed (avg: ${avgScore}/35)`);
10277
+ lines.push(
10278
+ `PLAN QUALITY: ${passed}/${plan.posts.length} passed (avg: ${avgScore}/35)`
10279
+ );
9808
10280
  lines.push("");
9809
10281
  for (const post of postsWithQuality) {
9810
10282
  const icon = post.quality.passed ? "[PASS]" : "[FAIL]";
9811
- lines.push(`${icon} ${post.id} | ${post.platform} | ${post.quality.score}/35`);
10283
+ lines.push(
10284
+ `${icon} ${post.id} | ${post.platform} | ${post.quality.score}/35`
10285
+ );
9812
10286
  if (post.quality.blockers.length > 0) {
9813
10287
  for (const b of post.quality.blockers) {
9814
10288
  lines.push(` - ${b}`);
9815
10289
  }
9816
10290
  }
9817
10291
  }
9818
- return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
10292
+ return {
10293
+ content: [{ type: "text", text: lines.join("\n") }],
10294
+ isError: false
10295
+ };
9819
10296
  }
9820
10297
  );
9821
10298
  }
@@ -9825,11 +10302,15 @@ init_edge_function();
9825
10302
  init_supabase();
9826
10303
  import { z as z18 } from "zod";
9827
10304
  import { randomUUID as randomUUID2 } from "node:crypto";
10305
+ init_version();
9828
10306
  function toRecord(value) {
9829
10307
  return value && typeof value === "object" ? value : void 0;
9830
10308
  }
9831
10309
  function asEnvelope14(data) {
9832
- return { _meta: { version: "0.2.0", timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
10310
+ return {
10311
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
10312
+ data
10313
+ };
9833
10314
  }
9834
10315
  function tomorrowIsoDate() {
9835
10316
  const d = /* @__PURE__ */ new Date();
@@ -9867,7 +10348,9 @@ function formatPlanAsText(plan) {
9867
10348
  lines.push(`WEEKLY CONTENT PLAN: "${plan.topic}"`);
9868
10349
  lines.push(`Period: ${plan.start_date} to ${plan.end_date}`);
9869
10350
  lines.push(`Platforms: ${plan.platforms.join(", ")}`);
9870
- lines.push(`Posts: ${plan.posts.length} | Estimated credits: ~${plan.estimated_credits}`);
10351
+ lines.push(
10352
+ `Posts: ${plan.posts.length} | Estimated credits: ~${plan.estimated_credits}`
10353
+ );
9871
10354
  if (plan.plan_id) lines.push(`Plan ID: ${plan.plan_id}`);
9872
10355
  if (plan.insights_applied?.has_historical_data) {
9873
10356
  lines.push("");
@@ -9882,7 +10365,9 @@ function formatPlanAsText(plan) {
9882
10365
  `- Best posting time: ${days[timing.dayOfWeek] ?? timing.dayOfWeek} ${timing.hourOfDay}:00`
9883
10366
  );
9884
10367
  }
9885
- lines.push(`- Recommended model: ${plan.insights_applied.recommended_model ?? "N/A"}`);
10368
+ lines.push(
10369
+ `- Recommended model: ${plan.insights_applied.recommended_model ?? "N/A"}`
10370
+ );
9886
10371
  lines.push(`- Insights count: ${plan.insights_applied.insights_count}`);
9887
10372
  }
9888
10373
  lines.push("");
@@ -9903,9 +10388,11 @@ function formatPlanAsText(plan) {
9903
10388
  ` Caption: ${post.caption.slice(0, 200)}${post.caption.length > 200 ? "..." : ""}`
9904
10389
  );
9905
10390
  if (post.title) lines.push(` Title: ${post.title}`);
9906
- if (post.visual_direction) lines.push(` Visual: ${post.visual_direction}`);
10391
+ if (post.visual_direction)
10392
+ lines.push(` Visual: ${post.visual_direction}`);
9907
10393
  if (post.media_type) lines.push(` Media: ${post.media_type}`);
9908
- if (post.hashtags?.length) lines.push(` Hashtags: ${post.hashtags.join(" ")}`);
10394
+ if (post.hashtags?.length)
10395
+ lines.push(` Hashtags: ${post.hashtags.join(" ")}`);
9909
10396
  lines.push("");
9910
10397
  }
9911
10398
  }
@@ -9988,7 +10475,10 @@ function registerPlanningTools(server2) {
9988
10475
  "mcp-data",
9989
10476
  {
9990
10477
  action: "brand-profile",
9991
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
10478
+ ...resolvedProjectId ? {
10479
+ projectId: resolvedProjectId,
10480
+ project_id: resolvedProjectId
10481
+ } : {}
9992
10482
  },
9993
10483
  { timeoutMs: 15e3 }
9994
10484
  );
@@ -10192,7 +10682,12 @@ ${rawText.slice(0, 1e3)}`
10192
10682
  details: { topic, error: `plan persistence failed: ${message}` }
10193
10683
  });
10194
10684
  return {
10195
- content: [{ type: "text", text: `Plan persistence failed: ${message}` }],
10685
+ content: [
10686
+ {
10687
+ type: "text",
10688
+ text: `Plan persistence failed: ${message}`
10689
+ }
10690
+ ],
10196
10691
  isError: true
10197
10692
  };
10198
10693
  }
@@ -10206,7 +10701,12 @@ ${rawText.slice(0, 1e3)}`
10206
10701
  });
10207
10702
  if (response_format === "json") {
10208
10703
  return {
10209
- content: [{ type: "text", text: JSON.stringify(asEnvelope14(plan), null, 2) }],
10704
+ content: [
10705
+ {
10706
+ type: "text",
10707
+ text: JSON.stringify(asEnvelope14(plan), null, 2)
10708
+ }
10709
+ ],
10210
10710
  isError: false
10211
10711
  };
10212
10712
  }
@@ -10224,7 +10724,12 @@ ${rawText.slice(0, 1e3)}`
10224
10724
  details: { topic, error: message }
10225
10725
  });
10226
10726
  return {
10227
- content: [{ type: "text", text: `Plan generation failed: ${message}` }],
10727
+ content: [
10728
+ {
10729
+ type: "text",
10730
+ text: `Plan generation failed: ${message}`
10731
+ }
10732
+ ],
10228
10733
  isError: true
10229
10734
  };
10230
10735
  }
@@ -10284,7 +10789,11 @@ ${rawText.slice(0, 1e3)}`
10284
10789
  toolName: "save_content_plan",
10285
10790
  status: "success",
10286
10791
  durationMs,
10287
- details: { plan_id: planId, project_id: resolvedProjectId, status: normalizedStatus }
10792
+ details: {
10793
+ plan_id: planId,
10794
+ project_id: resolvedProjectId,
10795
+ status: normalizedStatus
10796
+ }
10288
10797
  });
10289
10798
  const result = {
10290
10799
  plan_id: planId,
@@ -10293,13 +10802,21 @@ ${rawText.slice(0, 1e3)}`
10293
10802
  };
10294
10803
  if (response_format === "json") {
10295
10804
  return {
10296
- content: [{ type: "text", text: JSON.stringify(asEnvelope14(result), null, 2) }],
10805
+ content: [
10806
+ {
10807
+ type: "text",
10808
+ text: JSON.stringify(asEnvelope14(result), null, 2)
10809
+ }
10810
+ ],
10297
10811
  isError: false
10298
10812
  };
10299
10813
  }
10300
10814
  return {
10301
10815
  content: [
10302
- { type: "text", text: `Saved content plan ${planId} (${normalizedStatus}).` }
10816
+ {
10817
+ type: "text",
10818
+ text: `Saved content plan ${planId} (${normalizedStatus}).`
10819
+ }
10303
10820
  ],
10304
10821
  isError: false
10305
10822
  };
@@ -10313,7 +10830,12 @@ ${rawText.slice(0, 1e3)}`
10313
10830
  details: { error: message }
10314
10831
  });
10315
10832
  return {
10316
- content: [{ type: "text", text: `Failed to save content plan: ${message}` }],
10833
+ content: [
10834
+ {
10835
+ type: "text",
10836
+ text: `Failed to save content plan: ${message}`
10837
+ }
10838
+ ],
10317
10839
  isError: true
10318
10840
  };
10319
10841
  }
@@ -10329,7 +10851,9 @@ ${rawText.slice(0, 1e3)}`
10329
10851
  async ({ plan_id, response_format }) => {
10330
10852
  const supabase = getSupabaseClient();
10331
10853
  const userId = await getDefaultUserId();
10332
- 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();
10854
+ const { data, error } = await supabase.from("content_plans").select(
10855
+ "id, topic, status, plan_payload, insights_applied, created_at, updated_at"
10856
+ ).eq("id", plan_id).eq("user_id", userId).maybeSingle();
10333
10857
  if (error) {
10334
10858
  return {
10335
10859
  content: [
@@ -10344,7 +10868,10 @@ ${rawText.slice(0, 1e3)}`
10344
10868
  if (!data) {
10345
10869
  return {
10346
10870
  content: [
10347
- { type: "text", text: `No content plan found for plan_id=${plan_id}` }
10871
+ {
10872
+ type: "text",
10873
+ text: `No content plan found for plan_id=${plan_id}`
10874
+ }
10348
10875
  ],
10349
10876
  isError: true
10350
10877
  };
@@ -10360,7 +10887,12 @@ ${rawText.slice(0, 1e3)}`
10360
10887
  };
10361
10888
  if (response_format === "json") {
10362
10889
  return {
10363
- content: [{ type: "text", text: JSON.stringify(asEnvelope14(payload), null, 2) }],
10890
+ content: [
10891
+ {
10892
+ type: "text",
10893
+ text: JSON.stringify(asEnvelope14(payload), null, 2)
10894
+ }
10895
+ ],
10364
10896
  isError: false
10365
10897
  };
10366
10898
  }
@@ -10371,7 +10903,10 @@ ${rawText.slice(0, 1e3)}`
10371
10903
  `Status: ${data.status}`,
10372
10904
  `Posts: ${Array.isArray(plan?.posts) ? plan.posts.length : 0}`
10373
10905
  ];
10374
- return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
10906
+ return {
10907
+ content: [{ type: "text", text: lines.join("\n") }],
10908
+ isError: false
10909
+ };
10375
10910
  }
10376
10911
  );
10377
10912
  server2.tool(
@@ -10414,14 +10949,19 @@ ${rawText.slice(0, 1e3)}`
10414
10949
  if (!stored?.plan_payload) {
10415
10950
  return {
10416
10951
  content: [
10417
- { type: "text", text: `No content plan found for plan_id=${plan_id}` }
10952
+ {
10953
+ type: "text",
10954
+ text: `No content plan found for plan_id=${plan_id}`
10955
+ }
10418
10956
  ],
10419
10957
  isError: true
10420
10958
  };
10421
10959
  }
10422
10960
  const plan = stored.plan_payload;
10423
10961
  const existingPosts = Array.isArray(plan.posts) ? plan.posts : [];
10424
- const updatesById = new Map(post_updates.map((update) => [update.post_id, update]));
10962
+ const updatesById = new Map(
10963
+ post_updates.map((update) => [update.post_id, update])
10964
+ );
10425
10965
  const updatedPosts = existingPosts.map((post) => {
10426
10966
  const update = updatesById.get(post.id);
10427
10967
  if (!update) return post;
@@ -10439,7 +10979,9 @@ ${rawText.slice(0, 1e3)}`
10439
10979
  ...update.status !== void 0 ? { status: update.status } : {}
10440
10980
  };
10441
10981
  });
10442
- const nextStatus = updatedPosts.length > 0 && updatedPosts.every((post) => post.status === "approved" || post.status === "edited") ? "approved" : stored.status === "scheduled" || stored.status === "completed" ? stored.status : "draft";
10982
+ const nextStatus = updatedPosts.length > 0 && updatedPosts.every(
10983
+ (post) => post.status === "approved" || post.status === "edited"
10984
+ ) ? "approved" : stored.status === "scheduled" || stored.status === "completed" ? stored.status : "draft";
10443
10985
  const updatedPlan = {
10444
10986
  ...plan,
10445
10987
  posts: updatedPosts
@@ -10466,7 +11008,12 @@ ${rawText.slice(0, 1e3)}`
10466
11008
  };
10467
11009
  if (response_format === "json") {
10468
11010
  return {
10469
- content: [{ type: "text", text: JSON.stringify(asEnvelope14(payload), null, 2) }],
11011
+ content: [
11012
+ {
11013
+ type: "text",
11014
+ text: JSON.stringify(asEnvelope14(payload), null, 2)
11015
+ }
11016
+ ],
10470
11017
  isError: false
10471
11018
  };
10472
11019
  }
@@ -10506,7 +11053,10 @@ ${rawText.slice(0, 1e3)}`
10506
11053
  if (!stored?.plan_payload || !stored.project_id) {
10507
11054
  return {
10508
11055
  content: [
10509
- { type: "text", text: `No content plan found for plan_id=${plan_id}` }
11056
+ {
11057
+ type: "text",
11058
+ text: `No content plan found for plan_id=${plan_id}`
11059
+ }
10510
11060
  ],
10511
11061
  isError: true
10512
11062
  };
@@ -10515,7 +11065,12 @@ ${rawText.slice(0, 1e3)}`
10515
11065
  const posts = Array.isArray(plan.posts) ? plan.posts : [];
10516
11066
  if (posts.length === 0) {
10517
11067
  return {
10518
- content: [{ type: "text", text: `Plan ${plan_id} has no posts to submit.` }],
11068
+ content: [
11069
+ {
11070
+ type: "text",
11071
+ text: `Plan ${plan_id} has no posts to submit.`
11072
+ }
11073
+ ],
10519
11074
  isError: true
10520
11075
  };
10521
11076
  }
@@ -10558,7 +11113,12 @@ ${rawText.slice(0, 1e3)}`
10558
11113
  };
10559
11114
  if (response_format === "json") {
10560
11115
  return {
10561
- content: [{ type: "text", text: JSON.stringify(asEnvelope14(payload), null, 2) }],
11116
+ content: [
11117
+ {
11118
+ type: "text",
11119
+ text: JSON.stringify(asEnvelope14(payload), null, 2)
11120
+ }
11121
+ ],
10562
11122
  isError: false
10563
11123
  };
10564
11124
  }
@@ -10578,10 +11138,11 @@ ${rawText.slice(0, 1e3)}`
10578
11138
  // src/tools/plan-approvals.ts
10579
11139
  init_supabase();
10580
11140
  import { z as z19 } from "zod";
11141
+ init_version();
10581
11142
  function asEnvelope15(data) {
10582
11143
  return {
10583
11144
  _meta: {
10584
- version: "0.2.0",
11145
+ version: MCP_VERSION,
10585
11146
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
10586
11147
  },
10587
11148
  data
@@ -10620,14 +11181,24 @@ function registerPlanApprovalTools(server2) {
10620
11181
  if (!projectId) {
10621
11182
  return {
10622
11183
  content: [
10623
- { type: "text", text: "No project_id provided and no default project found." }
11184
+ {
11185
+ type: "text",
11186
+ text: "No project_id provided and no default project found."
11187
+ }
10624
11188
  ],
10625
11189
  isError: true
10626
11190
  };
10627
11191
  }
10628
- const accessError = await assertProjectAccess(supabase, userId, projectId);
11192
+ const accessError = await assertProjectAccess(
11193
+ supabase,
11194
+ userId,
11195
+ projectId
11196
+ );
10629
11197
  if (accessError) {
10630
- return { content: [{ type: "text", text: accessError }], isError: true };
11198
+ return {
11199
+ content: [{ type: "text", text: accessError }],
11200
+ isError: true
11201
+ };
10631
11202
  }
10632
11203
  const rows = posts.map((post) => ({
10633
11204
  plan_id,
@@ -10656,7 +11227,12 @@ function registerPlanApprovalTools(server2) {
10656
11227
  };
10657
11228
  if ((response_format || "text") === "json") {
10658
11229
  return {
10659
- content: [{ type: "text", text: JSON.stringify(asEnvelope15(payload), null, 2) }],
11230
+ content: [
11231
+ {
11232
+ type: "text",
11233
+ text: JSON.stringify(asEnvelope15(payload), null, 2)
11234
+ }
11235
+ ],
10660
11236
  isError: false
10661
11237
  };
10662
11238
  }
@@ -10705,14 +11281,22 @@ function registerPlanApprovalTools(server2) {
10705
11281
  };
10706
11282
  if ((response_format || "text") === "json") {
10707
11283
  return {
10708
- content: [{ type: "text", text: JSON.stringify(asEnvelope15(payload), null, 2) }],
11284
+ content: [
11285
+ {
11286
+ type: "text",
11287
+ text: JSON.stringify(asEnvelope15(payload), null, 2)
11288
+ }
11289
+ ],
10709
11290
  isError: false
10710
11291
  };
10711
11292
  }
10712
11293
  if (!data || data.length === 0) {
10713
11294
  return {
10714
11295
  content: [
10715
- { type: "text", text: `No approval items found for plan ${plan_id}.` }
11296
+ {
11297
+ type: "text",
11298
+ text: `No approval items found for plan ${plan_id}.`
11299
+ }
10716
11300
  ],
10717
11301
  isError: false
10718
11302
  };
@@ -10725,7 +11309,10 @@ function registerPlanApprovalTools(server2) {
10725
11309
  }
10726
11310
  lines.push("");
10727
11311
  lines.push(`Total: ${data.length}`);
10728
- return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
11312
+ return {
11313
+ content: [{ type: "text", text: lines.join("\n") }],
11314
+ isError: false
11315
+ };
10729
11316
  }
10730
11317
  );
10731
11318
  server2.tool(
@@ -10744,7 +11331,10 @@ function registerPlanApprovalTools(server2) {
10744
11331
  if (decision === "edited" && !edited_post) {
10745
11332
  return {
10746
11333
  content: [
10747
- { type: "text", text: 'edited_post is required when decision is "edited".' }
11334
+ {
11335
+ type: "text",
11336
+ text: 'edited_post is required when decision is "edited".'
11337
+ }
10748
11338
  ],
10749
11339
  isError: true
10750
11340
  };
@@ -10757,7 +11347,9 @@ function registerPlanApprovalTools(server2) {
10757
11347
  if (decision === "edited") {
10758
11348
  updates.edited_post = edited_post;
10759
11349
  }
10760
- 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();
11350
+ const { data, error } = await supabase.from("content_plan_approvals").update(updates).eq("id", approval_id).eq("user_id", userId).eq("status", "pending").select(
11351
+ "id, plan_id, post_id, status, reason, decided_at, original_post, edited_post"
11352
+ ).maybeSingle();
10761
11353
  if (error) {
10762
11354
  return {
10763
11355
  content: [
@@ -10782,7 +11374,12 @@ function registerPlanApprovalTools(server2) {
10782
11374
  }
10783
11375
  if ((response_format || "text") === "json") {
10784
11376
  return {
10785
- content: [{ type: "text", text: JSON.stringify(asEnvelope15(data), null, 2) }],
11377
+ content: [
11378
+ {
11379
+ type: "text",
11380
+ text: JSON.stringify(asEnvelope15(data), null, 2)
11381
+ }
11382
+ ],
10786
11383
  isError: false
10787
11384
  };
10788
11385
  }
@@ -10922,16 +11519,40 @@ function registerAllTools(server2, options) {
10922
11519
  init_posthog();
10923
11520
  init_supabase();
10924
11521
  init_sn();
11522
+ function flushAndExit(code) {
11523
+ const done = { out: false, err: false };
11524
+ const tryExit = () => {
11525
+ if (done.out && done.err) process.exit(code);
11526
+ };
11527
+ if (process.stdout.writableFinished) {
11528
+ done.out = true;
11529
+ } else {
11530
+ process.stdout.end(() => {
11531
+ done.out = true;
11532
+ tryExit();
11533
+ });
11534
+ }
11535
+ if (process.stderr.writableFinished) {
11536
+ done.err = true;
11537
+ } else {
11538
+ process.stderr.end(() => {
11539
+ done.err = true;
11540
+ tryExit();
11541
+ });
11542
+ }
11543
+ tryExit();
11544
+ setTimeout(() => process.exit(code), 2e3).unref();
11545
+ }
10925
11546
  process.on("uncaughtException", (err) => {
10926
11547
  process.stderr.write(`MCP server error: ${err.message}
10927
11548
  `);
10928
- process.exit(1);
11549
+ flushAndExit(1);
10929
11550
  });
10930
11551
  process.on("unhandledRejection", (reason) => {
10931
11552
  const message = reason instanceof Error ? reason.message : String(reason);
10932
11553
  process.stderr.write(`MCP server error: ${message}
10933
11554
  `);
10934
- process.exit(1);
11555
+ flushAndExit(1);
10935
11556
  });
10936
11557
  var command = process.argv[2];
10937
11558
  if (command === "--version" || command === "-v") {
@@ -10940,7 +11561,11 @@ if (command === "--version" || command === "-v") {
10940
11561
  const { fileURLToPath } = await import("node:url");
10941
11562
  let version = MCP_VERSION;
10942
11563
  try {
10943
- const pkgPath = resolve3(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
11564
+ const pkgPath = resolve3(
11565
+ dirname(fileURLToPath(import.meta.url)),
11566
+ "..",
11567
+ "package.json"
11568
+ );
10944
11569
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
10945
11570
  version = pkg.version;
10946
11571
  } catch {
@@ -10972,7 +11597,11 @@ if (command === "--help" || command === "-h") {
10972
11597
  ok: true,
10973
11598
  command: "help",
10974
11599
  commands: [
10975
- { name: "setup", aliases: ["login"], description: "Interactive OAuth setup" },
11600
+ {
11601
+ name: "setup",
11602
+ aliases: ["login"],
11603
+ description: "Interactive OAuth setup"
11604
+ },
10976
11605
  { name: "logout", description: "Remove credentials" },
10977
11606
  { name: "whoami", description: "Show auth info" },
10978
11607
  { name: "health", description: "Check connectivity" },
@@ -11061,10 +11690,14 @@ if (command === "sn") {
11061
11690
  await runSnCli(process.argv.slice(3));
11062
11691
  process.exit(0);
11063
11692
  }
11064
- if (command && !["setup", "login", "logout", "whoami", "health", "sn", "repl"].includes(command)) {
11065
- process.stderr.write(`Unknown command: ${command}
11693
+ if (command && !["setup", "login", "logout", "whoami", "health", "sn", "repl"].includes(
11694
+ command
11695
+ )) {
11696
+ process.stderr.write(
11697
+ `Unknown command: ${command}
11066
11698
  Run socialneuron-mcp --help for usage.
11067
- `);
11699
+ `
11700
+ );
11068
11701
  process.exit(1);
11069
11702
  }
11070
11703
  await initializeAuth();