@socialneuron/mcp-server 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/http.js +1223 -524
  2. package/dist/index.js +1063 -426
  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.1";
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",
@@ -3746,7 +3746,7 @@ var TOOL_SCOPES = {
3746
3746
  adapt_content: "mcp:write",
3747
3747
  generate_video: "mcp:write",
3748
3748
  generate_image: "mcp:write",
3749
- check_status: "mcp:write",
3749
+ check_status: "mcp:read",
3750
3750
  render_demo_video: "mcp:write",
3751
3751
  save_brand_profile: "mcp:write",
3752
3752
  update_platform_voice: "mcp:write",
@@ -3800,6 +3800,77 @@ function hasScope(userScopes, required) {
3800
3800
  // src/tools/ideation.ts
3801
3801
  init_edge_function();
3802
3802
  import { z } from "zod";
3803
+
3804
+ // src/lib/rate-limit.ts
3805
+ var CATEGORY_CONFIGS = {
3806
+ posting: { maxTokens: 30, refillRate: 30 / 60 },
3807
+ // 30 req/min
3808
+ screenshot: { maxTokens: 10, refillRate: 10 / 60 },
3809
+ // 10 req/min
3810
+ read: { maxTokens: 60, refillRate: 60 / 60 }
3811
+ // 60 req/min
3812
+ };
3813
+ var RateLimiter = class {
3814
+ tokens;
3815
+ lastRefill;
3816
+ maxTokens;
3817
+ refillRate;
3818
+ // tokens per second
3819
+ constructor(config) {
3820
+ this.maxTokens = config.maxTokens;
3821
+ this.refillRate = config.refillRate;
3822
+ this.tokens = config.maxTokens;
3823
+ this.lastRefill = Date.now();
3824
+ }
3825
+ /**
3826
+ * Try to consume one token. Returns true if the request is allowed,
3827
+ * false if rate-limited.
3828
+ */
3829
+ consume() {
3830
+ this.refill();
3831
+ if (this.tokens >= 1) {
3832
+ this.tokens -= 1;
3833
+ return true;
3834
+ }
3835
+ return false;
3836
+ }
3837
+ /**
3838
+ * Seconds until at least one token is available.
3839
+ */
3840
+ retryAfter() {
3841
+ this.refill();
3842
+ if (this.tokens >= 1) return 0;
3843
+ return Math.ceil((1 - this.tokens) / this.refillRate);
3844
+ }
3845
+ refill() {
3846
+ const now = Date.now();
3847
+ const elapsed = (now - this.lastRefill) / 1e3;
3848
+ this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
3849
+ this.lastRefill = now;
3850
+ }
3851
+ };
3852
+ var limiters = /* @__PURE__ */ new Map();
3853
+ function getRateLimiter(category) {
3854
+ let limiter = limiters.get(category);
3855
+ if (!limiter) {
3856
+ const config = CATEGORY_CONFIGS[category] ?? CATEGORY_CONFIGS.read;
3857
+ limiter = new RateLimiter(config);
3858
+ limiters.set(category, limiter);
3859
+ }
3860
+ return limiter;
3861
+ }
3862
+ function checkRateLimit(category, key) {
3863
+ const bucketKey = key ? `${category}:${key}` : category;
3864
+ const limiter = getRateLimiter(bucketKey);
3865
+ const allowed = limiter.consume();
3866
+ return {
3867
+ allowed,
3868
+ retryAfter: allowed ? 0 : limiter.retryAfter()
3869
+ };
3870
+ }
3871
+
3872
+ // src/tools/ideation.ts
3873
+ init_supabase();
3803
3874
  function registerIdeationTools(server2) {
3804
3875
  server2.tool(
3805
3876
  "generate_content",
@@ -3820,7 +3891,9 @@ function registerIdeationTools(server2) {
3820
3891
  "facebook",
3821
3892
  "threads",
3822
3893
  "bluesky"
3823
- ]).optional().describe("Target social media platform. Helps tailor tone, length, and format."),
3894
+ ]).optional().describe(
3895
+ "Target social media platform. Helps tailor tone, length, and format."
3896
+ ),
3824
3897
  brand_voice: z.string().max(500).optional().describe(
3825
3898
  'Brand voice guidelines to follow (e.g. "professional and empathetic", "playful and Gen-Z"). Leave blank to use a neutral tone.'
3826
3899
  ),
@@ -3831,7 +3904,30 @@ function registerIdeationTools(server2) {
3831
3904
  "Project ID to auto-load brand profile and performance context for prompt enrichment."
3832
3905
  )
3833
3906
  },
3834
- async ({ prompt: prompt2, content_type, platform: platform3, brand_voice, model, project_id }) => {
3907
+ async ({
3908
+ prompt: prompt2,
3909
+ content_type,
3910
+ platform: platform3,
3911
+ brand_voice,
3912
+ model,
3913
+ project_id
3914
+ }) => {
3915
+ try {
3916
+ const userId = await getDefaultUserId();
3917
+ const rl = checkRateLimit("posting", userId);
3918
+ if (!rl.allowed) {
3919
+ return {
3920
+ content: [
3921
+ {
3922
+ type: "text",
3923
+ text: `Rate limited. Retry after ${rl.retryAfter}s.`
3924
+ }
3925
+ ],
3926
+ isError: true
3927
+ };
3928
+ }
3929
+ } catch {
3930
+ }
3835
3931
  let enrichedPrompt = prompt2;
3836
3932
  if (platform3) {
3837
3933
  enrichedPrompt += `
@@ -3963,8 +4059,12 @@ Content Type: ${content_type}`;
3963
4059
  category: z.string().optional().describe(
3964
4060
  "Category filter (for YouTube). Examples: general, entertainment, education, tech, music, gaming, sports, news."
3965
4061
  ),
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.'),
4062
+ niche: z.string().optional().describe(
4063
+ "Niche keyword filter. Only return trends matching these keywords."
4064
+ ),
4065
+ url: z.string().optional().describe(
4066
+ 'Required when source is "rss" or "url". The feed or page URL to fetch.'
4067
+ ),
3968
4068
  force_refresh: z.boolean().optional().describe("Skip the server-side cache and fetch fresh data.")
3969
4069
  },
3970
4070
  async ({ source, category, niche, url, force_refresh }) => {
@@ -4039,7 +4139,9 @@ Content Type: ${content_type}`;
4039
4139
  "adapt_content",
4040
4140
  "Adapt existing content for a different social media platform. Rewrites content to match the target platform's norms including character limits, hashtag style, tone, and CTA conventions.",
4041
4141
  {
4042
- content: z.string().max(5e3).describe("The content to adapt. Can be a caption, script, blog excerpt, or any text."),
4142
+ content: z.string().max(5e3).describe(
4143
+ "The content to adapt. Can be a caption, script, blog excerpt, or any text."
4144
+ ),
4043
4145
  source_platform: z.enum([
4044
4146
  "youtube",
4045
4147
  "tiktok",
@@ -4049,7 +4151,9 @@ Content Type: ${content_type}`;
4049
4151
  "facebook",
4050
4152
  "threads",
4051
4153
  "bluesky"
4052
- ]).optional().describe("The platform the content was originally written for. Helps preserve intent."),
4154
+ ]).optional().describe(
4155
+ "The platform the content was originally written for. Helps preserve intent."
4156
+ ),
4053
4157
  target_platform: z.enum([
4054
4158
  "youtube",
4055
4159
  "tiktok",
@@ -4063,9 +4167,33 @@ Content Type: ${content_type}`;
4063
4167
  brand_voice: z.string().max(500).optional().describe(
4064
4168
  'Brand voice guidelines to maintain during adaptation (e.g. "professional", "playful").'
4065
4169
  ),
4066
- project_id: z.string().uuid().optional().describe("Optional project ID to load platform voice overrides from brand profile.")
4170
+ project_id: z.string().uuid().optional().describe(
4171
+ "Optional project ID to load platform voice overrides from brand profile."
4172
+ )
4067
4173
  },
4068
- async ({ content, source_platform, target_platform, brand_voice, project_id }) => {
4174
+ async ({
4175
+ content,
4176
+ source_platform,
4177
+ target_platform,
4178
+ brand_voice,
4179
+ project_id
4180
+ }) => {
4181
+ try {
4182
+ const userId = await getDefaultUserId();
4183
+ const rl = checkRateLimit("posting", userId);
4184
+ if (!rl.allowed) {
4185
+ return {
4186
+ content: [
4187
+ {
4188
+ type: "text",
4189
+ text: `Rate limited. Retry after ${rl.retryAfter}s.`
4190
+ }
4191
+ ],
4192
+ isError: true
4193
+ };
4194
+ }
4195
+ } catch {
4196
+ }
4069
4197
  const platformGuidelines = {
4070
4198
  twitter: "Max 280 characters. Concise, punchy. 1-3 hashtags max. Thread-friendly.",
4071
4199
  threads: "Max 500 characters. Conversational, opinion-driven. Minimal hashtags.",
@@ -4148,76 +4276,6 @@ ${content}`,
4148
4276
  // src/tools/content.ts
4149
4277
  init_edge_function();
4150
4278
  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
4279
  init_supabase();
4222
4280
 
4223
4281
  // src/lib/sanitize-error.ts
@@ -4271,8 +4329,15 @@ function sanitizeDbError(error) {
4271
4329
 
4272
4330
  // src/tools/content.ts
4273
4331
  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));
4332
+ init_version();
4333
+ var MAX_CREDITS_PER_RUN = Math.max(
4334
+ 0,
4335
+ Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0)
4336
+ );
4337
+ var MAX_ASSETS_PER_RUN = Math.max(
4338
+ 0,
4339
+ Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0)
4340
+ );
4276
4341
  var _globalCreditsUsed = 0;
4277
4342
  var _globalAssetsGenerated = 0;
