@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/CHANGELOG.md +16 -0
- package/dist/http.js +535 -128
- package/dist/index.js +535 -128
- 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.5.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
11344
|
-
|
|
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) {
|