@socialneuron/mcp-server 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.5.0";
17
+ MCP_VERSION = "1.5.2";
18
18
  }
19
19
  });
20
20
 
@@ -3895,6 +3895,41 @@ function checkRateLimit(category, key) {
3895
3895
 
3896
3896
  // src/tools/ideation.ts
3897
3897
  init_supabase();
3898
+
3899
+ // src/lib/tool-errors.ts
3900
+ function formatToolError(rawMessage) {
3901
+ const msg = rawMessage.toLowerCase();
3902
+ if (msg.includes("rate limit") || msg.includes("too many requests")) {
3903
+ return `${rawMessage} Reduce request frequency or wait before retrying.`;
3904
+ }
3905
+ if (msg.includes("insufficient credit") || msg.includes("budget") || msg.includes("spending cap")) {
3906
+ return `${rawMessage} Call get_credit_balance to check remaining credits. Consider a cheaper model or wait for monthly refresh.`;
3907
+ }
3908
+ if (msg.includes("oauth") || msg.includes("token expired") || msg.includes("not connected") || msg.includes("reconnect")) {
3909
+ return `${rawMessage} Call list_connected_accounts to check status. User may need to reconnect at socialneuron.com/settings/connections.`;
3910
+ }
3911
+ if (msg.includes("generation failed") || msg.includes("failed to start") || msg.includes("no job id") || msg.includes("could not be parsed")) {
3912
+ return `${rawMessage} Try simplifying the prompt, using a different model, or check credits with get_credit_balance.`;
3913
+ }
3914
+ if (msg.includes("not found") || msg.includes("no ") && msg.includes(" found")) {
3915
+ return `${rawMessage} Verify the ID is correct \u2014 use the corresponding list tool to find valid IDs.`;
3916
+ }
3917
+ if (msg.includes("not accessible") || msg.includes("unauthorized") || msg.includes("permission")) {
3918
+ return `${rawMessage} Check API key scopes with get_credit_balance. A higher-tier plan may be required.`;
3919
+ }
3920
+ if (msg.includes("ssrf") || msg.includes("url blocked")) {
3921
+ return `${rawMessage} The URL was blocked for security. Use a publicly accessible HTTPS URL.`;
3922
+ }
3923
+ if (msg.includes("failed to schedule") || msg.includes("scheduling failed")) {
3924
+ return `${rawMessage} Verify platform OAuth is active with list_connected_accounts, then retry.`;
3925
+ }
3926
+ if (msg.includes("no posts") || msg.includes("plan") && msg.includes("has no")) {
3927
+ return `${rawMessage} Generate a plan with plan_content_week first, then save with save_content_plan.`;
3928
+ }
3929
+ return rawMessage;
3930
+ }
3931
+
3932
+ // src/tools/ideation.ts
3898
3933
  function registerIdeationTools(server2) {
3899
3934
  server2.tool(
3900
3935
  "generate_content",
@@ -3928,6 +3963,13 @@ function registerIdeationTools(server2) {
3928
3963
  "Project ID to auto-load brand profile and performance context for prompt enrichment."
3929
3964
  )
3930
3965
  },
3966
+ {
3967
+ title: "Generate Content",
3968
+ readOnlyHint: false,
3969
+ destructiveHint: false,
3970
+ idempotentHint: false,
3971
+ openWorldHint: true
3972
+ },
3931
3973
  async ({
3932
3974
  prompt: prompt2,
3933
3975
  content_type,
@@ -4061,7 +4103,7 @@ Content Type: ${content_type}`;
4061
4103
  content: [
4062
4104
  {
4063
4105
  type: "text",
4064
- text: `Content generation failed: ${error}`
4106
+ text: formatToolError(`Content generation failed: ${error}`)
4065
4107
  }
4066
4108
  ],
4067
4109
  isError: true
@@ -4091,6 +4133,13 @@ Content Type: ${content_type}`;
4091
4133
  ),
4092
4134
  force_refresh: z.boolean().optional().describe("Skip the server-side cache and fetch fresh data.")
4093
4135
  },
4136
+ {
4137
+ title: "Fetch Trends",
4138
+ readOnlyHint: true,
4139
+ destructiveHint: false,
4140
+ idempotentHint: false,
4141
+ openWorldHint: true
4142
+ },
4094
4143
  async ({ source, category, niche, url, force_refresh }) => {
4095
4144
  if ((source === "rss" || source === "url") && !url) {
4096
4145
  return {
@@ -4119,7 +4168,7 @@ Content Type: ${content_type}`;
4119
4168
  content: [
4120
4169
  {
4121
4170
  type: "text",
4122
- text: `Failed to fetch trends: ${error}`
4171
+ text: formatToolError(`Failed to fetch trends: ${error}`)
4123
4172
  }
4124
4173
  ],
4125
4174
  isError: true
@@ -4195,6 +4244,13 @@ Content Type: ${content_type}`;
4195
4244
  "Optional project ID to load platform voice overrides from brand profile."
4196
4245
  )
4197
4246
  },
4247
+ {
4248
+ title: "Adapt Content",
4249
+ readOnlyHint: false,
4250
+ destructiveHint: false,
4251
+ idempotentHint: false,
4252
+ openWorldHint: true
4253
+ },
4198
4254
  async ({
4199
4255
  content,
4200
4256
  source_platform,
@@ -4280,7 +4336,7 @@ ${content}`,
4280
4336
  content: [
4281
4337
  {
4282
4338
  type: "text",
4283
- text: `Content adaptation failed: ${error}`
4339
+ text: formatToolError(`Content adaptation failed: ${error}`)
4284
4340
  }
4285
4341
  ],
4286
4342
  isError: true
@@ -4493,6 +4549,13 @@ function registerContentTools(server2) {
4493
4549
  ),
4494
4550
  response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
4495
4551
  },
4552
+ {
4553
+ title: "Generate Video",
4554
+ readOnlyHint: false,
4555
+ destructiveHint: false,
4556
+ idempotentHint: false,
4557
+ openWorldHint: true
4558
+ },
4496
4559
  async ({
4497
4560
  prompt: prompt2,
4498
4561
  model,
@@ -4584,7 +4647,7 @@ function registerContentTools(server2) {
4584
4647
  content: [
4585
4648
  {
4586
4649
  type: "text",
4587
- text: `Video generation failed to start: ${error}`
4650
+ text: formatToolError(`Video generation failed to start: ${error}`)
4588
4651
  }
4589
4652
  ],
4590
4653
  isError: true
@@ -4601,7 +4664,7 @@ function registerContentTools(server2) {
4601
4664
  content: [
4602
4665
  {
4603
4666
  type: "text",
4604
- text: "Video generation failed: no job ID returned."
4667
+ text: formatToolError("Video generation failed: no job ID returned.")
4605
4668
  }
4606
4669
  ],
4607
4670
  isError: true
@@ -4691,6 +4754,13 @@ function registerContentTools(server2) {
4691
4754
  ),
4692
4755
  response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
4693
4756
  },
4757
+ {
4758
+ title: "Generate Image",
4759
+ readOnlyHint: false,
4760
+ destructiveHint: false,
4761
+ idempotentHint: false,
4762
+ openWorldHint: true
4763
+ },
4694
4764
  async ({ prompt: prompt2, model, aspect_ratio, image_url, response_format }) => {
4695
4765
  const format = response_format ?? "text";
4696
4766
  const startedAt = Date.now();
@@ -4770,7 +4840,7 @@ function registerContentTools(server2) {
4770
4840
  content: [
4771
4841
  {
4772
4842
  type: "text",
4773
- text: `Image generation failed to start: ${error}`
4843
+ text: formatToolError(`Image generation failed to start: ${error}`)
4774
4844
  }
4775
4845
  ],
4776
4846
  isError: true
@@ -4787,7 +4857,7 @@ function registerContentTools(server2) {
4787
4857
  content: [
4788
4858
  {
4789
4859
  type: "text",
4790
- text: "Image generation failed: no job ID returned."
4860
+ text: formatToolError("Image generation failed: no job ID returned.")
4791
4861
  }
4792
4862
  ],
4793
4863
  isError: true
@@ -4854,6 +4924,13 @@ function registerContentTools(server2) {
4854
4924
  ),
4855
4925
  response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
4856
4926
  },
4927
+ {
4928
+ title: "Check Job Status",
4929
+ readOnlyHint: true,
4930
+ destructiveHint: false,
4931
+ idempotentHint: true,
4932
+ openWorldHint: true
4933
+ },
4857
4934
  async ({ job_id, response_format }) => {
4858
4935
  const format = response_format ?? "text";
4859
4936
  const startedAt = Date.now();
@@ -4904,7 +4981,7 @@ function registerContentTools(server2) {
4904
4981
  content: [
4905
4982
  {
4906
4983
  type: "text",
4907
- text: `Failed to look up job: ${sanitizeDbError(jobError)}`
4984
+ text: formatToolError(`Failed to look up job: ${sanitizeDbError(jobError)}`)
4908
4985
  }
4909
4986
  ],
4910
4987
  isError: true
@@ -4921,7 +4998,7 @@ function registerContentTools(server2) {
4921
4998
  content: [
4922
4999
  {
4923
5000
  type: "text",
4924
- text: `No job found with ID "${job_id}". The ID may be incorrect or the job has expired.`
5001
+ text: formatToolError(`No job found with ID "${job_id}". The ID may be incorrect or the job has expired.`)
4925
5002
  }
4926
5003
  ],
4927
5004
  isError: true
@@ -5051,6 +5128,13 @@ function registerContentTools(server2) {
5051
5128
  "Response format. Defaults to json for structured storyboard data."
5052
5129
  )
5053
5130
  },
5131
+ {
5132
+ title: "Create Storyboard",
5133
+ readOnlyHint: false,
5134
+ destructiveHint: false,
5135
+ idempotentHint: false,
5136
+ openWorldHint: true
5137
+ },
5054
5138
  async ({
5055
5139
  concept,
5056
5140
  brand_context,
@@ -5162,7 +5246,7 @@ Return ONLY valid JSON in this exact format:
5162
5246
  content: [
5163
5247
  {
5164
5248
  type: "text",
5165
- text: `Storyboard generation failed: ${error}`
5249
+ text: formatToolError(`Storyboard generation failed: ${error}`)
5166
5250
  }
5167
5251
  ],
5168
5252
  isError: true
@@ -5231,6 +5315,13 @@ Return ONLY valid JSON in this exact format:
5231
5315
  speed: z2.number().min(0.5).max(2).optional().describe("Speech speed multiplier. 1.0 is normal. Defaults to 1.0."),
5232
5316
  response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
5233
5317
  },
5318
+ {
5319
+ title: "Generate Voiceover",
5320
+ readOnlyHint: false,
5321
+ destructiveHint: false,
5322
+ idempotentHint: false,
5323
+ openWorldHint: true
5324
+ },
5234
5325
  async ({ text, voice, speed, response_format }) => {
5235
5326
  const format = response_format ?? "text";
5236
5327
  const startedAt = Date.now();
@@ -5290,7 +5381,7 @@ Return ONLY valid JSON in this exact format:
5290
5381
  content: [
5291
5382
  {
5292
5383
  type: "text",
5293
- text: `Voiceover generation failed: ${error}`
5384
+ text: formatToolError(`Voiceover generation failed: ${error}`)
5294
5385
  }
5295
5386
  ],
5296
5387
  isError: true
@@ -5307,7 +5398,7 @@ Return ONLY valid JSON in this exact format:
5307
5398
  content: [
5308
5399
  {
5309
5400
  type: "text",
5310
- text: "Voiceover generation failed: no audio URL returned."
5401
+ text: formatToolError("Voiceover generation failed: no audio URL returned.")
5311
5402
  }
5312
5403
  ],
5313
5404
  isError: true
@@ -5389,6 +5480,13 @@ Return ONLY valid JSON in this exact format:
5389
5480
  project_id: z2.string().optional().describe("Project ID to associate the carousel with."),
5390
5481
  response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to json.")
5391
5482
  },
5483
+ {
5484
+ title: "Generate Carousel",
5485
+ readOnlyHint: false,
5486
+ destructiveHint: false,
5487
+ idempotentHint: false,
5488
+ openWorldHint: true
5489
+ },
5392
5490
  async ({
5393
5491
  topic,
5394
5492
  template_id,
@@ -5463,7 +5561,7 @@ Return ONLY valid JSON in this exact format:
5463
5561
  content: [
5464
5562
  {
5465
5563
  type: "text",
5466
- text: `Carousel generation failed: ${error}`
5564
+ text: formatToolError(`Carousel generation failed: ${error}`)
5467
5565
  }
5468
5566
  ],
5469
5567
  isError: true
@@ -5602,6 +5700,13 @@ function registerDistributionTools(server2) {
5602
5700
  'If true, appends "Created with Social Neuron" to the caption. Default: false.'
5603
5701
  )
5604
5702
  },
5703
+ {
5704
+ title: "Schedule Post",
5705
+ readOnlyHint: false,
5706
+ destructiveHint: false,
5707
+ idempotentHint: false,
5708
+ openWorldHint: true
5709
+ },
5605
5710
  async ({
5606
5711
  media_url,
5607
5712
  media_urls,
@@ -5682,7 +5787,7 @@ Created with Social Neuron`;
5682
5787
  content: [
5683
5788
  {
5684
5789
  type: "text",
5685
- text: `Failed to schedule post: ${error}`
5790
+ text: formatToolError(`Failed to schedule post: ${error}`)
5686
5791
  }
5687
5792
  ],
5688
5793
  isError: true
@@ -5753,6 +5858,13 @@ Created with Social Neuron`;
5753
5858
  {
5754
5859
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
5755
5860
  },
5861
+ {
5862
+ title: "List Connected Accounts",
5863
+ readOnlyHint: true,
5864
+ destructiveHint: false,
5865
+ idempotentHint: true,
5866
+ openWorldHint: false
5867
+ },
5756
5868
  async ({ response_format }) => {
5757
5869
  const format = response_format ?? "text";
5758
5870
  const supabase = getSupabaseClient();
@@ -5763,7 +5875,7 @@ Created with Social Neuron`;
5763
5875
  content: [
5764
5876
  {
5765
5877
  type: "text",
5766
- text: `Failed to list connected accounts: ${sanitizeDbError(error)}`
5878
+ text: formatToolError(`Failed to list connected accounts: ${sanitizeDbError(error)}`)
5767
5879
  }
5768
5880
  ],
5769
5881
  isError: true
@@ -5831,6 +5943,13 @@ Created with Social Neuron`;
5831
5943
  limit: z3.number().min(1).max(50).optional().describe("Maximum number of posts to return. Defaults to 20."),
5832
5944
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
5833
5945
  },
5946
+ {
5947
+ title: "List Recent Posts",
5948
+ readOnlyHint: true,
5949
+ destructiveHint: false,
5950
+ idempotentHint: true,
5951
+ openWorldHint: false
5952
+ },
5834
5953
  async ({ platform: platform3, status, days, limit, response_format }) => {
5835
5954
  const format = response_format ?? "text";
5836
5955
  const supabase = getSupabaseClient();
@@ -5855,7 +5974,7 @@ Created with Social Neuron`;
5855
5974
  content: [
5856
5975
  {
5857
5976
  type: "text",
5858
- text: `Failed to list posts: ${sanitizeDbError(error)}`
5977
+ text: formatToolError(`Failed to list posts: ${sanitizeDbError(error)}`)
5859
5978
  }
5860
5979
  ],
5861
5980
  isError: true
@@ -5930,7 +6049,7 @@ Created with Social Neuron`;
5930
6049
  };
5931
6050
  server2.tool(
5932
6051
  "find_next_slots",
5933
- "Find optimal posting time slots based on best posting times and existing schedule. Returns non-conflicting slots sorted by engagement score.",
6052
+ "Find the next available posting time slots that avoid conflicts with already-scheduled posts. Uses engagement data from get_best_posting_times to rank slots. Call this before schedule_content_plan to pick optimal, non-overlapping times for each post.",
5934
6053
  {
5935
6054
  platforms: z3.array(
5936
6055
  z3.enum([
@@ -5943,11 +6062,18 @@ Created with Social Neuron`;
5943
6062
  "threads",
5944
6063
  "bluesky"
5945
6064
  ])
5946
- ).min(1),
6065
+ ).min(1).describe("Platforms to find posting slots for."),
5947
6066
  count: z3.number().min(1).max(20).default(7).describe("Number of slots to find"),
5948
6067
  start_after: z3.string().optional().describe("ISO datetime, defaults to now"),
5949
6068
  min_gap_hours: z3.number().min(1).max(24).default(4).describe("Minimum gap between posts on same platform"),
5950
- response_format: z3.enum(["text", "json"]).default("text")
6069
+ response_format: z3.enum(["text", "json"]).default("text").describe("Response format. Defaults to text.")
6070
+ },
6071
+ {
6072
+ title: "Find Next Posting Slots",
6073
+ readOnlyHint: true,
6074
+ destructiveHint: false,
6075
+ idempotentHint: true,
6076
+ openWorldHint: false
5951
6077
  },