4278
4343
  function getCreditsUsed() {
@@ -4314,7 +4379,7 @@ function getCurrentBudgetStatus() {
4314
4379
  function asEnvelope(data) {
4315
4380
  return {
4316
4381
  _meta: {
4317
- version: "0.2.0",
4382
+ version: MCP_VERSION,
4318
4383
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
4319
4384
  },
4320
4385
  data
@@ -4396,8 +4461,12 @@ function registerContentTools(server2) {
4396
4461
  enable_audio: z2.boolean().optional().describe(
4397
4462
  "Enable native audio generation. Kling 2.6: doubles cost. Kling 3.0: 50% more (std 30/sec, pro 40/sec). 5+ languages."
4398
4463
  ),
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."),
4464
+ image_url: z2.string().optional().describe(
4465
+ "Start frame image URL for image-to-video (Kling 3.0 frame control)."
4466
+ ),
4467
+ end_frame_url: z2.string().optional().describe(
4468
+ "End frame image URL (Kling 3.0 only). Enables seamless loop transitions."
4469
+ ),
4401
4470
  response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
4402
4471
  },
4403
4472
  async ({
@@ -4835,10 +4904,13 @@ function registerContentTools(server2) {
4835
4904
  };
4836
4905
  }
4837
4906
  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
- });
4907
+ const { data: liveStatus } = await callEdgeFunction(
4908
+ "kie-task-status",
4909
+ {
4910
+ taskId: job.external_id,
4911
+ model: job.model
4912
+ }
4913
+ );
4842
4914
  if (liveStatus) {
4843
4915
  const lines2 = [
4844
4916
  `Job: ${job.id}`,
@@ -4912,7 +4984,12 @@ function registerContentTools(server2) {
4912
4984
  });
4913
4985
  if (format === "json") {
4914
4986
  return {
4915
- content: [{ type: "text", text: JSON.stringify(asEnvelope(job), null, 2) }]
4987
+ content: [
4988
+ {
4989
+ type: "text",
4990
+ text: JSON.stringify(asEnvelope(job), null, 2)
4991
+ }
4992
+ ]
4916
4993
  };
4917
4994
  }
4918
4995
  return {
@@ -4930,7 +5007,15 @@ function registerContentTools(server2) {
4930
5007
  brand_context: z2.string().max(3e3).optional().describe(
4931
5008
  "Brand context JSON from extract_brand. Include colors, voice tone, visual style keywords for consistent branding across frames."
4932
5009
  ),
4933
- platform: z2.enum(["tiktok", "instagram-reels", "youtube-shorts", "youtube", "general"]).describe("Target platform. Determines aspect ratio, duration, and pacing."),
5010
+ platform: z2.enum([
5011
+ "tiktok",
5012
+ "instagram-reels",
5013
+ "youtube-shorts",
5014
+ "youtube",
5015
+ "general"
5016
+ ]).describe(
5017
+ "Target platform. Determines aspect ratio, duration, and pacing."
5018
+ ),
4934
5019
  target_duration: z2.number().min(5).max(120).optional().describe(
4935
5020
  "Target total duration in seconds. Defaults to 30s for short-form, 60s for YouTube."
4936
5021
  ),
@@ -4938,7 +5023,9 @@ function registerContentTools(server2) {
4938
5023
  style: z2.string().optional().describe(
4939
5024
  'Visual style direction (e.g., "cinematic", "anime", "documentary", "motion graphics").'
4940
5025
  ),
4941
- response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to json for structured storyboard data.")
5026
+ response_format: z2.enum(["text", "json"]).optional().describe(
5027
+ "Response format. Defaults to json for structured storyboard data."
5028
+ )
4942
5029
  },
4943
5030
  async ({
4944
5031
  concept,
@@ -4951,7 +5038,11 @@ function registerContentTools(server2) {
4951
5038
  }) => {
4952
5039
  const format = response_format ?? "json";
4953
5040
  const startedAt = Date.now();
4954
- const isShortForm = ["tiktok", "instagram-reels", "youtube-shorts"].includes(platform3);
5041
+ const isShortForm = [
5042
+ "tiktok",
5043
+ "instagram-reels",
5044
+ "youtube-shorts"
5045
+ ].includes(platform3);
4955
5046
  const duration = target_duration ?? (isShortForm ? 30 : 60);
4956
5047
  const scenes = num_scenes ?? (isShortForm ? 7 : 10);
4957
5048
  const aspectRatio = isShortForm ? "9:16" : "16:9";
@@ -5044,7 +5135,12 @@ Return ONLY valid JSON in this exact format:
5044
5135
  details: { error }
5045
5136
  });
5046
5137
  return {
5047
- content: [{ type: "text", text: `Storyboard generation failed: ${error}` }],
5138
+ content: [
5139
+ {
5140
+ type: "text",
5141
+ text: `Storyboard generation failed: ${error}`
5142
+ }
5143
+ ],
5048
5144
  isError: true
5049
5145
  };
5050
5146
  }
@@ -5060,7 +5156,12 @@ Return ONLY valid JSON in this exact format:
5060
5156
  try {
5061
5157
  const parsed = JSON.parse(rawContent);
5062
5158
  return {
5063
- content: [{ type: "text", text: JSON.stringify(asEnvelope(parsed), null, 2) }]
5159
+ content: [
5160
+ {
5161
+ type: "text",
5162
+ text: JSON.stringify(asEnvelope(parsed), null, 2)
5163
+ }
5164
+ ]
5064
5165
  };
5065
5166
  } catch {
5066
5167
  return {
@@ -5124,7 +5225,10 @@ Return ONLY valid JSON in this exact format:
5124
5225
  isError: true
5125
5226
  };
5126
5227
  }
5127
- const rateLimit = checkRateLimit("posting", `generate_voiceover:${userId}`);
5228
+ const rateLimit = checkRateLimit(
5229
+ "posting",
5230
+ `generate_voiceover:${userId}`
5231
+ );
5128
5232
  if (!rateLimit.allowed) {
5129
5233
  await logMcpToolInvocation({
5130
5234
  toolName: "generate_voiceover",
@@ -5159,7 +5263,12 @@ Return ONLY valid JSON in this exact format:
5159
5263
  details: { error }
5160
5264
  });
5161
5265
  return {
5162
- content: [{ type: "text", text: `Voiceover generation failed: ${error}` }],
5266
+ content: [
5267
+ {
5268
+ type: "text",
5269
+ text: `Voiceover generation failed: ${error}`
5270
+ }
5271
+ ],
5163
5272
  isError: true
5164
5273
  };
5165
5274
  }
@@ -5172,7 +5281,10 @@ Return ONLY valid JSON in this exact format:
5172
5281
  });
5173
5282
  return {
5174
5283
  content: [
5175
- { type: "text", text: "Voiceover generation failed: no audio URL returned." }
5284
+ {
5285
+ type: "text",
5286
+ text: "Voiceover generation failed: no audio URL returned."
5287
+ }
5176
5288
  ],
5177
5289
  isError: true
5178
5290
  };
@@ -5244,7 +5356,9 @@ Return ONLY valid JSON in this exact format:
5244
5356
  "Carousel template. hormozi-authority: bold typography, one idea per slide, dark backgrounds. educational-series: numbered tips. Default: hormozi-authority."
5245
5357
  ),
5246
5358
  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."),
5359
+ aspect_ratio: z2.enum(["1:1", "4:5", "9:16"]).optional().describe(
5360
+ "Aspect ratio. 1:1 square (default), 4:5 portrait, 9:16 story."
5361
+ ),
5248
5362
  style: z2.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
5249
5363
  "Visual style. hormozi: black bg, bold white text, gold accents. Default: hormozi (when using hormozi-authority template)."
5250
5364
  ),
@@ -5281,7 +5395,10 @@ Return ONLY valid JSON in this exact format:
5281
5395
  };
5282
5396
  }
5283
5397
  const userId = await getDefaultUserId();
5284
- const rateLimit = checkRateLimit("posting", `generate_carousel:${userId}`);
5398
+ const rateLimit = checkRateLimit(
5399
+ "posting",
5400
+ `generate_carousel:${userId}`
5401
+ );
5285
5402
  if (!rateLimit.allowed) {
5286
5403
  await logMcpToolInvocation({
5287
5404
  toolName: "generate_carousel",
@@ -5319,7 +5436,12 @@ Return ONLY valid JSON in this exact format:
5319
5436
  details: { error }
5320
5437
  });
5321
5438
  return {
5322
- content: [{ type: "text", text: `Carousel generation failed: ${error}` }],
5439
+ content: [
5440
+ {
5441
+ type: "text",
5442
+ text: `Carousel generation failed: ${error}`
5443
+ }
5444
+ ],
5323
5445
  isError: true
5324
5446
  };
5325
5447
  }
@@ -5331,7 +5453,12 @@ Return ONLY valid JSON in this exact format:
5331
5453
  details: { error: "No carousel data returned" }
5332
5454
  });
5333
5455
  return {
5334
- content: [{ type: "text", text: "Carousel generation returned no data." }],
5456
+ content: [
5457
+ {
5458
+ type: "text",
5459
+ text: "Carousel generation returned no data."
5460
+ }
5461
+ ],
5335
5462
  isError: true
5336
5463
  };
5337
5464
  }
@@ -5397,6 +5524,7 @@ import { z as z3 } from "zod";
5397
5524
  import { createHash as createHash2 } from "node:crypto";
5398
5525
  init_supabase();
5399
5526
  init_quality();
5527
+ init_version();
5400
5528
  var PLATFORM_CASE_MAP = {
5401
5529
  youtube: "YouTube",
5402
5530
  tiktok: "TikTok",
@@ -5410,7 +5538,7 @@ var PLATFORM_CASE_MAP = {
5410
5538
  function asEnvelope2(data) {
5411
5539
  return {
5412
5540
  _meta: {
5413
- version: "0.2.0",
5541
+ version: MCP_VERSION,
5414
5542
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5415
5543
  },
5416
5544
  data
@@ -5442,15 +5570,21 @@ function registerDistributionTools(server2) {
5442
5570
  "threads",
5443
5571
  "bluesky"
5444
5572
  ])
5445
- ).min(1).describe("Target platforms to post to. Each must have an active OAuth connection."),
5573
+ ).min(1).describe(
5574
+ "Target platforms to post to. Each must have an active OAuth connection."
5575
+ ),
5446
5576
  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.'),
5577
+ hashtags: z3.array(z3.string()).optional().describe(
5578
+ 'Hashtags to append to the caption. Include or omit the "#" prefix.'
5579
+ ),
5448
5580
  schedule_at: z3.string().optional().describe(
5449
5581
  'ISO 8601 datetime for scheduled posting (e.g. "2026-03-15T14:00:00Z"). Omit for immediate posting.'
5450
5582
  ),
5451
5583
  project_id: z3.string().optional().describe("Social Neuron project ID to associate this post with."),
5452
5584
  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.')
5585
+ attribution: z3.boolean().optional().describe(
5586
+ 'If true, appends "Created with Social Neuron" to the caption. Default: false.'
5587
+ )
5454
5588
  },
5455
5589
  async ({
5456
5590
  media_url,
@@ -5469,7 +5603,12 @@ function registerDistributionTools(server2) {
5469
5603
  const startedAt = Date.now();
5470
5604
  if ((!caption || caption.trim().length === 0) && (!title || title.trim().length === 0)) {
5471
5605
  return {
5472
- content: [{ type: "text", text: "Either caption or title is required." }],
5606
+ content: [
5607
+ {
5608
+ type: "text",
5609
+ text: "Either caption or title is required."
5610
+ }
5611
+ ],
5473
5612
  isError: true
5474
5613
  };
5475
5614
  }
@@ -5492,7 +5631,9 @@ function registerDistributionTools(server2) {
5492
5631
  isError: true
5493
5632
  };
5494
5633
  }
5495
- const normalizedPlatforms = platforms.map((p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p);
5634
+ const normalizedPlatforms = platforms.map(
5635
+ (p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
5636
+ );
5496
5637
  let finalCaption = caption;
5497
5638
  if (attribution && finalCaption) {
5498
5639
  finalCaption = `${finalCaption}
@@ -5556,7 +5697,9 @@ Created with Social Neuron`;
5556
5697
  ];
5557
5698
  for (const [platform3, result] of Object.entries(data.results)) {
5558
5699
  if (result.success) {
5559
- lines.push(` ${platform3}: OK (jobId=${result.jobId}, postId=${result.postId})`);
5700
+ lines.push(
5701
+ ` ${platform3}: OK (jobId=${result.jobId}, postId=${result.postId})`
5702
+ );
5560
5703
  } else {
5561
5704
  lines.push(` ${platform3}: FAILED - ${result.error}`);
5562
5705
  }
@@ -5573,7 +5716,12 @@ Created with Social Neuron`;
5573
5716
  });
5574
5717
  if (format === "json") {
5575
5718
  return {
5576
- content: [{ type: "text", text: JSON.stringify(asEnvelope2(data), null, 2) }],
5719
+ content: [
5720
+ {
5721
+ type: "text",
5722
+ text: JSON.stringify(asEnvelope2(data), null, 2)
5723
+ }
5724
+ ],
5577
5725
  isError: !data.success
5578
5726
  };
5579
5727
  }
@@ -5629,12 +5777,17 @@ Created with Social Neuron`;
5629
5777
  for (const account of accounts) {
5630
5778
  const name = account.username || "(unnamed)";
5631
5779
  const platformLower = account.platform.toLowerCase();
5632
- lines.push(` ${platformLower}: ${name} (connected ${account.created_at.split("T")[0]})`);
5780
+ lines.push(
5781
+ ` ${platformLower}: ${name} (connected ${account.created_at.split("T")[0]})`
5782
+ );
5633
5783
  }
5634
5784
  if (format === "json") {
5635
5785
  return {
5636
5786
  content: [
5637
- { type: "text", text: JSON.stringify(asEnvelope2({ accounts }), null, 2) }
5787
+ {
5788
+ type: "text",
5789
+ text: JSON.stringify(asEnvelope2({ accounts }), null, 2)
5790
+ }
5638
5791
  ]
5639
5792
  };
5640
5793
  }
@@ -5696,7 +5849,10 @@ Created with Social Neuron`;
5696
5849
  if (format === "json") {
5697
5850
  return {
5698
5851
  content: [
5699
- { type: "text", text: JSON.stringify(asEnvelope2({ posts: [] }), null, 2) }
5852
+ {
5853
+ type: "text",
5854
+ text: JSON.stringify(asEnvelope2({ posts: [] }), null, 2)
5855
+ }
5700
5856
  ]
5701
5857
  };
5702
5858
  }
@@ -5713,7 +5869,10 @@ Created with Social Neuron`;
5713
5869
  if (format === "json") {
5714
5870
  return {
5715
5871
  content: [
5716
- { type: "text", text: JSON.stringify(asEnvelope2({ posts }), null, 2) }
5872
+ {
5873
+ type: "text",
5874
+ text: JSON.stringify(asEnvelope2({ posts }), null, 2)
5875
+ }
5717
5876
  ]
5718
5877
  };
5719
5878
  }
@@ -5774,7 +5933,13 @@ Created with Social Neuron`;
5774
5933
  min_gap_hours: z3.number().min(1).max(24).default(4).describe("Minimum gap between posts on same platform"),
5775
5934
  response_format: z3.enum(["text", "json"]).default("text")
5776
5935
  },
5777
- async ({ platforms, count, start_after, min_gap_hours, response_format }) => {
5936
+ async ({
5937
+ platforms,
5938
+ count,
5939
+ start_after,
5940
+ min_gap_hours,
5941
+ response_format
5942
+ }) => {
5778
5943
  const startedAt = Date.now();
5779
5944
  try {
5780
5945
  const userId = await getDefaultUserId();
@@ -5785,7 +5950,9 @@ Created with Social Neuron`;
5785
5950
  const gapMs = min_gap_hours * 60 * 60 * 1e3;
5786
5951
  const candidates = [];
5787
5952
  for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
5788
- const date = new Date(startDate.getTime() + dayOffset * 24 * 60 * 60 * 1e3);
5953
+ const date = new Date(
5954
+ startDate.getTime() + dayOffset * 24 * 60 * 60 * 1e3
5955
+ );
5789
5956
  const dayOfWeek = date.getUTCDay();
5790
5957
  for (const platform3 of platforms) {
5791
5958
  const hours = PREFERRED_HOURS[platform3] ?? [12, 16];
@@ -5794,8 +5961,11 @@ Created with Social Neuron`;
5794
5961
  slotDate.setUTCHours(hours[hourIdx], 0, 0, 0);
5795
5962
  if (slotDate <= startDate) continue;
5796
5963
  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();
5964
+ if (String(post.platform).toLowerCase() !== platform3)
5965
+ return false;
5966
+ const postTime = new Date(
5967
+ post.scheduled_at ?? post.published_at
5968
+ ).getTime();
5799
5969
  return Math.abs(postTime - slotDate.getTime()) < gapMs;
5800
5970
  });
5801
5971
  let engagementScore = hours.length - hourIdx;
@@ -5840,15 +6010,22 @@ Created with Social Neuron`;
5840
6010
  };
5841
6011
  }
5842
6012
  const lines = [];
5843
- lines.push(`Found ${slots.length} optimal slots (${conflictsAvoided} conflicts avoided):`);
6013
+ lines.push(
6014
+ `Found ${slots.length} optimal slots (${conflictsAvoided} conflicts avoided):`
6015
+ );
5844
6016
  lines.push("");
5845
6017
  lines.push("Datetime (UTC) | Platform | Score");
5846
6018
  lines.push("-------------------------+------------+------");
5847
6019
  for (const s of slots) {
5848
6020
  const dt = s.datetime.replace("T", " ").slice(0, 19);
5849
- lines.push(`${dt.padEnd(25)}| ${s.platform.padEnd(11)}| ${s.engagement_score}`);
6021
+ lines.push(
6022
+ `${dt.padEnd(25)}| ${s.platform.padEnd(11)}| ${s.engagement_score}`
6023
+ );
5850
6024
  }
5851
- return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
6025
+ return {
6026
+ content: [{ type: "text", text: lines.join("\n") }],
6027
+ isError: false
6028
+ };
5852
6029
  } catch (err) {
5853
6030
  const durationMs = Date.now() - startedAt;
5854
6031
  const message = err instanceof Error ? err.message : String(err);
@@ -5859,7 +6036,9 @@ Created with Social Neuron`;
5859
6036
  details: { error: message }
5860
6037
  });
5861
6038
  return {
5862
- content: [{ type: "text", text: `Failed to find slots: ${message}` }],
6039
+ content: [
6040
+ { type: "text", text: `Failed to find slots: ${message}` }
6041
+ ],
5863
6042
  isError: true
5864
6043
  };
5865
6044
  }
@@ -5886,8 +6065,12 @@ Created with Social Neuron`;
5886
6065
  auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
5887
6066
  dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
5888
6067
  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."),
6068
+ enforce_quality: z3.boolean().default(true).describe(
6069
+ "When true, block scheduling for posts that fail quality checks."
6070
+ ),
6071
+ quality_threshold: z3.number().int().min(0).max(35).optional().describe(
6072
+ "Optional quality threshold override. Defaults to project setting or 26."
6073
+ ),
5891
6074
  batch_size: z3.number().int().min(1).max(10).default(4).describe("Concurrent schedule calls per platform batch."),
5892
6075
  idempotency_seed: z3.string().max(128).optional().describe("Optional stable seed used when building idempotency keys.")
5893
6076
  },
@@ -5926,17 +6109,25 @@ Created with Social Neuron`;
5926
6109
  if (!stored?.plan_payload) {
5927
6110
  return {
5928
6111
  content: [
5929
- { type: "text", text: `No content plan found for plan_id=${plan_id}` }
6112
+ {
6113
+ type: "text",
6114
+ text: `No content plan found for plan_id=${plan_id}`
6115
+ }
5930
6116
  ],
5931
6117
  isError: true
5932
6118
  };
5933
6119
  }
5934
6120
  const payload = stored.plan_payload;
5935
- const postsFromPayload = Array.isArray(payload.posts) ? payload.posts : Array.isArray(payload.data?.posts) ? payload.data.posts : null;
6121
+ const postsFromPayload = Array.isArray(payload.posts) ? payload.posts : Array.isArray(
6122
+ payload.data?.posts
6123
+ ) ? payload.data.posts : null;
5936
6124
  if (!postsFromPayload) {
5937
6125
  return {
5938
6126
  content: [
5939
- { type: "text", text: `Stored plan ${plan_id} has no posts array.` }
6127
+ {
6128
+ type: "text",
6129
+ text: `Stored plan ${plan_id} has no posts array.`
6130
+ }
5940
6131
  ],
5941
6132
  isError: true
5942
6133
  };
@@ -6028,7 +6219,10 @@ Created with Social Neuron`;
6028
6219
  approvalSummary = {
6029
6220
  total: approvals.length,
6030
6221
  eligible: approvedPosts.length,
6031
- skipped: Math.max(0, workingPlan.posts.length - approvedPosts.length)
6222
+ skipped: Math.max(
6223
+ 0,
6224
+ workingPlan.posts.length - approvedPosts.length
6225
+ )
6032
6226
  };
6033
6227
  workingPlan = {
6034
6228
  ...workingPlan,
@@ -6062,9 +6256,14 @@ Created with Social Neuron`;
6062
6256
  try {
6063
6257
  const { data: settingsData } = await supabase.from("system_settings").select("value").eq("key", "content_safety").maybeSingle();
6064
6258
  if (settingsData?.value?.quality_threshold !== void 0) {
6065
- const parsedThreshold = Number(settingsData.value.quality_threshold);
6259
+ const parsedThreshold = Number(
6260
+ settingsData.value.quality_threshold
6261
+ );
6066
6262
  if (Number.isFinite(parsedThreshold)) {
6067
- effectiveQualityThreshold = Math.max(0, Math.min(35, Math.trunc(parsedThreshold)));
6263
+ effectiveQualityThreshold = Math.max(
6264
+ 0,
6265
+ Math.min(35, Math.trunc(parsedThreshold))
6266
+ );
6068
6267
  }
6069
6268
  }
6070
6269
  if (Array.isArray(settingsData?.value?.custom_banned_terms)) {
@@ -6100,13 +6299,18 @@ Created with Social Neuron`;
6100
6299
  }
6101
6300
  };
6102
6301
  });
6103
- const qualityPassed = postsWithResults.filter((post) => post.quality.passed).length;
6302
+ const qualityPassed = postsWithResults.filter(
6303
+ (post) => post.quality.passed
6304
+ ).length;
6104
6305
  const qualitySummary = {
6105
6306
  total_posts: postsWithResults.length,
6106
6307
  passed: qualityPassed,
6107
6308
  failed: postsWithResults.length - qualityPassed,
6108
6309
  avg_score: postsWithResults.length > 0 ? Number(
6109
- (postsWithResults.reduce((sum, post) => sum + post.quality.score, 0) / postsWithResults.length).toFixed(2)
6310
+ (postsWithResults.reduce(
6311
+ (sum, post) => sum + post.quality.score,
6312
+ 0
6313
+ ) / postsWithResults.length).toFixed(2)
6110
6314
  ) : 0
6111
6315
  };
6112
6316
  if (dry_run) {
@@ -6176,8 +6380,13 @@ Created with Social Neuron`;
6176
6380
  }
6177
6381
  }
6178
6382
  lines2.push("");
6179
- lines2.push(`Summary: ${passed}/${workingPlan.posts.length} passed quality check`);
6180
- return { content: [{ type: "text", text: lines2.join("\n") }], isError: false };
6383
+ lines2.push(
6384
+ `Summary: ${passed}/${workingPlan.posts.length} passed quality check`
6385
+ );
6386
+ return {
6387
+ content: [{ type: "text", text: lines2.join("\n") }],
6388
+ isError: false
6389
+ };
6181
6390
  }
6182
6391
  let scheduled = 0;
6183
6392
  let failed = 0;
@@ -6269,7 +6478,8 @@ Created with Social Neuron`;
6269
6478
  }
6270
6479
  const chunk = (arr, size) => {
6271
6480
  const out = [];
6272
- for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
6481
+ for (let i = 0; i < arr.length; i += size)
6482
+ out.push(arr.slice(i, i + size));
6273
6483
  return out;
6274
6484
  };
6275
6485
  const platformBatches = Array.from(grouped.entries()).map(
@@ -6277,7 +6487,9 @@ Created with Social Neuron`;
6277
6487
  const platformResults = [];
6278
6488
  const batches = chunk(platformPosts, batch_size);
6279
6489
  for (const batch of batches) {
6280
- const settled = await Promise.allSettled(batch.map((post) => scheduleOne(post)));
6490
+ const settled = await Promise.allSettled(
6491
+ batch.map((post) => scheduleOne(post))
6492
+ );
6281
6493
  for (const outcome of settled) {
6282
6494
  if (outcome.status === "fulfilled") {
6283
6495
  platformResults.push(outcome.value);
@@ -6343,7 +6555,11 @@ Created with Social Neuron`;
6343
6555
  plan_id: effectivePlanId,
6344
6556
  approvals: approvalSummary,
6345
6557
  posts: results,
6346
- summary: { total_posts: workingPlan.posts.length, scheduled, failed }
6558
+ summary: {
6559
+ total_posts: workingPlan.posts.length,
6560
+ scheduled,
6561
+ failed
6562
+ }
6347
6563
  }),
6348
6564
  null,
6349
6565
  2
@@ -6381,7 +6597,12 @@ Created with Social Neuron`;
6381
6597
  details: { error: message }
6382
6598
  });
6383
6599
  return {
6384
- content: [{ type: "text", text: `Batch scheduling failed: ${message}` }],
6600
+ content: [
6601
+ {
6602
+ type: "text",
6603
+ text: `Batch scheduling failed: ${message}`
6604
+ }
6605
+ ],
6385
6606
  isError: true
6386
6607
  };
6387
6608
  }
@@ -6393,10 +6614,11 @@ Created with Social Neuron`;
6393
6614
  init_supabase();
6394
6615
  init_edge_function();
6395
6616
  import { z as z4 } from "zod";