5952
6078
  async ({
5953
6079
  platforms,
@@ -6053,7 +6179,7 @@ Created with Social Neuron`;
6053
6179
  });
6054
6180
  return {
6055
6181
  content: [
6056
- { type: "text", text: `Failed to find slots: ${message}` }
6182
+ { type: "text", text: formatToolError(`Failed to find slots: ${message}`) }
6057
6183
  ],
6058
6184
  isError: true
6059
6185
  };
@@ -6067,20 +6193,20 @@ Created with Social Neuron`;
6067
6193
  plan: z3.object({
6068
6194
  posts: z3.array(
6069
6195
  z3.object({
6070
- id: z3.string(),
6071
- caption: z3.string(),
6072
- platform: z3.string(),
6073
- title: z3.string().optional(),
6074
- media_url: z3.string().optional(),
6075
- schedule_at: z3.string().optional(),
6076
- hashtags: z3.array(z3.string()).optional()
6196
+ id: z3.string().describe("Unique post identifier from the content plan."),
6197
+ caption: z3.string().describe("Post caption/body text."),
6198
+ platform: z3.string().describe("Target platform name (e.g. instagram, youtube)."),
6199
+ title: z3.string().optional().describe("Post title, required for YouTube."),
6200
+ media_url: z3.string().optional().describe("Public or R2 signed URL for the post media."),
6201
+ schedule_at: z3.string().optional().describe("ISO 8601 UTC datetime to publish (e.g. 2026-03-20T14:00:00Z)."),
6202
+ hashtags: z3.array(z3.string()).optional().describe("Hashtags to append to the caption.")
6077
6203
  })
6078
6204
  )
6079
- }).passthrough().optional(),
6205
+ }).passthrough().optional().describe("Inline content plan object with a posts array. Provide this or plan_id."),
6080
6206
  plan_id: z3.string().uuid().optional().describe("Persisted content plan ID from content_plans table"),