6617
+ init_version();
6396
6618
  function asEnvelope3(data) {
6397
6619
  return {
6398
6620
  _meta: {
6399
- version: "0.2.0",
6621
+ version: MCP_VERSION,
6400
6622
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6401
6623
  },
6402
6624
  data
@@ -6509,7 +6731,9 @@ function registerAnalyticsTools(server2) {
6509
6731
  ]
6510
6732
  };
6511
6733
  }
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);
6734
+ const { data: simpleRows, error: simpleError } = await supabase.from("post_analytics").select(
6735
+ "id, post_id, platform, views, likes, comments, shares, captured_at"
6736
+ ).in("post_id", postIds).gte("captured_at", sinceIso).order("captured_at", { ascending: false }).limit(maxPosts);
6513
6737
  if (simpleError) {
6514
6738
  return {
6515
6739
  content: [
@@ -6521,7 +6745,12 @@ function registerAnalyticsTools(server2) {
6521
6745
  isError: true
6522
6746
  };
6523
6747
  }
6524
- return formatSimpleAnalytics(simpleRows, platform3, lookbackDays, format);
6748
+ return formatSimpleAnalytics(
6749
+ simpleRows,
6750
+ platform3,
6751
+ lookbackDays,
6752
+ format
6753
+ );
6525
6754
  }
6526
6755
  if (!rows || rows.length === 0) {
6527
6756
  if (format === "json") {
@@ -6598,7 +6827,10 @@ function registerAnalyticsTools(server2) {
6598
6827
  const format = response_format ?? "text";
6599
6828
  const startedAt = Date.now();
6600
6829
  const userId = await getDefaultUserId();
6601
- const rateLimit = checkRateLimit("posting", `refresh_platform_analytics:${userId}`);
6830
+ const rateLimit = checkRateLimit(
6831
+ "posting",
6832
+ `refresh_platform_analytics:${userId}`
6833
+ );
6602
6834
  if (!rateLimit.allowed) {
6603
6835
  await logMcpToolInvocation({
6604
6836
  toolName: "refresh_platform_analytics",
@@ -6616,7 +6848,9 @@ function registerAnalyticsTools(server2) {
6616
6848
  isError: true
6617
6849
  };
6618
6850
  }
6619
- const { data, error } = await callEdgeFunction("fetch-analytics", { userId });
6851
+ const { data, error } = await callEdgeFunction("fetch-analytics", {
6852
+ userId
6853
+ });
6620
6854
  if (error) {
6621
6855
  await logMcpToolInvocation({
6622
6856
  toolName: "refresh_platform_analytics",
@@ -6625,7 +6859,12 @@ function registerAnalyticsTools(server2) {
6625
6859
  details: { error }
6626
6860
  });
6627
6861
  return {
6628
- content: [{ type: "text", text: `Error refreshing analytics: ${error}` }],
6862
+ content: [
6863
+ {
6864
+ type: "text",
6865
+ text: `Error refreshing analytics: ${error}`
6866
+ }
6867
+ ],
6629
6868
  isError: true
6630
6869
  };
6631
6870
  }
@@ -6638,12 +6877,18 @@ function registerAnalyticsTools(server2) {
6638
6877
  details: { error: "Edge function returned success=false" }
6639
6878
  });
6640
6879
  return {
6641
- content: [{ type: "text", text: "Analytics refresh failed." }],
6880
+ content: [
6881
+ { type: "text", text: "Analytics refresh failed." }
6882
+ ],
6642
6883
  isError: true
6643
6884
  };
6644
6885
  }
6645
- const queued = (result.results ?? []).filter((r) => r.status === "queued").length;
6646
- const errored = (result.results ?? []).filter((r) => r.status === "error").length;
6886
+ const queued = (result.results ?? []).filter(
6887
+ (r) => r.status === "queued"
6888
+ ).length;
6889
+ const errored = (result.results ?? []).filter(
6890
+ (r) => r.status === "error"
6891
+ ).length;
6647
6892
  const lines = [
6648
6893
  `Analytics refresh triggered successfully.`,
6649
6894
  ` Posts processed: ${result.postsProcessed}`,
@@ -6689,7 +6934,10 @@ function formatAnalytics(summary, days, format) {
6689
6934
  if (format === "json") {
6690
6935
  return {
6691
6936
  content: [
6692
- { type: "text", text: JSON.stringify(asEnvelope3({ ...summary, days }), null, 2) }
6937
+ {
6938
+ type: "text",
6939
+ text: JSON.stringify(asEnvelope3({ ...summary, days }), null, 2)
6940
+ }
6693
6941
  ]
6694
6942
  };
6695
6943
  }
@@ -6807,10 +7055,160 @@ function formatSimpleAnalytics(rows, platform3, days, format) {
6807
7055
  init_edge_function();
6808
7056
  init_supabase();
6809
7057
  import { z as z5 } from "zod";
7058
+
7059
+ // src/lib/ssrf.ts
7060
+ var BLOCKED_IP_PATTERNS = [
7061
+ // IPv4 localhost/loopback
7062
+ /^127\./,
7063
+ /^0\./,
7064
+ // IPv4 private ranges (RFC 1918)
7065
+ /^10\./,
7066
+ /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
7067
+ /^192\.168\./,
7068
+ // IPv4 link-local
7069
+ /^169\.254\./,
7070
+ // Cloud metadata endpoint (AWS, GCP, Azure)
7071
+ /^169\.254\.169\.254$/,
7072
+ // IPv4 broadcast
7073
+ /^255\./,
7074
+ // Shared address space (RFC 6598)
7075
+ /^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./
7076
+ ];
7077
+ var BLOCKED_IPV6_PATTERNS = [
7078
+ /^::1$/i,
7079
+ // loopback
7080
+ /^::$/i,
7081
+ // unspecified
7082
+ /^fe[89ab][0-9a-f]:/i,
7083
+ // link-local fe80::/10
7084
+ /^fc[0-9a-f]:/i,
7085
+ // unique local fc00::/7
7086
+ /^fd[0-9a-f]:/i,
7087
+ // unique local fc00::/7
7088
+ /^::ffff:127\./i,
7089
+ // IPv4-mapped localhost
7090
+ /^::ffff:(0|10|127|169\.254|172\.(1[6-9]|2[0-9]|3[0-1])|192\.168)\./i
7091
+ // IPv4-mapped private
7092
+ ];
7093
+ var BLOCKED_HOSTNAMES = [
7094
+ "localhost",
7095
+ "localhost.localdomain",
7096
+ "local",
7097
+ "127.0.0.1",
7098
+ "0.0.0.0",
7099
+ "[::1]",
7100
+ "[::ffff:127.0.0.1]",
7101
+ // Cloud metadata endpoints
7102
+ "metadata.google.internal",
7103
+ "metadata.goog",
7104
+ "instance-data",
7105
+ "instance-data.ec2.internal"
7106
+ ];
7107
+ var ALLOWED_PROTOCOLS = ["http:", "https:"];
7108
+ var BLOCKED_PORTS = [22, 23, 25, 110, 143, 445, 3306, 5432, 6379, 27017, 11211];
7109
+ function isBlockedIP(ip) {
7110
+ const normalized = ip.replace(/^\[/, "").replace(/\]$/, "");
7111
+ if (normalized.includes(":")) {
7112
+ return BLOCKED_IPV6_PATTERNS.some((pattern) => pattern.test(normalized));
7113
+ }
7114
+ return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(normalized));
7115
+ }
7116
+ function isBlockedHostname(hostname) {
7117
+ return BLOCKED_HOSTNAMES.includes(hostname.toLowerCase());
7118
+ }
7119
+ function isIPAddress(hostname) {
7120
+ const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
7121
+ const ipv6Pattern = /^\[?[a-fA-F0-9:]+\]?$/;
7122
+ return ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname);
7123
+ }
7124
+ async function validateUrlForSSRF(urlString) {
7125
+ try {
7126
+ const url = new URL(urlString);
7127
+ if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
7128
+ return {
7129
+ isValid: false,
7130
+ error: `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are allowed.`
7131
+ };
7132
+ }
7133
+ if (url.username || url.password) {
7134
+ return {
7135
+ isValid: false,
7136
+ error: "URLs with embedded credentials are not allowed."
7137
+ };
7138
+ }
7139
+ const hostname = url.hostname.toLowerCase();
7140
+ if (isBlockedHostname(hostname)) {
7141
+ return {
7142
+ isValid: false,
7143
+ error: "Access to internal/localhost addresses is not allowed."
7144
+ };
7145
+ }
7146
+ if (isIPAddress(hostname) && isBlockedIP(hostname)) {
7147
+ return {
7148
+ isValid: false,
7149
+ error: "Access to private/internal IP addresses is not allowed."
7150
+ };
7151
+ }
7152
+ const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80;
7153
+ if (BLOCKED_PORTS.includes(port)) {
7154
+ return {
7155
+ isValid: false,
7156
+ error: `Access to port ${port} is not allowed.`
7157
+ };
7158
+ }
7159
+ let resolvedIP;
7160
+ if (!isIPAddress(hostname)) {
7161
+ try {
7162
+ const dns = await import("node:dns");
7163
+ const resolver = new dns.promises.Resolver();
7164
+ const resolvedIPs = [];
7165
+ try {
7166
+ const aRecords = await resolver.resolve4(hostname);
7167
+ resolvedIPs.push(...aRecords);
7168
+ } catch {
7169
+ }
7170
+ try {
7171
+ const aaaaRecords = await resolver.resolve6(hostname);
7172
+ resolvedIPs.push(...aaaaRecords);
7173
+ } catch {
7174
+ }
7175
+ if (resolvedIPs.length === 0) {
7176
+ return {
7177
+ isValid: false,
7178
+ error: "DNS resolution failed: hostname did not resolve to any address."
7179
+ };
7180
+ }
7181
+ for (const ip of resolvedIPs) {
7182
+ if (isBlockedIP(ip)) {
7183
+ return {
7184
+ isValid: false,
7185
+ error: "Hostname resolves to a private/internal IP address."
7186
+ };
7187
+ }
7188
+ }
7189
+ resolvedIP = resolvedIPs[0];
7190
+ } catch {
7191
+ return {
7192
+ isValid: false,
7193
+ error: "DNS resolution failed. Cannot verify hostname safety."
7194
+ };
7195
+ }
7196
+ }
7197
+ return { isValid: true, sanitizedUrl: url.toString(), resolvedIP };
7198
+ } catch (error) {
7199
+ return {
7200
+ isValid: false,
7201
+ error: `Invalid URL format: ${error instanceof Error ? error.message : "Unknown error"}`
7202
+ };
7203
+ }
7204
+ }
7205
+
7206
+ // src/tools/brand.ts
7207
+ init_version();
6810
7208
  function asEnvelope4(data) {
6811
7209
  return {
6812
7210
  _meta: {
6813
- version: "0.2.0",
7211
+ version: MCP_VERSION,
6814
7212
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6815
7213
  },
6816
7214
  data
@@ -6827,6 +7225,15 @@ function registerBrandTools(server2) {
6827
7225
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6828
7226
  },
6829
7227
  async ({ url, response_format }) => {
7228
+ const ssrfCheck = await validateUrlForSSRF(url);
7229
+ if (!ssrfCheck.isValid) {
7230
+ return {
7231
+ content: [
7232
+ { type: "text", text: `URL blocked: ${ssrfCheck.error}` }
7233
+ ],
7234
+ isError: true
7235
+ };
7236
+ }
6830
7237
  const { data, error } = await callEdgeFunction(
6831
7238
  "brand-extract",
6832
7239
  { url },
@@ -6856,7 +7263,12 @@ function registerBrandTools(server2) {
6856
7263
  }
6857
7264
  if ((response_format || "text") === "json") {
6858
7265
  return {
6859
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(data), null, 2) }]
7266
+ content: [
7267
+ {
7268
+ type: "text",
7269
+ text: JSON.stringify(asEnvelope4(data), null, 2)
7270
+ }
7271
+ ]
6860
7272
  };
6861
7273
  }
6862
7274
  const lines = [
@@ -6920,7 +7332,12 @@ function registerBrandTools(server2) {
6920
7332
  const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
6921
7333
  if (!membership) {
6922
7334
  return {
6923
- content: [{ type: "text", text: "Project is not accessible to current user." }],
7335
+ content: [
7336
+ {
7337
+ type: "text",
7338
+ text: "Project is not accessible to current user."
7339
+ }
7340
+ ],
6924
7341
  isError: true
6925
7342
  };
6926
7343
  }
@@ -6939,13 +7356,21 @@ function registerBrandTools(server2) {
6939
7356
  if (!data) {
6940
7357
  return {
6941
7358
  content: [
6942
- { type: "text", text: "No active brand profile found for this project." }
7359
+ {
7360
+ type: "text",
7361
+ text: "No active brand profile found for this project."
7362
+ }
6943
7363
  ]
6944
7364
  };
6945
7365
  }
6946
7366
  if ((response_format || "text") === "json") {
6947
7367
  return {
6948
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(data), null, 2) }]
7368
+ content: [
7369
+ {
7370
+ type: "text",
7371
+ text: JSON.stringify(asEnvelope4(data), null, 2)
7372
+ }
7373
+ ]
6949
7374
  };
6950
7375
  }
6951
7376
  const lines = [
@@ -6966,11 +7391,18 @@ function registerBrandTools(server2) {
6966
7391
  "Persist a brand profile as the active profile for a project.",
6967
7392
  {
6968
7393
  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."),
7394
+ brand_context: z5.record(z5.string(), z5.unknown()).describe(
7395
+ "Brand context payload to save to brand_profiles.brand_context."
7396
+ ),
6970
7397
  change_summary: z5.string().max(500).optional().describe("Optional summary of changes."),
6971
7398
  changed_paths: z5.array(z5.string()).optional().describe("Optional changed path list."),
6972
7399
  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."),
7400
+ extraction_method: z5.enum([
7401
+ "manual",
7402
+ "url_extract",
7403
+ "business_profiler",
7404
+ "product_showcase"
7405
+ ]).optional().describe("Extraction method metadata."),
6974
7406
  overall_confidence: z5.number().min(0).max(1).optional().describe("Optional overall confidence score in range 0..1."),
6975
7407
  extraction_metadata: z5.record(z5.string(), z5.unknown()).optional(),
6976
7408
  response_format: z5.enum(["text", "json"]).optional()
@@ -7010,20 +7442,28 @@ function registerBrandTools(server2) {
7010
7442
  const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
7011
7443
  if (!membership) {
7012
7444
  return {
7013
- content: [{ type: "text", text: "Project is not accessible to current user." }],
7445
+ content: [
7446
+ {
7447
+ type: "text",
7448
+ text: "Project is not accessible to current user."
7449
+ }
7450
+ ],
7014
7451
  isError: true
7015
7452
  };
7016
7453
  }
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
- });
7454
+ const { data: profileId, error } = await supabase.rpc(
7455
+ "set_active_brand_profile",
7456
+ {
7457
+ p_project_id: projectId,
7458
+ p_brand_context: brand_context,
7459
+ p_change_summary: change_summary || null,
7460
+ p_changed_paths: changed_paths || [],
7461
+ p_source_url: source_url || null,
7462
+ p_extraction_method: extraction_method || "manual",
7463
+ p_overall_confidence: overall_confidence ?? null,
7464
+ p_extraction_metadata: extraction_metadata || null
7465
+ }
7466
+ );
7027
7467
  if (error) {
7028
7468
  return {
7029
7469
  content: [
@@ -7044,7 +7484,12 @@ function registerBrandTools(server2) {
7044
7484
  };
7045
7485
  if ((response_format || "text") === "json") {
7046
7486
  return {
7047
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(payload), null, 2) }]
7487
+ content: [
7488
+ {
7489
+ type: "text",
7490
+ text: JSON.stringify(asEnvelope4(payload), null, 2)
7491
+ }
7492
+ ]
7048
7493
  };
7049
7494
  }
7050
7495
  return {
@@ -7100,7 +7545,10 @@ Version: ${payload.version ?? "N/A"}`
7100
7545
  if (!projectId) {
7101
7546
  return {
7102
7547
  content: [
7103
- { type: "text", text: "No project_id provided and no default project found." }
7548
+ {
7549
+ type: "text",
7550
+ text: "No project_id provided and no default project found."
7551
+ }
7104
7552
  ],
7105
7553
  isError: true
7106
7554
  };
@@ -7115,7 +7563,12 @@ Version: ${payload.version ?? "N/A"}`
7115
7563
  const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
7116
7564
  if (!membership) {
7117
7565
  return {
7118
- content: [{ type: "text", text: "Project is not accessible to current user." }],
7566
+ content: [
7567
+ {
7568
+ type: "text",
7569
+ text: "Project is not accessible to current user."
7570
+ }
7571
+ ],
7119
7572
  isError: true
7120
7573
  };
7121
7574
  }
@@ -7131,7 +7584,9 @@ Version: ${payload.version ?? "N/A"}`
7131
7584
  isError: true
7132
7585
  };
7133
7586
  }
7134
- const brandContext = { ...existingProfile.brand_context };
7587
+ const brandContext = {
7588
+ ...existingProfile.brand_context
7589
+ };
7135
7590
  const voiceProfile = brandContext.voiceProfile ?? {};
7136
7591
  const platformOverrides = voiceProfile.platformOverrides ?? {};
7137
7592
  const existingOverride = platformOverrides[platform3] ?? {};
@@ -7155,16 +7610,19 @@ Version: ${payload.version ?? "N/A"}`
7155
7610
  ...brandContext,
7156
7611
  voiceProfile: updatedVoiceProfile
7157
7612
  };
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
- });
7613
+ const { data: profileId, error: saveError } = await supabase.rpc(
7614
+ "set_active_brand_profile",
7615
+ {
7616
+ p_project_id: projectId,
7617
+ p_brand_context: updatedContext,
7618
+ p_change_summary: `Updated platform voice override for ${platform3}`,
7619
+ p_changed_paths: [`voiceProfile.platformOverrides.${platform3}`],
7620
+ p_source_url: null,
7621
+ p_extraction_method: "manual",
7622
+ p_overall_confidence: null,
7623
+ p_extraction_metadata: null
7624
+ }
7625
+ );
7168
7626
  if (saveError) {
7169
7627
  return {
7170
7628
  content: [
@@ -7185,7 +7643,12 @@ Version: ${payload.version ?? "N/A"}`
7185
7643
  };
7186
7644
  if ((response_format || "text") === "json") {
7187
7645
  return {
7188
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(payload), null, 2) }],
7646
+ content: [
7647
+ {
7648
+ type: "text",
7649
+ text: JSON.stringify(asEnvelope4(payload), null, 2)
7650
+ }
7651
+ ],
7189
7652
  isError: false
7190
7653
  };
7191
7654
  }
@@ -7283,155 +7746,6 @@ async function capturePageScreenshot(page, outputPath, selector) {
7283
7746
  // src/tools/screenshot.ts
7284
7747
  import { resolve, relative } from "node:path";
7285
7748
  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
7749
  init_supabase();
7436
7750
  function registerScreenshotTools(server2) {
7437
7751
  server2.tool(
@@ -8007,6 +8321,8 @@ function registerRemotionTools(server2) {
8007
8321
  // src/tools/insights.ts
8008
8322
  init_supabase();
8009
8323
  import { z as z8 } from "zod";
8324
+ init_version();
8325
+ var MAX_INSIGHT_AGE_DAYS = 30;
8010
8326
  var PLATFORM_ENUM = [
8011
8327
  "youtube",
8012
8328
  "tiktok",
@@ -8017,11 +8333,19 @@ var PLATFORM_ENUM = [
8017
8333
  "threads",
8018
8334
  "bluesky"
8019
8335
  ];
8020
- var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
8336
+ var DAY_NAMES = [
8337
+ "Sunday",
8338
+ "Monday",
8339
+ "Tuesday",
8340
+ "Wednesday",
8341
+ "Thursday",
8342
+ "Friday",
8343
+ "Saturday"
8344
+ ];
8021
8345
  function asEnvelope5(data) {
8022
8346
  return {
8023
8347
  _meta: {
8024
- version: "0.2.0",
8348
+ version: MCP_VERSION,
8025
8349
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
8026
8350
  },
8027
8351
  data
@@ -8032,7 +8356,12 @@ function registerInsightsTools(server2) {
8032
8356
  "get_performance_insights",
8033
8357
  "Query performance insights derived from post analytics. Returns metrics like engagement rate, view velocity, and click rate aggregated over time. Use this to understand what content is performing well.",
8034
8358
  {
8035
- insight_type: z8.enum(["top_hooks", "optimal_timing", "best_models", "competitor_patterns"]).optional().describe("Filter to a specific insight type."),
8359
+ insight_type: z8.enum([
8360
+ "top_hooks",
8361
+ "optimal_timing",
8362
+ "best_models",
8363
+ "competitor_patterns"
8364
+ ]).optional().describe("Filter to a specific insight type."),
8036
8365
  days: z8.number().min(1).max(90).optional().describe("Number of days to look back. Defaults to 30. Max 90."),
8037
8366
  limit: z8.number().min(1).max(50).optional().describe("Maximum number of insights to return. Defaults to 10."),
8038
8367
  response_format: z8.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
@@ -8051,10 +8380,13 @@ function registerInsightsTools(server2) {
8051
8380
  projectIds.push(...projects.map((p) => p.id));
8052
8381
  }
8053
8382
  }
8383
+ const effectiveDays = Math.min(lookbackDays, MAX_INSIGHT_AGE_DAYS);
8054
8384
  const since = /* @__PURE__ */ new Date();
8055
- since.setDate(since.getDate() - lookbackDays);
8385
+ since.setDate(since.getDate() - effectiveDays);
8056
8386
  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);
8387
+ let query = supabase.from("performance_insights").select(
8388
+ "id, project_id, insight_type, insight_data, confidence_score, generated_at"
8389
+ ).gte("generated_at", sinceIso).order("generated_at", { ascending: false }).limit(maxRows);
8058
8390
  if (projectIds.length > 0) {
8059
8391
  query = query.in("project_id", projectIds);
8060
8392
  } else {
@@ -8410,10 +8742,11 @@ function registerYouTubeAnalyticsTools(server2) {
8410
8742
  init_edge_function();
8411
8743
  import { z as z10 } from "zod";
8412
8744
  init_supabase();
8745
+ init_version();
8413
8746
  function asEnvelope6(data) {
8414
8747
  return {
8415
8748
  _meta: {
8416
- version: "0.2.0",
8749
+ version: MCP_VERSION,
8417
8750
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
8418
8751
  },
8419
8752
  data
@@ -8424,7 +8757,9 @@ function registerCommentsTools(server2) {
8424
8757
  "list_comments",
8425
8758
  "List YouTube comments. Without a video_id, returns recent comments across all channel videos. With a video_id, returns comments for that specific video.",
8426
8759
  {
8427
- video_id: z10.string().optional().describe("YouTube video ID. If omitted, returns comments across all channel videos."),
8760
+ video_id: z10.string().optional().describe(
8761
+ "YouTube video ID. If omitted, returns comments across all channel videos."
8762
+ ),
8428
8763
  max_results: z10.number().min(1).max(100).optional().describe("Maximum number of comments to return. Defaults to 50."),
8429
8764
  page_token: z10.string().optional().describe("Pagination token from a previous list_comments call."),
8430
8765
  response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
@@ -8439,7 +8774,9 @@ function registerCommentsTools(server2) {
8439
8774
  });
8440
8775
  if (error) {
8441
8776
  return {
8442
- content: [{ type: "text", text: `Error listing comments: ${error}` }],
8777
+ content: [
8778
+ { type: "text", text: `Error listing comments: ${error}` }
8779
+ ],
8443
8780
  isError: true
8444
8781
  };
8445
8782
  }
@@ -8451,7 +8788,10 @@ function registerCommentsTools(server2) {
8451
8788
  {
8452
8789
  type: "text",
8453
8790
  text: JSON.stringify(
8454
- asEnvelope6({ comments, nextPageToken: result.nextPageToken ?? null }),
8791
+ asEnvelope6({
8792
+ comments,
8793
+ nextPageToken: result.nextPageToken ?? null
8794
+ }),
8455
8795
  null,
8456
8796
  2
8457
8797
  )
@@ -8488,7 +8828,9 @@ function registerCommentsTools(server2) {
8488
8828
  "reply_to_comment",
8489
8829
  "Reply to a YouTube comment. Requires the parent comment ID and reply text.",
8490
8830
  {
8491
- parent_id: z10.string().describe("The ID of the parent comment to reply to (from list_comments)."),
8831
+ parent_id: z10.string().describe(
8832
+ "The ID of the parent comment to reply to (from list_comments)."
8833
+ ),
8492
8834
  text: z10.string().min(1).describe("The reply text."),
8493
8835
  response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8494
8836
  },
@@ -8527,7 +8869,12 @@ function registerCommentsTools(server2) {
8527
8869
  details: { error }
8528
8870
  });
8529
8871
  return {
8530
- content: [{ type: "text", text: `Error replying to comment: ${error}` }],
8872
+ content: [
8873
+ {
8874
+ type: "text",
8875
+ text: `Error replying to comment: ${error}`
8876
+ }
8877
+ ],
8531
8878
  isError: true
8532
8879
  };
8533
8880
  }
@@ -8540,7 +8887,12 @@ function registerCommentsTools(server2) {
8540
8887
  });
8541
8888
  if (format === "json") {
8542
8889
  return {
8543
- content: [{ type: "text", text: JSON.stringify(asEnvelope6(result), null, 2) }]
8890
+ content: [
8891
+ {
8892
+ type: "text",
8893
+ text: JSON.stringify(asEnvelope6(result), null, 2)
8894
+ }
8895
+ ]
8544
8896
  };
8545
8897
  }
8546
8898
  return {
@@ -8598,7 +8950,9 @@ function registerCommentsTools(server2) {
8598
8950
  details: { error }
8599
8951
  });
8600
8952
  return {
8601
- content: [{ type: "text", text: `Error posting comment: ${error}` }],
8953
+ content: [
8954
+ { type: "text", text: `Error posting comment: ${error}` }
8955
+ ],
8602
8956
  isError: true
8603
8957
  };
8604
8958
  }
@@ -8611,7 +8965,12 @@ function registerCommentsTools(server2) {
8611
8965
  });
8612
8966
  if (format === "json") {
8613
8967
  return {
8614
- content: [{ type: "text", text: JSON.stringify(asEnvelope6(result), null, 2) }]
8968
+ content: [
8969
+ {
8970
+ type: "text",
8971
+ text: JSON.stringify(asEnvelope6(result), null, 2)
8972
+ }
8973
+ ]
8615
8974
  };
8616
8975
  }
8617
8976
  return {
@@ -8669,7 +9028,12 @@ function registerCommentsTools(server2) {
8669
9028
  details: { error }
8670
9029
  });
8671
9030
  return {
8672
- content: [{ type: "text", text: `Error moderating comment: ${error}` }],
9031
+ content: [
9032
+ {
9033
+ type: "text",
9034
+ text: `Error moderating comment: ${error}`
9035
+ }
9036
+ ],
8673
9037
  isError: true
8674
9038
  };
8675
9039
  }
@@ -8748,7 +9112,9 @@ function registerCommentsTools(server2) {
8748
9112
  details: { error }
8749
9113
  });
8750
9114
  return {
8751
- content: [{ type: "text", text: `Error deleting comment: ${error}` }],
9115
+ content: [
9116
+ { type: "text", text: `Error deleting comment: ${error}` }
9117
+ ],
8752
9118
  isError: true
8753
9119
  };
8754
9120
  }
@@ -8763,13 +9129,22 @@ function registerCommentsTools(server2) {
8763
9129
  content: [
8764
9130
  {
8765
9131
  type: "text",
8766
- text: JSON.stringify(asEnvelope6({ success: true, commentId: comment_id }), null, 2)
9132
+ text: JSON.stringify(
9133
+ asEnvelope6({ success: true, commentId: comment_id }),
9134
+ null,
9135
+ 2
9136
+ )
8767
9137
  }
8768
9138
  ]
8769
9139
  };
8770
9140
  }
8771
9141
  return {
8772
- content: [{ type: "text", text: `Comment ${comment_id} deleted successfully.` }]
9142
+ content: [
9143
+ {
9144
+ type: "text",
9145
+ text: `Comment ${comment_id} deleted successfully.`
9146
+ }
9147
+ ]
8773
9148
  };
8774
9149
  }
8775
9150
  );
@@ -8778,6 +9153,7 @@ function registerCommentsTools(server2) {
8778
9153
  // src/tools/ideation-context.ts
8779
9154
  init_supabase();
8780
9155
  import { z as z11 } from "zod";
9156
+ init_version();
8781
9157
  function transformInsightsToPerformanceContext(projectId, insights) {
8782
9158
  if (!insights.length) {
8783
9159
  return {
@@ -8797,8 +9173,12 @@ function transformInsightsToPerformanceContext(projectId, insights) {
8797
9173
  };
8798
9174
  }
8799
9175
  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");
9176
+ const optimalTimingInsight = insights.find(
9177
+ (i) => i.insight_type === "optimal_timing"
9178
+ );
9179
+ const bestModelsInsight = insights.find(
9180
+ (i) => i.insight_type === "best_models"
9181
+ );
8802
9182
  const topHooks = topHooksInsight?.insight_data?.hooks || [];
8803
9183
  const hooksSummary = topHooksInsight?.insight_data?.summary || "";
8804
9184
  const timingSummary = optimalTimingInsight?.insight_data?.summary || "";
@@ -8809,7 +9189,10 @@ function transformInsightsToPerformanceContext(projectId, insights) {
8809
9189
  if (hooksSummary) promptParts.push(hooksSummary);
8810
9190
  if (timingSummary) promptParts.push(timingSummary);
8811
9191
  if (modelSummary) promptParts.push(modelSummary);
8812
- if (topHooks.length) promptParts.push(`Top performing hooks: ${topHooks.slice(0, 3).join(", ")}`);
9192
+ if (topHooks.length)
9193
+ promptParts.push(
9194
+ `Top performing hooks: ${topHooks.slice(0, 3).join(", ")}`
9195
+ );
8813
9196
  return {
8814
9197
  projectId,
8815
9198
  hasHistoricalData: true,
@@ -8835,7 +9218,7 @@ function transformInsightsToPerformanceContext(projectId, insights) {
8835
9218
  function asEnvelope7(data) {
8836
9219
  return {
8837
9220
  _meta: {
8838
- version: "0.2.0",
9221
+ version: MCP_VERSION,
8839
9222
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
8840
9223
  },
8841
9224
  data
@@ -8858,7 +9241,12 @@ function registerIdeationContextTools(server2) {
8858
9241
  const { data: member } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).limit(1).single();
8859
9242
  if (!member?.organization_id) {
8860
9243
  return {
8861
- content: [{ type: "text", text: "No organization found for current user." }],
9244
+ content: [
9245
+ {
9246
+ type: "text",
9247
+ text: "No organization found for current user."
9248
+ }
9249
+ ],
8862
9250
  isError: true
8863
9251
  };
8864
9252
  }
@@ -8866,7 +9254,10 @@ function registerIdeationContextTools(server2) {
8866
9254
  if (projectsError) {
8867
9255
  return {
8868
9256
  content: [
8869
- { type: "text", text: `Failed to resolve projects: ${projectsError.message}` }
9257
+ {
9258
+ type: "text",
9259
+ text: `Failed to resolve projects: ${projectsError.message}`
9260
+ }
8870
9261
  ],
8871
9262
  isError: true
8872
9263
  };
@@ -8874,7 +9265,12 @@ function registerIdeationContextTools(server2) {
8874
9265
  const projectIds = (projects || []).map((p) => p.id);
8875
9266
  if (projectIds.length === 0) {
8876
9267
  return {
8877
- content: [{ type: "text", text: "No projects found for current user." }],
9268
+ content: [
9269
+ {
9270
+ type: "text",
9271
+ text: "No projects found for current user."
9272
+ }
9273
+ ],
8878
9274
  isError: true
8879
9275
  };
8880
9276
  }
@@ -8883,7 +9279,10 @@ function registerIdeationContextTools(server2) {
8883
9279
  if (!selectedProjectId) {
8884
9280
  return {
8885
9281
  content: [
8886
- { type: "text", text: "No accessible project found for current user." }
9282
+ {
9283
+ type: "text",
9284
+ text: "No accessible project found for current user."
9285
+ }
8887
9286
  ],
8888
9287
  isError: true
8889
9288
  };
@@ -8901,7 +9300,9 @@ function registerIdeationContextTools(server2) {
8901
9300
  }
8902
9301
  const since = /* @__PURE__ */ new Date();
8903
9302
  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);
9303
+ const { data: insights, error } = await supabase.from("performance_insights").select(
9304
+ "id, project_id, insight_type, insight_data, generated_at, expires_at"
9305
+ ).eq("project_id", selectedProjectId).gte("generated_at", since.toISOString()).gt("expires_at", (/* @__PURE__ */ new Date()).toISOString()).order("generated_at", { ascending: false }).limit(30);
8905
9306
  if (error) {
8906
9307
  return {
8907
9308
  content: [
@@ -8919,7 +9320,12 @@ function registerIdeationContextTools(server2) {
8919
9320
  );
8920
9321
  if (format === "json") {
8921
9322
  return {
8922
- content: [{ type: "text", text: JSON.stringify(asEnvelope7(context), null, 2) }]
9323
+ content: [
9324
+ {
9325
+ type: "text",
9326
+ text: JSON.stringify(asEnvelope7(context), null, 2)
9327
+ }
9328
+ ]
8923
9329
  };
8924
9330
  }
8925
9331
  const lines = [
@@ -8940,10 +9346,11 @@ function registerIdeationContextTools(server2) {
8940
9346
  // src/tools/credits.ts
8941
9347
  init_supabase();
8942
9348
  import { z as z12 } from "zod";
9349
+ init_version();
8943
9350
  function asEnvelope8(data) {
8944
9351
  return {
8945
9352
  _meta: {
8946
- version: "0.2.0",
9353
+ version: MCP_VERSION,
8947
9354
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
8948
9355
  },
8949
9356
  data
@@ -8982,7 +9389,12 @@ function registerCreditsTools(server2) {
8982
9389
  };
8983
9390
  if ((response_format || "text") === "json") {
8984
9391
  return {
8985
- content: [{ type: "text", text: JSON.stringify(asEnvelope8(payload), null, 2) }]
9392
+ content: [
9393
+ {
9394
+ type: "text",
9395
+ text: JSON.stringify(asEnvelope8(payload), null, 2)
9396
+ }
9397
+ ]
8986
9398
  };
8987
9399
  }
8988
9400
  return {
@@ -9016,7 +9428,12 @@ Monthly used: ${payload.monthlyUsed}` + (payload.monthlyLimit ? ` / ${payload.mo
9016
9428
  };
9017
9429
  if ((response_format || "text") === "json") {
9018
9430
  return {
9019
- content: [{ type: "text", text: JSON.stringify(asEnvelope8(payload), null, 2) }]
9431
+ content: [
9432
+ {
9433
+ type: "text",
9434
+ text: JSON.stringify(asEnvelope8(payload), null, 2)
9435
+ }
9436
+ ]
9020
9437
  };
9021
9438
  }
9022
9439
  return {
@@ -9039,11 +9456,12 @@ Assets remaining: ${payload.remainingAssets ?? "unlimited"}`
9039
9456
 
9040
9457
  // src/tools/loop-summary.ts
9041
9458
  init_supabase();
9459
+ init_version();
9042
9460
  import { z as z13 } from "zod";
9043
9461
  function asEnvelope9(data) {
9044
9462
  return {
9045
9463
  _meta: {
9046
- version: "0.2.0",
9464
+ version: MCP_VERSION,
9047
9465
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
9048
9466
  },
9049
9467
  data
@@ -9082,7 +9500,12 @@ function registerLoopSummaryTools(server2) {
9082
9500
  const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
9083
9501
  if (!membership) {
9084
9502
  return {
9085
- content: [{ type: "text", text: "Project is not accessible to current user." }],
9503
+ content: [
9504
+ {
9505
+ type: "text",
9506
+ text: "Project is not accessible to current user."
9507
+ }
9508
+ ],
9086
9509
  isError: true
9087
9510
  };
9088
9511
  }
@@ -9105,7 +9528,12 @@ function registerLoopSummaryTools(server2) {
9105
9528
  };
9106
9529
  if ((response_format || "text") === "json") {
9107
9530
  return {
9108
- content: [{ type: "text", text: JSON.stringify(asEnvelope9(payload), null, 2) }]
9531
+ content: [
9532
+ {
9533
+ type: "text",
9534
+ text: JSON.stringify(asEnvelope9(payload), null, 2)
9535
+ }
9536
+ ]
9109
9537
  };
9110
9538
  }
9111
9539
  return {
@@ -9435,8 +9863,12 @@ ${"=".repeat(40)}
9435
9863
  init_edge_function();
9436
9864
  init_supabase();
9437
9865
  import { z as z16 } from "zod";
9866
+ init_version();
9438
9867
  function asEnvelope12(data) {
9439
- return { _meta: { version: "0.2.0", timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
9868
+ return {
9869
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
9870
+ data
9871
+ };
9440
9872
  }
9441
9873
  function isYouTubeUrl(url) {
9442
9874
  if (/youtube\.com\/watch|youtu\.be\//.test(url)) return "video";
@@ -9467,13 +9899,17 @@ Metadata:`);
9467
9899
  if (m.tags?.length) lines.push(` Tags: ${m.tags.join(", ")}`);
9468
9900
  }
9469
9901
  if (content.features?.length)
9470
- lines.push(`
9902
+ lines.push(
9903
+ `
9471
9904
  Features:
9472
- ${content.features.map((f) => ` - ${f}`).join("\n")}`);
9905
+ ${content.features.map((f) => ` - ${f}`).join("\n")}`
9906
+ );
9473
9907
  if (content.benefits?.length)
9474
- lines.push(`
9908
+ lines.push(
9909
+ `
9475
9910
  Benefits:
9476
- ${content.benefits.map((b) => ` - ${b}`).join("\n")}`);
9911
+ ${content.benefits.map((b) => ` - ${b}`).join("\n")}`
9912
+ );
9477
9913
  if (content.usp) lines.push(`
9478
9914
  USP: ${content.usp}`);
9479
9915
  if (content.suggested_hooks?.length)
@@ -9495,12 +9931,20 @@ function registerExtractionTools(server2) {
9495
9931
  max_results: z16.number().min(1).max(100).default(10).describe("Max comments to include"),
9496
9932
  response_format: z16.enum(["text", "json"]).default("text")
9497
9933
  },
9498
- async ({ url, extract_type, include_comments, max_results, response_format }) => {
9934
+ async ({
9935
+ url,
9936
+ extract_type,
9937
+ include_comments,
9938
+ max_results,
9939
+ response_format
9940
+ }) => {
9499
9941
  const startedAt = Date.now();
9500
9942
  const ssrfCheck = await validateUrlForSSRF(url);
9501
9943
  if (!ssrfCheck.isValid) {
9502
9944
  return {
9503
- content: [{ type: "text", text: `URL blocked: ${ssrfCheck.error}` }],
9945
+ content: [
9946
+ { type: "text", text: `URL blocked: ${ssrfCheck.error}` }
9947
+ ],
9504
9948
  isError: true
9505
9949
  };
9506
9950
  }
@@ -9628,13 +10072,21 @@ function registerExtractionTools(server2) {
9628
10072
  if (response_format === "json") {
9629
10073
  return {
9630
10074
  content: [
9631
- { type: "text", text: JSON.stringify(asEnvelope12(extracted), null, 2) }
10075
+ {
10076
+ type: "text",
10077
+ text: JSON.stringify(asEnvelope12(extracted), null, 2)
10078
+ }
9632
10079
  ],
9633
10080
  isError: false
9634
10081
  };
9635
10082
  }
9636
10083
  return {
9637
- content: [{ type: "text", text: formatExtractedContentAsText(extracted) }],
10084
+ content: [
10085
+ {
10086
+ type: "text",
10087
+ text: formatExtractedContentAsText(extracted)
10088
+ }
10089
+ ],
9638
10090
  isError: false
9639
10091
  };
9640
10092
  } catch (err) {
@@ -9647,7 +10099,9 @@ function registerExtractionTools(server2) {
9647
10099
  details: { url, error: message }
9648
10100
  });
9649
10101
  return {
9650
- content: [{ type: "text", text: `Extraction failed: ${message}` }],
10102
+ content: [
10103
+ { type: "text", text: `Extraction failed: ${message}` }
10104
+ ],
9651
10105
  isError: true
9652
10106
  };
9653
10107
  }
@@ -9658,9 +10112,13 @@ function registerExtractionTools(server2) {
9658
10112
  // src/tools/quality.ts
9659
10113
  init_quality();
9660
10114
  init_supabase();
10115
+ init_version();
9661
10116
  import { z as z17 } from "zod";
9662
10117
  function asEnvelope13(data) {
9663
- return { _meta: { version: "0.2.0", timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
10118
+ return {
10119
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
10120
+ data
10121
+ };
9664
10122
  }
9665
10123
  function registerQualityTools(server2) {
9666
10124
  server2.tool(
@@ -9716,7 +10174,12 @@ function registerQualityTools(server2) {
9716
10174
  });
9717
10175
  if (response_format === "json") {
9718
10176
  return {
9719
- content: [{ type: "text", text: JSON.stringify(asEnvelope13(result), null, 2) }],
10177
+ content: [
10178
+ {
10179
+ type: "text",
10180
+ text: JSON.stringify(asEnvelope13(result), null, 2)
10181
+ }
10182
+ ],
9720
10183
  isError: false
9721
10184
  };
9722
10185
  }
@@ -9726,7 +10189,9 @@ function registerQualityTools(server2) {
9726
10189
  );
9727
10190
  lines.push("");
9728
10191
  for (const cat of result.categories) {
9729
- lines.push(` ${cat.name}: ${cat.score}/${cat.maxScore} \u2014 ${cat.detail}`);
10192
+ lines.push(
10193
+ ` ${cat.name}: ${cat.score}/${cat.maxScore} \u2014 ${cat.detail}`
10194
+ );
9730
10195
  }
9731
10196
  if (result.blockers.length > 0) {
9732
10197
  lines.push("");
@@ -9737,7 +10202,10 @@ function registerQualityTools(server2) {
9737
10202
  }
9738
10203
  lines.push("");
9739
10204
  lines.push(`Threshold: ${result.threshold}/${result.maxTotal}`);
9740
- return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
10205
+ return {
10206
+ content: [{ type: "text", text: lines.join("\n") }],
10207
+ isError: false
10208
+ };
9741
10209
  }
9742
10210
  );
9743
10211
  server2.tool(
@@ -9778,7 +10246,9 @@ function registerQualityTools(server2) {
9778
10246
  });
9779
10247
  const scores = postsWithQuality.map((p) => p.quality.score);
9780
10248
  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;
10249
+ const avgScore = scores.length > 0 ? Math.round(
10250
+ scores.reduce((a, b) => a + b, 0) / scores.length * 10
10251
+ ) / 10 : 0;
9782
10252
  const summary = {
9783
10253
  total_posts: plan.posts.length,
9784
10254
  passed,
@@ -9797,25 +10267,36 @@ function registerQualityTools(server2) {
9797
10267
  content: [
9798
10268
  {
9799
10269
  type: "text",
9800
- text: JSON.stringify(asEnvelope13({ posts: postsWithQuality, summary }), null, 2)
10270
+ text: JSON.stringify(
10271
+ asEnvelope13({ posts: postsWithQuality, summary }),
10272
+ null,
10273
+ 2
10274
+ )
9801
10275
  }
9802
10276
  ],
9803
10277
  isError: false
9804
10278
  };
9805
10279
  }
9806
10280
  const lines = [];
9807
- lines.push(`PLAN QUALITY: ${passed}/${plan.posts.length} passed (avg: ${avgScore}/35)`);
10281
+ lines.push(
10282
+ `PLAN QUALITY: ${passed}/${plan.posts.length} passed (avg: ${avgScore}/35)`
10283
+ );
9808
10284
  lines.push("");
9809
10285
  for (const post of postsWithQuality) {
9810
10286
  const icon = post.quality.passed ? "[PASS]" : "[FAIL]";
9811
- lines.push(`${icon} ${post.id} | ${post.platform} | ${post.quality.score}/35`);
10287
+ lines.push(
10288
+ `${icon} ${post.id} | ${post.platform} | ${post.quality.score}/35`
10289
+ );
9812
10290
  if (post.quality.blockers.length > 0) {
9813
10291
  for (const b of post.quality.blockers) {
9814
10292
  lines.push(` - ${b}`);
9815
10293
  }
9816
10294
  }
9817
10295
  }
9818
- return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
10296
+ return {
10297
+ content: [{ type: "text", text: lines.join("\n") }],
10298
+ isError: false
10299
+ };
9819
10300
  }
9820
10301
  );
9821
10302
  }
@@ -9825,11 +10306,15 @@ init_edge_function();
9825
10306
  init_supabase();
9826
10307
  import { z as z18 } from "zod";
9827
10308
  import { randomUUID as randomUUID2 } from "node:crypto";
10309
+ init_version();
9828
10310
  function toRecord(value) {
9829
10311
  return value && typeof value === "object" ? value : void 0;
9830
10312
  }
9831
10313
  function asEnvelope14(data) {
9832
- return { _meta: { version: "0.2.0", timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
10314
+ return {
10315
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
10316
+ data
10317
+ };
9833
10318
  }
9834
10319
  function tomorrowIsoDate() {
9835
10320
  const d = /* @__PURE__ */ new Date();
@@ -9867,7 +10352,9 @@ function formatPlanAsText(plan) {
9867
10352
  lines.push(`WEEKLY CONTENT PLAN: "${plan.topic}"`);
9868
10353
  lines.push(`Period: ${plan.start_date} to ${plan.end_date}`);
9869
10354
  lines.push(`Platforms: ${plan.platforms.join(", ")}`);
9870
- lines.push(`Posts: ${plan.posts.length} | Estimated credits: ~${plan.estimated_credits}`);
10355
+ lines.push(
10356
+ `Posts: ${plan.posts.length} | Estimated credits: ~${plan.estimated_credits}`
10357
+ );
9871
10358
  if (plan.plan_id) lines.push(`Plan ID: ${plan.plan_id}`);
9872
10359
  if (plan.insights_applied?.has_historical_data) {
9873
10360
  lines.push("");
@@ -9882,7 +10369,9 @@ function formatPlanAsText(plan) {
9882
10369
  `- Best posting time: ${days[timing.dayOfWeek] ?? timing.dayOfWeek} ${timing.hourOfDay}:00`
9883
10370
  );
9884
10371
  }
9885
- lines.push(`- Recommended model: ${plan.insights_applied.recommended_model ?? "N/A"}`);
10372
+ lines.push(
10373
+ `- Recommended model: ${plan.insights_applied.recommended_model ?? "N/A"}`
10374
+ );
9886
10375
  lines.push(`- Insights count: ${plan.insights_applied.insights_count}`);
9887
10376
  }
9888
10377
  lines.push("");
@@ -9903,9 +10392,11 @@ function formatPlanAsText(plan) {
9903
10392
  ` Caption: ${post.caption.slice(0, 200)}${post.caption.length > 200 ? "..." : ""}`
9904
10393
  );
9905
10394
  if (post.title) lines.push(` Title: ${post.title}`);
9906
- if (post.visual_direction) lines.push(` Visual: ${post.visual_direction}`);
10395
+ if (post.visual_direction)
10396
+ lines.push(` Visual: ${post.visual_direction}`);
9907
10397
  if (post.media_type) lines.push(` Media: ${post.media_type}`);
9908
- if (post.hashtags?.length) lines.push(` Hashtags: ${post.hashtags.join(" ")}`);
10398
+ if (post.hashtags?.length)
10399
+ lines.push(` Hashtags: ${post.hashtags.join(" ")}`);
9909
10400
  lines.push("");
9910
10401
  }
9911
10402
  }
@@ -9988,7 +10479,10 @@ function registerPlanningTools(server2) {
9988
10479
  "mcp-data",
9989
10480
  {
9990
10481
  action: "brand-profile",
9991
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
10482
+ ...resolvedProjectId ? {
10483
+ projectId: resolvedProjectId,
10484
+ project_id: resolvedProjectId
10485
+ } : {}
9992
10486
  },
9993
10487
  { timeoutMs: 15e3 }
9994
10488
  );
@@ -10192,7 +10686,12 @@ ${rawText.slice(0, 1e3)}`
10192
10686
  details: { topic, error: `plan persistence failed: ${message}` }
10193
10687
  });
10194
10688
  return {
10195
- content: [{ type: "text", text: `Plan persistence failed: ${message}` }],
10689
+ content: [
10690
+ {
10691
+ type: "text",
10692
+ text: `Plan persistence failed: ${message}`
10693
+ }
10694
+ ],
10196
10695
  isError: true
10197
10696
  };
10198
10697
  }
@@ -10206,7 +10705,12 @@ ${rawText.slice(0, 1e3)}`
10206
10705
  });
10207
10706
  if (response_format === "json") {
10208
10707
  return {
10209
- content: [{ type: "text", text: JSON.stringify(asEnvelope14(plan), null, 2) }],
10708
+ content: [
10709
+ {
10710
+ type: "text",
10711
+ text: JSON.stringify(asEnvelope14(plan), null, 2)
10712
+ }
10713
+ ],
10210
10714
  isError: false
10211
10715
  };
10212
10716
  }
@@ -10224,7 +10728,12 @@ ${rawText.slice(0, 1e3)}`
10224
10728
  details: { topic, error: message }
10225
10729
  });
10226
10730
  return {
10227
- content: [{ type: "text", text: `Plan generation failed: ${message}` }],
10731
+ content: [
10732
+ {
10733
+ type: "text",
10734
+ text: `Plan generation failed: ${message}`
10735
+ }
10736
+ ],
10228
10737
  isError: true
10229
10738
  };
10230
10739
  }
@@ -10284,7 +10793,11 @@ ${rawText.slice(0, 1e3)}`
10284
10793
  toolName: "save_content_plan",
10285
10794
  status: "success",
10286
10795
  durationMs,
10287
- details: { plan_id: planId, project_id: resolvedProjectId, status: normalizedStatus }
10796
+ details: {
10797
+ plan_id: planId,
10798
+ project_id: resolvedProjectId,
10799
+ status: normalizedStatus
10800
+ }
10288
10801
  });
10289
10802
  const result = {
10290
10803
  plan_id: planId,
@@ -10293,13 +10806,21 @@ ${rawText.slice(0, 1e3)}`
10293
10806
  };
10294
10807
  if (response_format === "json") {
10295
10808
  return {
10296
- content: [{ type: "text", text: JSON.stringify(asEnvelope14(result), null, 2) }],
10809
+ content: [
10810
+ {
10811
+ type: "text",
10812
+ text: JSON.stringify(asEnvelope14(result), null, 2)
10813
+ }
10814
+ ],
10297
10815
  isError: false
10298
10816
  };
10299
10817
  }
10300
10818
  return {
10301
10819
  content: [
10302
- { type: "text", text: `Saved content plan ${planId} (${normalizedStatus}).` }
10820
+ {
10821
+ type: "text",
10822
+ text: `Saved content plan ${planId} (${normalizedStatus}).`
10823
+ }
10303
10824
  ],
10304
10825
  isError: false
10305
10826
  };
@@ -10313,7 +10834,12 @@ ${rawText.slice(0, 1e3)}`
10313
10834
  details: { error: message }
10314
10835
  });
10315
10836
  return {
10316
- content: [{ type: "text", text: `Failed to save content plan: ${message}` }],
10837
+ content: [
10838
+ {
10839
+ type: "text",
10840
+ text: `Failed to save content plan: ${message}`
10841
+ }
10842
+ ],
10317
10843
  isError: true
10318
10844
  };
10319
10845
  }
@@ -10329,7 +10855,9 @@ ${rawText.slice(0, 1e3)}`
10329
10855
  async ({ plan_id, response_format }) => {
10330
10856
  const supabase = getSupabaseClient();
10331
10857
  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();
10858
+ const { data, error } = await supabase.from("content_plans").select(
10859
+ "id, topic, status, plan_payload, insights_applied, created_at, updated_at"
10860
+ ).eq("id", plan_id).eq("user_id", userId).maybeSingle();
10333
10861
  if (error) {
10334
10862
  return {
10335
10863
  content: [
@@ -10344,7 +10872,10 @@ ${rawText.slice(0, 1e3)}`
10344
10872
  if (!data) {
10345
10873
  return {
10346
10874
  content: [
10347
- { type: "text", text: `No content plan found for plan_id=${plan_id}` }
10875
+ {
10876
+ type: "text",
10877
+ text: `No content plan found for plan_id=${plan_id}`
10878
+ }
10348
10879
  ],
10349
10880
  isError: true
10350
10881
  };
@@ -10360,7 +10891,12 @@ ${rawText.slice(0, 1e3)}`
10360
10891
  };
10361
10892
  if (response_format === "json") {
10362
10893
  return {
10363
- content: [{ type: "text", text: JSON.stringify(asEnvelope14(payload), null, 2) }],
10894
+ content: [
10895
+ {
10896
+ type: "text",
10897
+ text: JSON.stringify(asEnvelope14(payload), null, 2)
10898
+ }
10899
+ ],
10364
10900
  isError: false
10365
10901
  };
10366
10902
  }
@@ -10371,7 +10907,10 @@ ${rawText.slice(0, 1e3)}`
10371
10907
  `Status: ${data.status}`,
10372
10908
  `Posts: ${Array.isArray(plan?.posts) ? plan.posts.length : 0}`
10373
10909
  ];
10374
- return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
10910
+ return {
10911
+ content: [{ type: "text", text: lines.join("\n") }],
10912
+ isError: false
10913
+ };
10375
10914
  }
10376
10915
  );
10377
10916
  server2.tool(
@@ -10414,14 +10953,19 @@ ${rawText.slice(0, 1e3)}`
10414
10953
  if (!stored?.plan_payload) {
10415
10954
  return {
10416
10955
  content: [
10417
- { type: "text", text: `No content plan found for plan_id=${plan_id}` }
10956
+ {
10957
+ type: "text",
10958
+ text: `No content plan found for plan_id=${plan_id}`
10959
+ }
10418
10960
  ],
10419
10961
  isError: true
10420
10962
  };
10421
10963
  }
10422
10964
  const plan = stored.plan_payload;
10423
10965
  const existingPosts = Array.isArray(plan.posts) ? plan.posts : [];
10424
- const updatesById = new Map(post_updates.map((update) => [update.post_id, update]));
10966
+ const updatesById = new Map(
10967
+ post_updates.map((update) => [update.post_id, update])
10968
+ );
10425
10969
  const updatedPosts = existingPosts.map((post) => {
10426
10970
  const update = updatesById.get(post.id);
10427
10971
  if (!update) return post;
@@ -10439,7 +10983,9 @@ ${rawText.slice(0, 1e3)}`
10439
10983
  ...update.status !== void 0 ? { status: update.status } : {}
10440
10984
  };
10441
10985
  });
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";
10986
+ const nextStatus = updatedPosts.length > 0 && updatedPosts.every(
10987
+ (post) => post.status === "approved" || post.status === "edited"
10988
+ ) ? "approved" : stored.status === "scheduled" || stored.status === "completed" ? stored.status : "draft";
10443
10989
  const updatedPlan = {
10444
10990
  ...plan,
10445
10991
  posts: updatedPosts
@@ -10466,7 +11012,12 @@ ${rawText.slice(0, 1e3)}`
10466
11012
  };
10467
11013
  if (response_format === "json") {
10468
11014
  return {
10469
- content: [{ type: "text", text: JSON.stringify(asEnvelope14(payload), null, 2) }],
11015
+ content: [
11016
+ {
11017
+ type: "text",
11018
+ text: JSON.stringify(asEnvelope14(payload), null, 2)
11019
+ }
11020
+ ],
10470
11021
  isError: false
10471
11022
  };
10472
11023
  }
@@ -10506,7 +11057,10 @@ ${rawText.slice(0, 1e3)}`
10506
11057
  if (!stored?.plan_payload || !stored.project_id) {
10507
11058
  return {
10508
11059
  content: [
10509
- { type: "text", text: `No content plan found for plan_id=${plan_id}` }
11060
+ {
11061
+ type: "text",
11062
+ text: `No content plan found for plan_id=${plan_id}`
11063
+ }
10510
11064
  ],
10511
11065
  isError: true
10512
11066
  };
@@ -10515,7 +11069,12 @@ ${rawText.slice(0, 1e3)}`
10515
11069
  const posts = Array.isArray(plan.posts) ? plan.posts : [];
10516
11070
  if (posts.length === 0) {
10517
11071
  return {
10518
- content: [{ type: "text", text: `Plan ${plan_id} has no posts to submit.` }],
11072
+ content: [
11073
+ {
11074
+ type: "text",
11075
+ text: `Plan ${plan_id} has no posts to submit.`
11076
+ }
11077
+ ],
10519
11078
  isError: true
10520
11079
  };
10521
11080
  }
@@ -10558,7 +11117,12 @@ ${rawText.slice(0, 1e3)}`
10558
11117
  };
10559
11118
  if (response_format === "json") {
10560
11119
  return {
10561
- content: [{ type: "text", text: JSON.stringify(asEnvelope14(payload), null, 2) }],
11120
+ content: [
11121
+ {
11122
+ type: "text",
11123
+ text: JSON.stringify(asEnvelope14(payload), null, 2)
11124
+ }
11125
+ ],
10562
11126
  isError: false
10563
11127
  };
10564
11128
  }
@@ -10578,10 +11142,11 @@ ${rawText.slice(0, 1e3)}`
10578
11142
  // src/tools/plan-approvals.ts
10579
11143
  init_supabase();
10580
11144
  import { z as z19 } from "zod";
11145
+ init_version();
10581
11146
  function asEnvelope15(data) {
10582
11147
  return {
10583
11148
  _meta: {
10584
- version: "0.2.0",
11149
+ version: MCP_VERSION,
10585
11150
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
10586
11151
  },
10587
11152
  data
@@ -10620,14 +11185,24 @@ function registerPlanApprovalTools(server2) {
10620
11185
  if (!projectId) {
10621
11186
  return {
10622
11187
  content: [
10623
- { type: "text", text: "No project_id provided and no default project found." }
11188
+ {
11189
+ type: "text",
11190
+ text: "No project_id provided and no default project found."
11191
+ }
10624
11192
  ],
10625
11193
  isError: true
10626
11194
  };
10627
11195
  }
10628
- const accessError = await assertProjectAccess(supabase, userId, projectId);
11196
+ const accessError = await assertProjectAccess(
11197
+ supabase,
11198
+ userId,
11199
+ projectId
11200
+ );
10629
11201
  if (accessError) {
10630
- return { content: [{ type: "text", text: accessError }], isError: true };
11202
+ return {
11203
+ content: [{ type: "text", text: accessError }],
11204
+ isError: true
11205
+ };
10631
11206
  }
10632
11207
  const rows = posts.map((post) => ({
10633
11208
  plan_id,
@@ -10656,7 +11231,12 @@ function registerPlanApprovalTools(server2) {
10656
11231
  };
10657
11232
  if ((response_format || "text") === "json") {
10658
11233
  return {
10659
- content: [{ type: "text", text: JSON.stringify(asEnvelope15(payload), null, 2) }],
11234
+ content: [
11235
+ {
11236
+ type: "text",
11237
+ text: JSON.stringify(asEnvelope15(payload), null, 2)
11238
+ }
11239
+ ],
10660
11240
  isError: false
10661
11241
  };
10662
11242
  }
@@ -10705,14 +11285,22 @@ function registerPlanApprovalTools(server2) {
10705
11285
  };
10706
11286
  if ((response_format || "text") === "json") {
10707
11287
  return {
10708
- content: [{ type: "text", text: JSON.stringify(asEnvelope15(payload), null, 2) }],
11288
+ content: [
11289
+ {
11290
+ type: "text",
11291
+ text: JSON.stringify(asEnvelope15(payload), null, 2)
11292
+ }
11293
+ ],
10709
11294
  isError: false
10710
11295
  };
10711
11296
  }
10712
11297
  if (!data || data.length === 0) {
10713
11298
  return {
10714
11299
  content: [
10715
- { type: "text", text: `No approval items found for plan ${plan_id}.` }
11300
+ {
11301
+ type: "text",
11302
+ text: `No approval items found for plan ${plan_id}.`
11303
+ }
10716
11304
  ],
10717
11305
  isError: false
10718
11306
  };
@@ -10725,7 +11313,10 @@ function registerPlanApprovalTools(server2) {
10725
11313
  }
10726
11314
  lines.push("");
10727
11315
  lines.push(`Total: ${data.length}`);
10728
- return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
11316
+ return {
11317
+ content: [{ type: "text", text: lines.join("\n") }],
11318
+ isError: false
11319
+ };
10729
11320
  }
10730
11321
  );
10731
11322
  server2.tool(
@@ -10744,7 +11335,10 @@ function registerPlanApprovalTools(server2) {
10744
11335
  if (decision === "edited" && !edited_post) {
10745
11336
  return {
10746
11337
  content: [
10747
- { type: "text", text: 'edited_post is required when decision is "edited".' }
11338
+ {
11339
+ type: "text",
11340
+ text: 'edited_post is required when decision is "edited".'
11341
+ }
10748
11342
  ],
10749
11343
  isError: true
10750
11344
  };
@@ -10757,7 +11351,9 @@ function registerPlanApprovalTools(server2) {
10757
11351
  if (decision === "edited") {
10758
11352
  updates.edited_post = edited_post;
10759
11353
  }
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();
11354
+ const { data, error } = await supabase.from("content_plan_approvals").update(updates).eq("id", approval_id).eq("user_id", userId).eq("status", "pending").select(
11355
+ "id, plan_id, post_id, status, reason, decided_at, original_post, edited_post"
11356
+ ).maybeSingle();
10761
11357
  if (error) {
10762
11358
  return {
10763
11359
  content: [
@@ -10782,7 +11378,12 @@ function registerPlanApprovalTools(server2) {
10782
11378
  }
10783
11379
  if ((response_format || "text") === "json") {
10784
11380
  return {
10785
- content: [{ type: "text", text: JSON.stringify(asEnvelope15(data), null, 2) }],
11381
+ content: [
11382
+ {
11383
+ type: "text",
11384
+ text: JSON.stringify(asEnvelope15(data), null, 2)
11385
+ }
11386
+ ],
10786
11387
  isError: false
10787
11388
  };
10788
11389
  }
@@ -10922,16 +11523,40 @@ function registerAllTools(server2, options) {
10922
11523
  init_posthog();
10923
11524
  init_supabase();
10924
11525
  init_sn();
11526
+ function flushAndExit(code) {
11527
+ const done = { out: false, err: false };
11528
+ const tryExit = () => {
11529
+ if (done.out && done.err) process.exit(code);
11530
+ };
11531
+ if (process.stdout.writableFinished) {
11532
+ done.out = true;
11533
+ } else {
11534
+ process.stdout.end(() => {
11535
+ done.out = true;
11536
+ tryExit();
11537
+ });
11538
+ }
11539
+ if (process.stderr.writableFinished) {
11540
+ done.err = true;
11541
+ } else {
11542
+ process.stderr.end(() => {
11543
+ done.err = true;
11544
+ tryExit();
11545
+ });
11546
+ }
11547
+ tryExit();
11548
+ setTimeout(() => process.exit(code), 2e3).unref();
11549
+ }
10925
11550
  process.on("uncaughtException", (err) => {
10926
11551
  process.stderr.write(`MCP server error: ${err.message}
10927
11552
  `);
10928
- process.exit(1);
11553
+ flushAndExit(1);
10929
11554
  });
10930
11555
  process.on("unhandledRejection", (reason) => {
10931
11556
  const message = reason instanceof Error ? reason.message : String(reason);
10932
11557
  process.stderr.write(`MCP server error: ${message}
10933
11558
  `);
10934
- process.exit(1);
11559
+ flushAndExit(1);
10935
11560
  });
10936
11561
  var command = process.argv[2];
10937
11562
  if (command === "--version" || command === "-v") {
@@ -10940,7 +11565,11 @@ if (command === "--version" || command === "-v") {
10940
11565
  const { fileURLToPath } = await import("node:url");
10941
11566
  let version = MCP_VERSION;
10942
11567
  try {
10943
- const pkgPath = resolve3(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
11568
+ const pkgPath = resolve3(
11569
+ dirname(fileURLToPath(import.meta.url)),
11570
+ "..",
11571
+ "package.json"
11572
+ );
10944
11573
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
10945
11574
  version = pkg.version;
10946
11575
  } catch {
@@ -10972,7 +11601,11 @@ if (command === "--help" || command === "-h") {
10972
11601
  ok: true,
10973
11602
  command: "help",
10974
11603
  commands: [
10975
- { name: "setup", aliases: ["login"], description: "Interactive OAuth setup" },
11604
+ {
11605
+ name: "setup",
11606
+ aliases: ["login"],
11607
+ description: "Interactive OAuth setup"
11608
+ },
10976
11609
  { name: "logout", description: "Remove credentials" },
10977
11610
  { name: "whoami", description: "Show auth info" },
10978
11611
  { name: "health", description: "Check connectivity" },
@@ -11061,10 +11694,14 @@ if (command === "sn") {
11061
11694
  await runSnCli(process.argv.slice(3));
11062
11695
  process.exit(0);
11063
11696
  }
11064
- if (command && !["setup", "login", "logout", "whoami", "health", "sn", "repl"].includes(command)) {
11065
- process.stderr.write(`Unknown command: ${command}
11697
+ if (command && !["setup", "login", "logout", "whoami", "health", "sn", "repl"].includes(
11698
+ command
11699
+ )) {
11700
+ process.stderr.write(
11701
+ `Unknown command: ${command}
11066
11702
  Run socialneuron-mcp --help for usage.
11067
- `);
11703
+ `
11704
+ );
11068
11705
  process.exit(1);
11069
11706
  }
11070
11707
  await initializeAuth();