6081
6207
  auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
6082
6208
  dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
6083
- response_format: z3.enum(["text", "json"]).default("text"),
6209
+ response_format: z3.enum(["text", "json"]).default("text").describe("Response format. Defaults to text."),
6084
6210
  enforce_quality: z3.boolean().default(true).describe(
6085
6211
  "When true, block scheduling for posts that fail quality checks."
6086
6212
  ),
@@ -6090,6 +6216,13 @@ Created with Social Neuron`;
6090
6216
  batch_size: z3.number().int().min(1).max(10).default(4).describe("Concurrent schedule calls per platform batch."),
6091
6217
  idempotency_seed: z3.string().max(128).optional().describe("Optional stable seed used when building idempotency keys.")
6092
6218
  },
6219
+ {
6220
+ title: "Schedule Content Plan",
6221
+ readOnlyHint: false,
6222
+ destructiveHint: false,
6223
+ idempotentHint: false,
6224
+ openWorldHint: true
6225
+ },
6093
6226
  async ({
6094
6227
  plan,
6095
6228
  plan_id,
@@ -6116,7 +6249,7 @@ Created with Social Neuron`;
6116
6249
  content: [
6117
6250
  {
6118
6251
  type: "text",
6119
- text: `Failed to load content plan: ${sanitizeDbError(storedError)}`
6252
+ text: formatToolError(`Failed to load content plan: ${sanitizeDbError(storedError)}`)
6120
6253
  }
6121
6254
  ],
6122
6255
  isError: true
@@ -6127,7 +6260,7 @@ Created with Social Neuron`;
6127
6260
  content: [
6128
6261
  {
6129
6262
  type: "text",
6130
- text: `No content plan found for plan_id=${plan_id}`
6263
+ text: formatToolError(`No content plan found for plan_id=${plan_id}`)
6131
6264
  }
6132
6265
  ],
6133
6266
  isError: true
@@ -6142,7 +6275,7 @@ Created with Social Neuron`;
6142
6275
  content: [
6143
6276
  {
6144
6277
  type: "text",
6145
- text: `Stored plan ${plan_id} has no posts array.`
6278
+ text: formatToolError(`Stored plan ${plan_id} has no posts array.`)
6146
6279
  }
6147
6280
  ],
6148
6281
  isError: true
@@ -6181,7 +6314,7 @@ Created with Social Neuron`;
6181
6314
  content: [
6182
6315
  {
6183
6316
  type: "text",
6184
- text: `Failed to load plan approvals: ${sanitizeDbError(approvalsError)}`
6317
+ text: formatToolError(`Failed to load plan approvals: ${sanitizeDbError(approvalsError)}`)
6185
6318
  }
6186
6319
  ],
6187
6320
  isError: true
@@ -6616,7 +6749,7 @@ Created with Social Neuron`;
6616
6749
  content: [
6617
6750
  {
6618
6751
  type: "text",
6619
- text: `Batch scheduling failed: ${message}`
6752
+ text: formatToolError(`Batch scheduling failed: ${message}`)
6620
6753
  }
6621
6754
  ],
6622
6755
  isError: true
@@ -6662,6 +6795,7 @@ function registerAnalyticsTools(server2) {
6662
6795
  limit: z4.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
6663
6796
  response_format: z4.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6664
6797
  },
6798
+ { title: "Fetch Analytics", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
6665
6799
  async ({ platform: platform3, days, content_id, limit, response_format }) => {
6666
6800
  const format = response_format ?? "text";
6667
6801
  const supabase = getSupabaseClient();
@@ -6708,7 +6842,7 @@ function registerAnalyticsTools(server2) {
6708
6842
  content: [
6709
6843
  {
6710
6844
  type: "text",
6711
- text: `Failed to fetch user-scoped posts: ${sanitizeDbError(postsError)}`
6845
+ text: formatToolError(`Failed to fetch user-scoped posts: ${sanitizeDbError(postsError)}`)
6712
6846
  }
6713
6847
  ],
6714
6848
  isError: true
@@ -6755,7 +6889,7 @@ function registerAnalyticsTools(server2) {
6755
6889
  content: [
6756
6890
  {
6757
6891
  type: "text",
6758
- text: `Failed to fetch analytics: ${sanitizeDbError(simpleError)}`
6892
+ text: formatToolError(`Failed to fetch analytics: ${sanitizeDbError(simpleError)}`)
6759
6893
  }
6760
6894
  ],
6761
6895
  isError: true
@@ -6839,6 +6973,13 @@ function registerAnalyticsTools(server2) {
6839
6973
  {
6840
6974
  response_format: z4.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6841
6975
  },
6976
+ {
6977
+ title: "Refresh Platform Analytics",
6978
+ readOnlyHint: false,
6979
+ destructiveHint: false,
6980
+ idempotentHint: false,
6981
+ openWorldHint: true
6982
+ },
6842
6983
  async ({ response_format }) => {
6843
6984
  const format = response_format ?? "text";
6844
6985
  const startedAt = Date.now();
@@ -6878,7 +7019,7 @@ function registerAnalyticsTools(server2) {
6878
7019
  content: [
6879
7020
  {
6880
7021
  type: "text",
6881
- text: `Error refreshing analytics: ${error}`
7022
+ text: formatToolError(`Error refreshing analytics: ${error}`)
6882
7023
  }
6883
7024
  ],
6884
7025
  isError: true
@@ -7240,6 +7381,13 @@ function registerBrandTools(server2) {
7240
7381
  ),
7241
7382
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7242
7383
  },
7384
+ {
7385
+ title: "Extract Brand",
7386
+ readOnlyHint: true,
7387
+ destructiveHint: false,
7388
+ idempotentHint: true,
7389
+ openWorldHint: true
7390
+ },
7243
7391
  async ({ url, response_format }) => {
7244
7392
  const ssrfCheck = await validateUrlForSSRF(url);
7245
7393
  if (!ssrfCheck.isValid) {
@@ -7260,7 +7408,7 @@ function registerBrandTools(server2) {
7260
7408
  content: [
7261
7409
  {
7262
7410
  type: "text",
7263
- text: `Brand extraction failed: ${error}`
7411
+ text: formatToolError(`Brand extraction failed: ${error}`)
7264
7412
  }
7265
7413
  ],
7266
7414
  isError: true
@@ -7323,6 +7471,13 @@ function registerBrandTools(server2) {
7323
7471
  project_id: z5.string().uuid().optional().describe("Project ID. Defaults to active project context."),
7324
7472
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7325
7473
  },
7474
+ {
7475
+ title: "Get Brand Profile",
7476
+ readOnlyHint: true,
7477
+ destructiveHint: false,
7478
+ idempotentHint: true,
7479
+ openWorldHint: false
7480
+ },
7326
7481
  async ({ project_id, response_format }) => {
7327
7482
  const supabase = getSupabaseClient();
7328
7483
  const userId = await getDefaultUserId();
@@ -7363,7 +7518,7 @@ function registerBrandTools(server2) {
7363
7518
  content: [
7364
7519
  {
7365
7520
  type: "text",
7366
- text: `Failed to load brand profile: ${sanitizeDbError(error)}`
7521
+ text: formatToolError(`Failed to load brand profile: ${sanitizeDbError(error)}`)
7367
7522
  }
7368
7523
  ],
7369
7524
  isError: true
@@ -7420,8 +7575,17 @@ function registerBrandTools(server2) {
7420
7575
  "product_showcase"
7421
7576
  ]).optional().describe("Extraction method metadata."),
7422
7577
  overall_confidence: z5.number().min(0).max(1).optional().describe("Optional overall confidence score in range 0..1."),
7423
- extraction_metadata: z5.record(z5.string(), z5.unknown()).optional(),
7424
- response_format: z5.enum(["text", "json"]).optional()
7578
+ extraction_metadata: z5.record(z5.string(), z5.unknown()).optional().describe(
7579
+ "Arbitrary key-value metadata about the extraction process."
7580
+ ),
7581
+ response_format: z5.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
7582
+ },
7583
+ {
7584
+ title: "Save Brand Profile",
7585
+ readOnlyHint: false,
7586
+ destructiveHint: false,
7587
+ idempotentHint: false,
7588
+ openWorldHint: false
7425
7589
  },
7426
7590
  async ({
7427
7591
  project_id,
@@ -7485,7 +7649,7 @@ function registerBrandTools(server2) {
7485
7649
  content: [
7486
7650
  {
7487
7651
  type: "text",
7488
- text: `Failed to save brand profile: ${sanitizeDbError(error)}`
7652
+ text: formatToolError(`Failed to save brand profile: ${sanitizeDbError(error)}`)
7489
7653
  }
7490
7654
  ],
7491
7655
  isError: true
@@ -7534,15 +7698,32 @@ Version: ${payload.version ?? "N/A"}`
7534
7698
  "facebook",
7535
7699
  "threads",
7536
7700
  "bluesky"
7537
- ]),
7701
+ ]).describe("Social platform to set voice overrides for."),
7538
7702
  project_id: z5.string().uuid().optional().describe("Project ID. Defaults to active project context."),
7539
7703
  samples: z5.string().max(3e3).optional().describe("3-5 real platform post examples for style anchoring."),
7540
- tone: z5.array(z5.string()).optional(),
7541
- style: z5.array(z5.string()).optional(),
7542
- avoid_patterns: z5.array(z5.string()).optional(),
7543
- hashtag_strategy: z5.string().max(300).optional(),
7544
- cta_style: z5.string().max(300).optional(),
7545
- response_format: z5.enum(["text", "json"]).optional()
7704
+ tone: z5.array(z5.string()).optional().describe(
7705
+ 'Tone descriptors for this platform (e.g. ["casual", "witty", "informative"]).'
7706
+ ),
7707
+ style: z5.array(z5.string()).optional().describe(
7708
+ 'Writing style tags (e.g. ["short-form", "emoji-heavy", "storytelling"]).'
7709
+ ),
7710
+ avoid_patterns: z5.array(z5.string()).optional().describe(
7711
+ 'Phrases or patterns the brand should never use on this platform (e.g. ["click here", "buy now"]).'
7712
+ ),
7713
+ hashtag_strategy: z5.string().max(300).optional().describe(
7714
+ 'Hashtag usage guidelines for this platform (e.g. "3-5 niche hashtags, no generic tags").'
7715
+ ),
7716
+ cta_style: z5.string().max(300).optional().describe(
7717
+ 'Preferred call-to-action style (e.g. "soft CTA with question" or "direct link in bio").'
7718
+ ),
7719
+ response_format: z5.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
7720
+ },
7721
+ {
7722
+ title: "Update Platform Voice",
7723
+ readOnlyHint: false,
7724
+ destructiveHint: false,
7725
+ idempotentHint: false,
7726
+ openWorldHint: false
7546
7727
  },
7547
7728
  async ({
7548
7729
  platform: platform3,
@@ -7644,7 +7825,7 @@ Version: ${payload.version ?? "N/A"}`
7644
7825
  content: [
7645
7826
  {
7646
7827
  type: "text",
7647
- text: `Failed to update platform voice: ${saveError.message}`
7828
+ text: formatToolError(`Failed to update platform voice: ${saveError.message}`)
7648
7829
  }
7649
7830
  ],
7650
7831
  isError: true
@@ -7791,6 +7972,13 @@ function registerScreenshotTools(server2) {
7791
7972
  "Extra milliseconds to wait after page load before capturing. Useful for animations. Defaults to 2000."
7792
7973
  )
7793
7974
  },
7975
+ {
7976
+ title: "Capture App Page",
7977
+ readOnlyHint: true,
7978
+ destructiveHint: false,
7979
+ idempotentHint: false,
7980
+ openWorldHint: false
7981
+ },
7794
7982
  async ({ page: pageName, viewport, theme, selector, wait_ms }) => {
7795
7983
  const startedAt = Date.now();
7796
7984
  let rateLimitKey = "anonymous";
@@ -7911,6 +8099,13 @@ function registerScreenshotTools(server2) {
7911
8099
  ),
7912
8100
  wait_ms: z6.number().min(0).max(3e4).optional().describe("Extra milliseconds to wait after page load before capturing. Defaults to 1000.")
7913
8101
  },
8102
+ {
8103
+ title: "Capture Screenshot",
8104
+ readOnlyHint: true,
8105
+ destructiveHint: false,
8106
+ idempotentHint: false,
8107
+ openWorldHint: true
8108
+ },
7914
8109
  async ({ url, viewport, selector, output_path, wait_ms }) => {
7915
8110
  const startedAt = Date.now();
7916
8111
  let rateLimitKey = "anonymous";
@@ -8173,6 +8368,13 @@ function registerRemotionTools(server2) {
8173
8368
  "list_compositions",
8174
8369
  "List all available Remotion video compositions defined in Social Neuron. Returns composition IDs, dimensions, duration, and descriptions. Use this to discover what videos can be rendered with render_demo_video.",
8175
8370
  {},
8371
+ {
8372
+ title: "List Compositions",
8373
+ readOnlyHint: true,
8374
+ destructiveHint: false,
8375
+ idempotentHint: true,
8376
+ openWorldHint: false
8377
+ },
8176
8378
  async () => {
8177
8379
  const lines = [`${COMPOSITIONS.length} Remotion compositions available:`, ""];
8178
8380
  for (const comp of COMPOSITIONS) {
@@ -8201,6 +8403,13 @@ function registerRemotionTools(server2) {
8201
8403
  "JSON string of input props to pass to the composition. Each composition accepts different props. Omit for defaults."
8202
8404
  )
8203
8405
  },
8406
+ {
8407
+ title: "Render Demo Video",
8408
+ readOnlyHint: false,
8409
+ destructiveHint: false,
8410
+ idempotentHint: false,
8411
+ openWorldHint: false
8412
+ },
8204
8413
  async ({ composition_id, output_format, props }) => {
8205
8414
  const startedAt = Date.now();
8206
8415
  const userId = await getDefaultUserId();
@@ -8382,6 +8591,13 @@ function registerInsightsTools(server2) {
8382
8591
  limit: z8.number().min(1).max(50).optional().describe("Maximum number of insights to return. Defaults to 10."),
8383
8592
  response_format: z8.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8384
8593
  },
8594
+ {
8595
+ title: "Get Performance Insights",
8596
+ readOnlyHint: true,
8597
+ destructiveHint: false,
8598
+ idempotentHint: true,
8599
+ openWorldHint: false
8600
+ },
8385
8601
  async ({ insight_type, days, limit, response_format }) => {
8386
8602
  const format = response_format ?? "text";
8387
8603
  const supabase = getSupabaseClient();
@@ -8509,6 +8725,13 @@ function registerInsightsTools(server2) {
8509
8725
  days: z8.number().min(1).max(90).optional().describe("Number of days to analyze. Defaults to 30. Max 90."),
8510
8726
  response_format: z8.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8511
8727
  },
8728
+ {
8729
+ title: "Get Best Posting Times",
8730
+ readOnlyHint: true,
8731
+ destructiveHint: false,
8732
+ idempotentHint: true,
8733
+ openWorldHint: false
8734
+ },
8512
8735
  async ({ platform: platform3, days, response_format }) => {
8513
8736
  const format = response_format ?? "text";
8514
8737
  const supabase = getSupabaseClient();
@@ -8658,6 +8881,13 @@ function registerYouTubeAnalyticsTools(server2) {
8658
8881
  video_id: z9.string().optional().describe('YouTube video ID. Required when action is "video".'),
8659
8882
  max_results: z9.number().min(1).max(50).optional().describe('Max videos to return for "topVideos" action. Defaults to 10.')
8660
8883
  },
8884
+ {
8885
+ title: "Fetch YouTube Analytics",
8886
+ readOnlyHint: true,
8887
+ destructiveHint: false,
8888
+ idempotentHint: true,
8889
+ openWorldHint: true
8890
+ },
8661
8891
  async ({ action, start_date, end_date, video_id, max_results }) => {
8662
8892
  if (action === "video" && !video_id) {
8663
8893
  return {
@@ -8778,6 +9008,13 @@ function registerCommentsTools(server2) {
8778
9008
  page_token: z10.string().optional().describe("Pagination cursor from previous list_comments response nextPageToken field. Omit for first page of results."),
8779
9009
  response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8780
9010
  },
9011
+ {
9012
+ title: "List Comments",
9013
+ readOnlyHint: true,
9014
+ destructiveHint: false,
9015
+ idempotentHint: true,
9016
+ openWorldHint: true
9017
+ },
8781
9018
  async ({ video_id, max_results, page_token, response_format }) => {
8782
9019
  const format = response_format ?? "text";
8783
9020
  const { data, error } = await callEdgeFunction("youtube-comments", {
@@ -8789,7 +9026,7 @@ function registerCommentsTools(server2) {
8789
9026
  if (error) {
8790
9027
  return {
8791
9028
  content: [
8792
- { type: "text", text: `Error listing comments: ${error}` }
9029
+ { type: "text", text: formatToolError(`Error listing comments: ${error}`) }
8793
9030
  ],
8794
9031
  isError: true
8795
9032
  };
@@ -8848,6 +9085,13 @@ function registerCommentsTools(server2) {
8848
9085
  text: z10.string().min(1).describe("The reply text."),
8849
9086
  response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8850
9087
  },
9088
+ {
9089
+ title: "Reply to Comment",
9090
+ readOnlyHint: false,
9091
+ destructiveHint: false,
9092
+ idempotentHint: false,
9093
+ openWorldHint: true
9094
+ },
8851
9095
  async ({ parent_id, text, response_format }) => {
8852
9096
  const format = response_format ?? "text";
8853
9097
  const startedAt = Date.now();
@@ -8886,7 +9130,7 @@ function registerCommentsTools(server2) {
8886
9130
  content: [
8887
9131
  {
8888
9132
  type: "text",
8889
- text: `Error replying to comment: ${error}`
9133
+ text: formatToolError(`Error replying to comment: ${error}`)
8890
9134
  }
8891
9135
  ],
8892
9136
  isError: true
@@ -8929,6 +9173,13 @@ function registerCommentsTools(server2) {
8929
9173
  text: z10.string().min(1).describe("The comment text."),
8930
9174
  response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8931
9175
  },
9176
+ {
9177
+ title: "Post Comment",
9178
+ readOnlyHint: false,
9179
+ destructiveHint: false,
9180
+ idempotentHint: false,
9181
+ openWorldHint: true
9182
+ },
8932
9183
  async ({ video_id, text, response_format }) => {
8933
9184
  const format = response_format ?? "text";
8934
9185
  const startedAt = Date.now();
@@ -8965,7 +9216,7 @@ function registerCommentsTools(server2) {
8965
9216
  });
8966
9217
  return {
8967
9218
  content: [
8968
- { type: "text", text: `Error posting comment: ${error}` }
9219
+ { type: "text", text: formatToolError(`Error posting comment: ${error}`) }
8969
9220
  ],
8970
9221
  isError: true
8971
9222
  };
@@ -9007,6 +9258,13 @@ function registerCommentsTools(server2) {
9007
9258
  moderation_status: z10.enum(["published", "rejected"]).describe('"published" to approve, "rejected" to hide.'),
9008
9259
  response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9009
9260
  },
9261
+ {
9262
+ title: "Moderate Comment",
9263
+ readOnlyHint: false,
9264
+ destructiveHint: false,
9265
+ idempotentHint: true,
9266
+ openWorldHint: true
9267
+ },
9010
9268
  async ({ comment_id, moderation_status, response_format }) => {
9011
9269
  const format = response_format ?? "text";
9012
9270
  const startedAt = Date.now();
@@ -9045,7 +9303,7 @@ function registerCommentsTools(server2) {
9045
9303
  content: [
9046
9304
  {
9047
9305
  type: "text",
9048
- text: `Error moderating comment: ${error}`
9306
+ text: formatToolError(`Error moderating comment: ${error}`)
9049
9307
  }
9050
9308
  ],
9051
9309
  isError: true
@@ -9092,6 +9350,13 @@ function registerCommentsTools(server2) {
9092
9350
  comment_id: z10.string().describe("The comment ID to delete."),
9093
9351
  response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9094
9352
  },
9353
+ {
9354
+ title: "Delete Comment",
9355
+ readOnlyHint: false,
9356
+ destructiveHint: true,
9357
+ idempotentHint: true,
9358
+ openWorldHint: true
9359
+ },
9095
9360
  async ({ comment_id, response_format }) => {
9096
9361
  const format = response_format ?? "text";
9097
9362
  const startedAt = Date.now();
@@ -9127,7 +9392,7 @@ function registerCommentsTools(server2) {
9127
9392
  });
9128
9393
  return {
9129
9394
  content: [
9130
- { type: "text", text: `Error deleting comment: ${error}` }
9395
+ { type: "text", text: formatToolError(`Error deleting comment: ${error}`) }
9131
9396
  ],
9132
9397
  isError: true
9133
9398
  };
@@ -9241,12 +9506,19 @@ function asEnvelope7(data) {
9241
9506
  function registerIdeationContextTools(server2) {
9242
9507
  server2.tool(
9243
9508
  "get_ideation_context",
9244
- "Get synthesized ideation context from performance insights. Returns the same prompt-injection context used by ideation generation.",
9509
+ "Load performance-derived context (top hooks, optimal timing, winning patterns) that should inform your next content generation. Call this before generate_content or plan_content_week to ground new content in what has actually performed well. Returns a promptInjection string ready to pass into generation tools.",
9245
9510
  {
9246
9511
  project_id: z11.string().uuid().optional().describe("Project ID to scope insights."),
9247
9512
  days: z11.number().min(1).max(90).optional().describe("Lookback window for insights. Defaults to 30 days."),
9248
9513
  response_format: z11.enum(["text", "json"]).optional().describe("Optional output format. Defaults to text.")
9249
9514
  },
9515
+ {
9516
+ title: "Get Ideation Context",
9517
+ readOnlyHint: true,
9518
+ destructiveHint: false,
9519
+ idempotentHint: true,
9520
+ openWorldHint: false
9521
+ },
9250
9522
  async ({ project_id, days, response_format }) => {
9251
9523
  const supabase = getSupabaseClient();
9252
9524
  const userId = await getDefaultUserId();
@@ -9377,6 +9649,13 @@ function registerCreditsTools(server2) {
9377
9649
  {
9378
9650
  response_format: z12.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9379
9651
  },
9652
+ {
9653
+ title: "Get Credit Balance",
9654
+ readOnlyHint: true,
9655
+ destructiveHint: false,
9656
+ idempotentHint: true,
9657
+ openWorldHint: false
9658
+ },
9380
9659
  async ({ response_format }) => {
9381
9660
  const supabase = getSupabaseClient();
9382
9661
  const userId = await getDefaultUserId();
@@ -9389,7 +9668,7 @@ function registerCreditsTools(server2) {
9389
9668
  content: [
9390
9669
  {
9391
9670
  type: "text",
9392
- text: `Failed to fetch credit balance: ${sanitizeDbError(profileResult.error)}`
9671
+ text: formatToolError(`Failed to fetch credit balance: ${sanitizeDbError(profileResult.error)}`)
9393
9672
  }
9394
9673
  ],
9395
9674
  isError: true
@@ -9430,6 +9709,13 @@ Monthly used: ${payload.monthlyUsed}` + (payload.monthlyLimit ? ` / ${payload.mo
9430
9709
  {
9431
9710
  response_format: z12.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9432
9711
  },
9712
+ {
9713
+ title: "Get Budget Status",
9714
+ readOnlyHint: true,
9715
+ destructiveHint: false,
9716
+ idempotentHint: true,
9717
+ openWorldHint: false
9718
+ },
9433
9719
  async ({ response_format }) => {
9434
9720
  const budget = getCurrentBudgetStatus();
9435
9721
  const payload = {
@@ -9484,11 +9770,18 @@ function asEnvelope9(data) {
9484
9770
  function registerLoopSummaryTools(server2) {
9485
9771
  server2.tool(
9486
9772
  "get_loop_summary",
9487
- "Get a one-call dashboard summary of the feedback loop state (brand profile, recent content, and current insights).",
9773
+ "Get a single-call health check of the content feedback loop: brand profile status, recent content, and active insights. Call at the start of a session to decide what to do next. The response includes a recommendedNextAction field that tells you which tool to call.",
9488
9774
  {
9489
9775
  project_id: z13.string().uuid().optional().describe("Project ID. Defaults to active project context."),
9490
9776
  response_format: z13.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9491
9777
  },
9778
+ {
9779
+ title: "Get Loop Summary",
9780
+ readOnlyHint: true,
9781
+ destructiveHint: false,
9782
+ idempotentHint: true,
9783
+ openWorldHint: false
9784
+ },
9492
9785
  async ({ project_id, response_format }) => {
9493
9786
  const supabase = getSupabaseClient();
9494
9787
  const userId = await getDefaultUserId();
@@ -9587,6 +9880,13 @@ function registerUsageTools(server2) {
9587
9880
  {
9588
9881
  response_format: z14.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9589
9882
  },
9883
+ {
9884
+ title: "Get MCP Usage",
9885
+ readOnlyHint: true,
9886
+ destructiveHint: false,
9887
+ idempotentHint: true,
9888
+ openWorldHint: false
9889
+ },
9590
9890
  async ({ response_format }) => {
9591
9891
  const format = response_format ?? "text";
9592
9892
  const supabase = getSupabaseClient();
@@ -9677,6 +9977,13 @@ function registerAutopilotTools(server2) {
9677
9977
  active_only: z15.boolean().optional().describe("If true, only return active configs. Defaults to false (show all)."),
9678
9978
  response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9679
9979
  },
9980
+ {
9981
+ title: "List Autopilot Configs",
9982
+ readOnlyHint: true,
9983
+ destructiveHint: false,
9984
+ idempotentHint: true,
9985
+ openWorldHint: false
9986
+ },
9680
9987
  async ({ active_only, response_format }) => {
9681
9988
  const format = response_format ?? "text";
9682
9989
  const supabase = getSupabaseClient();
@@ -9754,11 +10061,18 @@ ${"=".repeat(40)}
9754
10061
  {
9755
10062
  config_id: z15.string().uuid().describe("The autopilot config ID to update."),
9756
10063
  is_active: z15.boolean().optional().describe("Enable or disable this autopilot config."),
9757
- schedule_days: z15.array(z15.enum(["mon", "tue", "wed", "thu", "fri", "sat", "sun"])).optional().describe('Days of the week to run (e.g., ["mon", "wed", "fri"]).'),
10064
+ schedule_days: z15.array(z15.enum(["mon", "tue", "wed", "thu", "fri", "sat", "sun"]).describe("Three-letter lowercase day abbreviation.")).optional().describe('Days of the week to run (e.g. ["mon", "wed", "fri"]).'),
9758
10065
  schedule_time: z15.string().optional().describe('Time to run in HH:MM format (24h, user timezone). E.g., "09:00".'),
9759
10066
  max_credits_per_run: z15.number().optional().describe("Maximum credits per execution."),
9760
10067
  max_credits_per_week: z15.number().optional().describe("Maximum credits per week.")
9761
10068
  },
10069
+ {
10070
+ title: "Update Autopilot Config",
10071
+ readOnlyHint: false,
10072
+ destructiveHint: false,
10073
+ idempotentHint: true,
10074
+ openWorldHint: false
10075
+ },
9762
10076
  async ({
9763
10077
  config_id,
9764
10078
  is_active,
@@ -9822,6 +10136,13 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
9822
10136
  {
9823
10137
  response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9824
10138
  },
10139
+ {
10140
+ title: "Get Autopilot Status",
10141
+ readOnlyHint: true,
10142
+ destructiveHint: false,
10143
+ idempotentHint: true,
10144
+ openWorldHint: false
10145
+ },
9825
10146
  async ({ response_format }) => {
9826
10147
  const format = response_format ?? "text";
9827
10148
  const supabase = getSupabaseClient();
@@ -9943,7 +10264,14 @@ function registerExtractionTools(server2) {
9943
10264
  extract_type: z16.enum(["auto", "transcript", "article", "product"]).default("auto").describe("Type of extraction"),
9944
10265
  include_comments: z16.boolean().default(false).describe("Include top comments (YouTube only)"),
9945
10266
  max_results: z16.number().min(1).max(100).default(10).describe("Max comments to include"),
9946
- response_format: z16.enum(["text", "json"]).default("text")
10267
+ response_format: z16.enum(["text", "json"]).default("text").describe("Response format. Defaults to text.")
10268
+ },
10269
+ {
10270
+ title: "Extract URL Content",
10271
+ readOnlyHint: true,
10272
+ destructiveHint: false,
10273
+ idempotentHint: true,
10274
+ openWorldHint: true
9947
10275
  },
9948
10276
  async ({
9949
10277
  url,
@@ -9986,7 +10314,7 @@ function registerExtractionTools(server2) {
9986
10314
  content: [
9987
10315
  {
9988
10316
  type: "text",
9989
- text: `Failed to extract YouTube video: ${error ?? "No data returned"}`
10317
+ text: formatToolError(`Failed to extract YouTube video: ${error ?? "No data returned"}`)
9990
10318
  }
9991
10319
  ],
9992
10320
  isError: true
@@ -10026,7 +10354,7 @@ function registerExtractionTools(server2) {
10026
10354
  content: [
10027
10355
  {
10028
10356
  type: "text",
10029
- text: `Failed to extract YouTube channel: ${error ?? "No data returned"}`
10357
+ text: formatToolError(`Failed to extract YouTube channel: ${error ?? "No data returned"}`)
10030
10358
  }
10031
10359
  ],
10032
10360
  isError: true
@@ -10057,7 +10385,7 @@ function registerExtractionTools(server2) {
10057
10385
  content: [
10058
10386
  {
10059
10387
  type: "text",
10060
- text: `Failed to extract URL content: ${error ?? "No data returned"}`
10388
+ text: formatToolError(`Failed to extract URL content: ${error ?? "No data returned"}`)
10061
10389
  }
10062
10390
  ],
10063
10391
  isError: true
@@ -10114,7 +10442,7 @@ function registerExtractionTools(server2) {
10114
10442
  });
10115
10443
  return {
10116
10444
  content: [
10117
- { type: "text", text: `Extraction failed: ${message}` }
10445
+ { type: "text", text: formatToolError(`Extraction failed: ${message}`) }
10118
10446
  ],
10119
10447
  isError: true
10120
10448
  };
@@ -10155,9 +10483,16 @@ function registerQualityTools(server2) {
10155
10483
  ).min(1).describe("Target platforms"),
10156
10484
  threshold: z17.number().min(0).max(35).default(26).describe("Minimum total score to pass (max 35, scored across 7 categories at 0-5 each). Default 26 (~75%). Use 20 for rough drafts, 28+ for final posts going to large audiences."),
10157
10485
  brand_keyword: z17.string().optional().describe("Brand keyword for alignment check"),
10158
- brand_avoid_patterns: z17.array(z17.string()).optional(),
10159
- custom_banned_terms: z17.array(z17.string()).optional(),
10160
- response_format: z17.enum(["text", "json"]).default("text")
10486
+ brand_avoid_patterns: z17.array(z17.string()).optional().describe("Phrases the brand should never use (e.g. competitor names, off-brand slang). Matched case-insensitively."),
10487
+ custom_banned_terms: z17.array(z17.string()).optional().describe("Additional banned words beyond the built-in safety list. Useful for industry-specific compliance terms."),
10488
+ response_format: z17.enum(["text", "json"]).default("text").describe("'text' for human-readable report, 'json' for structured scores suitable for pipeline automation.")
10489
+ },
10490
+ {
10491
+ title: "Quality Check",
10492
+ readOnlyHint: true,
10493
+ destructiveHint: false,
10494
+ idempotentHint: true,
10495
+ openWorldHint: false
10161
10496
  },
10162
10497
  async ({
10163
10498
  caption,
@@ -10229,15 +10564,22 @@ function registerQualityTools(server2) {
10229
10564
  plan: z17.object({
10230
10565
  posts: z17.array(
10231
10566
  z17.object({
10232
- id: z17.string(),
10233
- caption: z17.string(),
10234
- title: z17.string().optional(),
10235
- platform: z17.string()
10567
+ id: z17.string().describe("Unique post identifier."),
10568
+ caption: z17.string().describe("Post caption/body text to quality-check."),
10569
+ title: z17.string().optional().describe("Post title (important for YouTube)."),
10570
+ platform: z17.string().describe("Target platform (e.g. instagram, youtube).")
10236
10571
  })
10237
10572
  )
10238
- }).passthrough().describe("Content plan with posts array"),
10573
+ }).passthrough().describe("Content plan with posts array."),
10239
10574
  threshold: z17.number().min(0).max(35).default(26).describe("Minimum total score to pass (max 35, scored across 7 categories at 0-5 each). Default 26 (~75%). Use 20 for rough drafts, 28+ for final posts going to large audiences."),
10240
- response_format: z17.enum(["text", "json"]).default("text")
10575
+ response_format: z17.enum(["text", "json"]).default("text").describe("Response format. Defaults to text.")
10576
+ },
10577
+ {
10578
+ title: "Quality Check Plan",
10579
+ readOnlyHint: true,
10580
+ destructiveHint: false,
10581
+ idempotentHint: true,
10582
+ openWorldHint: false
10241
10583
  },
10242
10584
  async ({ plan, threshold, response_format }) => {
10243
10585
  const startedAt = Date.now();
@@ -10440,7 +10782,14 @@ function registerPlanningTools(server2) {
10440
10782
  start_date: z18.string().optional().describe("ISO date, defaults to tomorrow"),
10441
10783
  brand_voice: z18.string().optional().describe("Override brand voice description"),
10442
10784
  project_id: z18.string().optional().describe("Project ID for brand/insights context"),
10443
- response_format: z18.enum(["text", "json"]).default("json")
10785
+ response_format: z18.enum(["text", "json"]).default("json").describe("Response format. Defaults to json.")
10786
+ },
10787
+ {
10788
+ title: "Plan Content Week",
10789
+ readOnlyHint: false,
10790
+ destructiveHint: false,
10791
+ idempotentHint: false,
10792
+ openWorldHint: true
10444
10793
  },
10445
10794
  async ({
10446
10795
  topic,
@@ -10595,7 +10944,7 @@ ${ideationContext.promptInjection.slice(0, 1500)}` : "",
10595
10944
  content: [
10596
10945
  {
10597
10946
  type: "text",
10598
- text: `Plan generation failed: ${aiError ?? "No response from AI"}`
10947
+ text: formatToolError(`Plan generation failed: ${aiError ?? "No response from AI"}`)
10599
10948
  }
10600
10949
  ],
10601
10950
  isError: true
@@ -10615,10 +10964,10 @@ ${ideationContext.promptInjection.slice(0, 1500)}` : "",
10615
10964
  content: [
10616
10965
  {
10617
10966
  type: "text",
10618
- text: `AI response could not be parsed as JSON.
10967
+ text: formatToolError(`AI response could not be parsed as JSON.
10619
10968
 
10620
10969
  Raw output (first 1000 chars):
10621
- ${rawText.slice(0, 1e3)}`
10970
+ ${rawText.slice(0, 1e3)}`)
10622
10971
  }
10623
10972
  ],
10624
10973
  isError: true
@@ -10745,7 +11094,7 @@ ${rawText.slice(0, 1e3)}`
10745
11094
  content: [
10746
11095
  {
10747
11096
  type: "text",
10748
- text: `Plan generation failed: ${message}`
11097
+ text: formatToolError(`Plan generation failed: ${message}`)
10749
11098
  }
10750
11099
  ],
10751
11100
  isError: true
@@ -10758,12 +11107,19 @@ ${rawText.slice(0, 1e3)}`
10758
11107
  "Save a content plan to the database for team review, approval workflows, and scheduled publishing. Creates a plan_id you can reference in get_content_plan, update_content_plan, and schedule_content_plan.",
10759
11108
  {
10760
11109
  plan: z18.object({
10761
- topic: z18.string(),
10762
- posts: z18.array(z18.record(z18.string(), z18.unknown()))
10763
- }).passthrough(),
10764
- project_id: z18.string().uuid().optional(),
10765
- status: z18.enum(["draft", "in_review", "approved", "scheduled", "completed"]).default("draft"),
10766
- response_format: z18.enum(["text", "json"]).default("json")
11110
+ topic: z18.string().describe("Content plan topic or theme."),
11111
+ posts: z18.array(z18.record(z18.string(), z18.unknown())).describe("Array of post objects to save.")
11112
+ }).passthrough().describe("Content plan object with topic and posts array."),
11113
+ project_id: z18.string().uuid().optional().describe("Project ID. Defaults to active project context."),
11114
+ status: z18.enum(["draft", "in_review", "approved", "scheduled", "completed"]).default("draft").describe("Initial plan status. Defaults to draft."),
11115
+ response_format: z18.enum(["text", "json"]).default("json").describe("Response format. Defaults to json.")
11116
+ },
11117
+ {
11118
+ title: "Save Content Plan",
11119
+ readOnlyHint: false,
11120
+ destructiveHint: false,
11121
+ idempotentHint: false,
11122
+ openWorldHint: false
10767
11123
  },
10768
11124
  async ({ plan, project_id, status, response_format }) => {
10769
11125
  const startedAt = Date.now();
@@ -10851,7 +11207,7 @@ ${rawText.slice(0, 1e3)}`
10851
11207
  content: [
10852
11208
  {
10853
11209
  type: "text",
10854
- text: `Failed to save content plan: ${message}`
11210
+ text: formatToolError(`Failed to save content plan: ${message}`)
10855
11211
  }
10856
11212
  ],
10857
11213
  isError: true
@@ -10861,10 +11217,17 @@ ${rawText.slice(0, 1e3)}`
10861
11217
  );
10862
11218
  server2.tool(
10863
11219
  "get_content_plan",
10864
- "Retrieve a persisted content plan by ID.",
11220
+ "Retrieve a saved content plan to review its posts, status, and applied insights. Use after plan_content_week or save_content_plan to inspect what was generated. Feed the result into update_content_plan to revise posts or submit_content_plan_for_approval to start the review workflow.",
10865
11221
  {
10866
11222
  plan_id: z18.string().uuid().describe("Persisted content plan ID"),
10867
- response_format: z18.enum(["text", "json"]).default("json")
11223
+ response_format: z18.enum(["text", "json"]).default("json").describe("Response format. Defaults to json.")
11224
+ },
11225
+ {
11226
+ title: "Get Content Plan",
11227
+ readOnlyHint: true,
11228
+ destructiveHint: false,
11229
+ idempotentHint: true,
11230
+ openWorldHint: false
10868
11231
  },
10869
11232
  async ({ plan_id, response_format }) => {
10870
11233
  const supabase = getSupabaseClient();
@@ -10877,7 +11240,7 @@ ${rawText.slice(0, 1e3)}`
10877
11240
  content: [
10878
11241
  {
10879
11242
  type: "text",
10880
- text: `Failed to load content plan: ${sanitizeDbError(error)}`
11243
+ text: formatToolError(`Failed to load content plan: ${sanitizeDbError(error)}`)
10881
11244
  }
10882
11245
  ],
10883
11246
  isError: true
@@ -10888,7 +11251,7 @@ ${rawText.slice(0, 1e3)}`
10888
11251
  content: [
10889
11252
  {
10890
11253
  type: "text",
10891
- text: `No content plan found for plan_id=${plan_id}`
11254
+ text: formatToolError(`No content plan found for plan_id=${plan_id}`)
10892
11255
  }
10893
11256
  ],
10894
11257
  isError: true
@@ -10929,25 +11292,32 @@ ${rawText.slice(0, 1e3)}`
10929
11292
  );
10930
11293
  server2.tool(
10931
11294
  "update_content_plan",
10932
- "Update individual posts in a persisted content plan.",
11295
+ "Revise specific posts in a saved content plan -- edit captions, hooks, hashtags, schedule times, or mark posts as approved/rejected. Call after reviewing a plan with get_content_plan. When all posts are approved, the plan status auto-advances so it can be scheduled.",
10933
11296
  {
10934
- plan_id: z18.string().uuid(),
11297
+ plan_id: z18.string().uuid().describe("Content plan ID to update."),
10935
11298
  post_updates: z18.array(
10936
11299
  z18.object({
10937
- post_id: z18.string(),
10938
- caption: z18.string().optional(),
10939
- title: z18.string().optional(),
10940
- hashtags: z18.array(z18.string()).optional(),
10941
- hook: z18.string().optional(),
10942
- angle: z18.string().optional(),
10943
- visual_direction: z18.string().optional(),
10944
- media_url: z18.string().optional(),
10945
- schedule_at: z18.string().optional(),
10946
- platform: z18.string().optional(),
10947
- status: z18.enum(["approved", "rejected", "needs_edit"]).optional()
11300
+ post_id: z18.string().describe("ID of the post to update within this plan."),
11301
+ caption: z18.string().optional().describe("Revised caption/body text."),
11302
+ title: z18.string().optional().describe("Revised post title."),
11303
+ hashtags: z18.array(z18.string()).optional().describe("Revised hashtags array."),
11304
+ hook: z18.string().optional().describe("Revised attention-grabbing opening line."),
11305
+ angle: z18.string().optional().describe("Revised content angle or perspective."),
11306
+ visual_direction: z18.string().optional().describe("Revised visual/media direction notes."),
11307
+ media_url: z18.string().optional().describe("Revised media URL (public or R2 signed URL)."),
11308
+ schedule_at: z18.string().optional().describe("Revised ISO 8601 UTC publish datetime."),
11309
+ platform: z18.string().optional().describe("Revised target platform."),
11310
+ status: z18.enum(["approved", "rejected", "needs_edit"]).optional().describe("Review status for this post.")
10948
11311
  })
10949
- ).min(1),
10950
- response_format: z18.enum(["text", "json"]).default("json")
11312
+ ).min(1).describe("Array of post-level updates to apply."),
11313
+ response_format: z18.enum(["text", "json"]).default("json").describe("Response format. Defaults to json.")
11314
+ },
11315
+ {
11316
+ title: "Update Content Plan",
11317
+ readOnlyHint: false,
11318
+ destructiveHint: false,
11319
+ idempotentHint: true,
11320
+ openWorldHint: false
10951
11321
  },
10952
11322
  async ({ plan_id, post_updates, response_format }) => {
10953
11323
  const supabase = getSupabaseClient();
@@ -10969,7 +11339,7 @@ ${rawText.slice(0, 1e3)}`
10969
11339
  content: [
10970
11340
  {
10971
11341
  type: "text",
10972
- text: `No content plan found for plan_id=${plan_id}`
11342
+ text: formatToolError(`No content plan found for plan_id=${plan_id}`)
10973
11343
  }
10974
11344
  ],
10975
11345
  isError: true
@@ -11013,7 +11383,7 @@ ${rawText.slice(0, 1e3)}`
11013
11383
  content: [
11014
11384
  {
11015
11385
  type: "text",
11016
- text: `Failed to update content plan: ${sanitizeDbError(saveError)}`
11386
+ text: formatToolError(`Failed to update content plan: ${sanitizeDbError(saveError)}`)
11017
11387
  }
11018
11388
  ],
11019
11389
  isError: true
@@ -11048,10 +11418,17 @@ ${rawText.slice(0, 1e3)}`
11048
11418
  );
11049
11419
  server2.tool(
11050
11420
  "submit_content_plan_for_approval",
11051
- "Create pending approval items for each post in a plan and mark plan status as in_review.",
11421
+ "Submit an entire saved content plan for team review in one call -- creates approval items for every post and sets the plan to in_review status. Call after plan_content_week and any update_content_plan edits are done. Use list_plan_approvals to track reviewer decisions.",
11052
11422
  {
11053
- plan_id: z18.string().uuid(),
11054
- response_format: z18.enum(["text", "json"]).default("json")
11423
+ plan_id: z18.string().uuid().describe("Content plan ID to submit for review."),
11424
+ response_format: z18.enum(["text", "json"]).default("json").describe("Response format. Defaults to json.")
11425
+ },
11426
+ {
11427
+ title: "Submit Plan for Approval",
11428
+ readOnlyHint: false,
11429
+ destructiveHint: false,
11430
+ idempotentHint: true,
11431
+ openWorldHint: false
11055
11432
  },
11056
11433
  async ({ plan_id, response_format }) => {
11057
11434
  const supabase = getSupabaseClient();
@@ -11073,7 +11450,7 @@ ${rawText.slice(0, 1e3)}`
11073
11450
  content: [
11074
11451
  {
11075
11452
  type: "text",
11076
- text: `No content plan found for plan_id=${plan_id}`
11453
+ text: formatToolError(`No content plan found for plan_id=${plan_id}`)
11077
11454
  }
11078
11455
  ],
11079
11456
  isError: true
@@ -11086,7 +11463,7 @@ ${rawText.slice(0, 1e3)}`
11086
11463
  content: [
11087
11464
  {
11088
11465
  type: "text",
11089
- text: `Plan ${plan_id} has no posts to submit.`
11466
+ text: formatToolError(`Plan ${plan_id} has no posts to submit.`)
11090
11467
  }
11091
11468
  ],
11092
11469
  isError: true
@@ -11106,7 +11483,7 @@ ${rawText.slice(0, 1e3)}`
11106
11483
  content: [
11107
11484
  {
11108
11485
  type: "text",
11109
- text: `Failed to create approvals: ${sanitizeDbError(approvalError)}`
11486
+ text: formatToolError(`Failed to create approvals: ${sanitizeDbError(approvalError)}`)
11110
11487
  }
11111
11488
  ],
11112
11489
  isError: true
@@ -11118,7 +11495,7 @@ ${rawText.slice(0, 1e3)}`
11118
11495
  content: [
11119
11496
  {
11120
11497
  type: "text",
11121
- text: `Failed to update plan status: ${sanitizeDbError(statusError)}`
11498
+ text: formatToolError(`Failed to update plan status: ${sanitizeDbError(statusError)}`)
11122
11499
  }
11123
11500
  ],
11124
11501
  isError: true
@@ -11176,21 +11553,28 @@ async function assertProjectAccess(supabase, userId, projectId) {
11176
11553
  function registerPlanApprovalTools(server2) {
11177
11554
  server2.tool(
11178
11555
  "create_plan_approvals",
11179
- "Create pending approval rows for each post in a content plan.",
11556
+ "Create individual approval items for posts you supply explicitly, useful when building a custom approval queue outside the standard plan workflow. Requires the post array as input. Use list_plan_approvals to check status afterward, and respond_plan_approval to approve or reject each item.",
11180
11557
  {
11181
11558
  plan_id: z19.string().uuid().describe("Content plan ID"),
11182
11559
  posts: z19.array(
11183
11560
  z19.object({
11184
- id: z19.string(),
11185
- platform: z19.string().optional(),
11186
- caption: z19.string().optional(),
11187
- title: z19.string().optional(),
11188
- media_url: z19.string().optional(),
11189
- schedule_at: z19.string().optional()
11561
+ id: z19.string().describe("Unique post identifier from the content plan."),
11562
+ platform: z19.string().optional().describe("Target platform (e.g. instagram, youtube)."),
11563
+ caption: z19.string().optional().describe("Post caption/body text."),
11564
+ title: z19.string().optional().describe("Post title, used by YouTube and LinkedIn articles."),
11565
+ media_url: z19.string().optional().describe("Public or R2 signed URL for the post media."),
11566
+ schedule_at: z19.string().optional().describe("ISO 8601 UTC datetime to publish (e.g. 2026-03-20T14:00:00Z).")
11190
11567
  }).passthrough()
11191
11568
  ).min(1).describe("Posts to create approval entries for."),
11192
11569
  project_id: z19.string().uuid().optional().describe("Project ID. Defaults to active project context."),
11193
- response_format: z19.enum(["text", "json"]).optional()
11570
+ response_format: z19.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
11571
+ },
11572
+ {
11573
+ title: "Create Plan Approvals",
11574
+ readOnlyHint: false,
11575
+ destructiveHint: false,
11576
+ idempotentHint: false,
11577
+ openWorldHint: false
11194
11578
  },
11195
11579
  async ({ plan_id, posts, project_id, response_format }) => {
11196
11580
  const supabase = getSupabaseClient();
@@ -11270,8 +11654,15 @@ function registerPlanApprovalTools(server2) {
11270
11654
  "List MCP-native approval items for a specific content plan.",
11271
11655
  {
11272
11656
  plan_id: z19.string().uuid().describe("Content plan ID"),
11273
- status: z19.enum(["pending", "approved", "rejected", "edited"]).optional(),
11274
- response_format: z19.enum(["text", "json"]).optional()
11657
+ status: z19.enum(["pending", "approved", "rejected", "edited"]).optional().describe("Filter approvals by status. Omit to return all statuses."),
11658
+ response_format: z19.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
11659
+ },
11660
+ {
11661
+ title: "List Plan Approvals",
11662
+ readOnlyHint: true,
11663
+ destructiveHint: false,
11664
+ idempotentHint: true,
11665
+ openWorldHint: false
11275
11666
  },
11276
11667
  async ({ plan_id, status, response_format }) => {
11277
11668
  const supabase = getSupabaseClient();
@@ -11338,10 +11729,19 @@ function registerPlanApprovalTools(server2) {
11338
11729
  "Approve, reject, or edit a pending plan approval item.",
11339
11730
  {
11340
11731
  approval_id: z19.string().uuid().describe("Approval item ID"),
11341
- decision: z19.enum(["approved", "rejected", "edited"]),
11342
- edited_post: z19.record(z19.string(), z19.unknown()).optional(),
11343
- reason: z19.string().max(1e3).optional(),
11344
- response_format: z19.enum(["text", "json"]).optional()
11732
+ decision: z19.enum(["approved", "rejected", "edited"]).describe("Approval decision for this post."),
11733
+ edited_post: z19.record(z19.string(), z19.unknown()).optional().describe(
11734
+ 'Revised post fields when decision is "edited" (e.g. {caption: "...", hashtags: [...]}).'
11735
+ ),
11736
+ reason: z19.string().max(1e3).optional().describe("Optional reason for the decision, visible to the plan author."),
11737
+ response_format: z19.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
11738
+ },
11739
+ {
11740
+ title: "Respond to Plan Approval",
11741
+ readOnlyHint: false,
11742
+ destructiveHint: false,
11743
+ idempotentHint: true,
11744
+ openWorldHint: false
11345
11745
  },
11346
11746
  async ({ approval_id, decision, edited_post, reason, response_format }) => {
11347
11747
  const supabase = getSupabaseClient();
@@ -11429,6 +11829,13 @@ function registerDiscoveryTools(server2) {
11429
11829
  'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
11430
11830
  )
11431
11831
  },
11832
+ {
11833
+ title: "Search Tools",
11834
+ readOnlyHint: true,
11835
+ destructiveHint: false,
11836
+ idempotentHint: true,
11837
+ openWorldHint: false
11838
+ },
11432
11839
  async ({ query, module, scope, detail }) => {
11433
11840
  let results = [...TOOL_CATALOG];
11434
11841
  if (query) {