@socialneuron/mcp-server 1.4.2 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/http.js +525 -132
- package/dist/index.js +571 -181
- package/package.json +4 -4
package/dist/http.js
CHANGED
|
@@ -393,7 +393,9 @@ async function getDefaultUserId() {
|
|
|
393
393
|
if (authenticatedUserId) return authenticatedUserId;
|
|
394
394
|
const envUserId = process.env.SOCIALNEURON_USER_ID;
|
|
395
395
|
if (envUserId) return envUserId;
|
|
396
|
-
throw new Error(
|
|
396
|
+
throw new Error(
|
|
397
|
+
"No user ID available. Set SOCIALNEURON_USER_ID or authenticate via API key."
|
|
398
|
+
);
|
|
397
399
|
}
|
|
398
400
|
async function getDefaultProjectId() {
|
|
399
401
|
const userId = await getDefaultUserId().catch(() => null);
|
|
@@ -437,7 +439,9 @@ async function initializeAuth() {
|
|
|
437
439
|
console.error("[MCP] Scopes: " + authenticatedScopes.join(", "));
|
|
438
440
|
if (authenticatedExpiresAt) {
|
|
439
441
|
const expiresMs = new Date(authenticatedExpiresAt).getTime();
|
|
440
|
-
const daysLeft = Math.ceil(
|
|
442
|
+
const daysLeft = Math.ceil(
|
|
443
|
+
(expiresMs - Date.now()) / (1e3 * 60 * 60 * 24)
|
|
444
|
+
);
|
|
441
445
|
console.error("[MCP] Key expires: " + authenticatedExpiresAt);
|
|
442
446
|
if (daysLeft <= 7) {
|
|
443
447
|
console.error(
|
|
@@ -460,8 +464,12 @@ async function initializeAuth() {
|
|
|
460
464
|
console.error(
|
|
461
465
|
"[MCP] \u26A0 DEPRECATED: Service role keys grant full admin access to your database."
|
|
462
466
|
);
|
|
463
|
-
console.error(
|
|
464
|
-
|
|
467
|
+
console.error(
|
|
468
|
+
"[MCP] Migrate to API key auth: npx @socialneuron/mcp-server setup"
|
|
469
|
+
);
|
|
470
|
+
console.error(
|
|
471
|
+
"[MCP] Then remove SOCIALNEURON_SERVICE_KEY from your environment."
|
|
472
|
+
);
|
|
465
473
|
if (!process.env.SOCIALNEURON_USER_ID) {
|
|
466
474
|
console.error(
|
|
467
475
|
"[MCP] Warning: SOCIALNEURON_USER_ID not set. Tools requiring a user will fail."
|
|
@@ -848,10 +856,10 @@ init_supabase();
|
|
|
848
856
|
function registerIdeationTools(server) {
|
|
849
857
|
server.tool(
|
|
850
858
|
"generate_content",
|
|
851
|
-
"
|
|
859
|
+
"Create a script, caption, hook, or blog post tailored to a specific platform. Pass project_id to auto-load brand profile and performance context, or call get_ideation_context first for full context. Output is draft text ready for quality_check then schedule_post.",
|
|
852
860
|
{
|
|
853
861
|
prompt: z.string().max(1e4).describe(
|
|
854
|
-
|
|
862
|
+
'Detailed content prompt. Include topic, angle, audience, and requirements. Example: "LinkedIn post about AI productivity for CTOs, 300 words, include 3 actionable tips, conversational tone." Richer prompts produce better results.'
|
|
855
863
|
),
|
|
856
864
|
content_type: z.enum(["script", "caption", "blog", "hook"]).describe(
|
|
857
865
|
'Type of content to generate. "script" for video scripts, "caption" for social media captions, "blog" for blog posts, "hook" for attention-grabbing hooks.'
|
|
@@ -869,7 +877,7 @@ function registerIdeationTools(server) {
|
|
|
869
877
|
"Target social media platform. Helps tailor tone, length, and format."
|
|
870
878
|
),
|
|
871
879
|
brand_voice: z.string().max(500).optional().describe(
|
|
872
|
-
'
|
|
880
|
+
'Tone directive (e.g. "direct, no jargon, second person" or "witty Gen-Z energy with emoji"). Leave blank to auto-load from project brand profile if project_id is set.'
|
|
873
881
|
),
|
|
874
882
|
model: z.enum(["gemini-2.0-flash", "gemini-2.5-flash", "gemini-2.5-pro"]).optional().describe(
|
|
875
883
|
"AI model to use. Defaults to gemini-2.5-flash. Use gemini-2.5-pro for highest quality."
|
|
@@ -878,6 +886,13 @@ function registerIdeationTools(server) {
|
|
|
878
886
|
"Project ID to auto-load brand profile and performance context for prompt enrichment."
|
|
879
887
|
)
|
|
880
888
|
},
|
|
889
|
+
{
|
|
890
|
+
title: "Generate Content",
|
|
891
|
+
readOnlyHint: false,
|
|
892
|
+
destructiveHint: false,
|
|
893
|
+
idempotentHint: false,
|
|
894
|
+
openWorldHint: true
|
|
895
|
+
},
|
|
881
896
|
async ({
|
|
882
897
|
prompt,
|
|
883
898
|
content_type,
|
|
@@ -1025,7 +1040,7 @@ Content Type: ${content_type}`;
|
|
|
1025
1040
|
);
|
|
1026
1041
|
server.tool(
|
|
1027
1042
|
"fetch_trends",
|
|
1028
|
-
|
|
1043
|
+
'Get current trending topics for content inspiration. Source "youtube" returns trending videos with view counts, "google_trends" returns rising search terms, "rss"/"url" extracts topics from any feed or page. Results cached 1 hour \u2014 set force_refresh=true for real-time. Feed results into generate_content or plan_content_week.',
|
|
1029
1044
|
{
|
|
1030
1045
|
source: z.enum(["youtube", "google_trends", "rss", "url"]).describe(
|
|
1031
1046
|
'Data source. "youtube" fetches trending videos, "google_trends" fetches daily search trends, "rss" fetches from a custom RSS feed URL, "url" extracts trend data from a web page.'
|
|
@@ -1041,6 +1056,13 @@ Content Type: ${content_type}`;
|
|
|
1041
1056
|
),
|
|
1042
1057
|
force_refresh: z.boolean().optional().describe("Skip the server-side cache and fetch fresh data.")
|
|
1043
1058
|
},
|
|
1059
|
+
{
|
|
1060
|
+
title: "Fetch Trends",
|
|
1061
|
+
readOnlyHint: true,
|
|
1062
|
+
destructiveHint: false,
|
|
1063
|
+
idempotentHint: false,
|
|
1064
|
+
openWorldHint: true
|
|
1065
|
+
},
|
|
1044
1066
|
async ({ source, category, niche, url, force_refresh }) => {
|
|
1045
1067
|
if ((source === "rss" || source === "url") && !url) {
|
|
1046
1068
|
return {
|
|
@@ -1111,7 +1133,7 @@ Content Type: ${content_type}`;
|
|
|
1111
1133
|
);
|
|
1112
1134
|
server.tool(
|
|
1113
1135
|
"adapt_content",
|
|
1114
|
-
"
|
|
1136
|
+
"Rewrite existing content for a different platform \u2014 adjusts character limits, hashtag style, tone, and CTA format automatically. Use after generate_content when you need the same message across multiple platforms. Pass project_id to apply platform-specific voice overrides from your brand profile.",
|
|
1115
1137
|
{
|
|
1116
1138
|
content: z.string().max(5e3).describe(
|
|
1117
1139
|
"The content to adapt. Can be a caption, script, blog excerpt, or any text."
|
|
@@ -1145,6 +1167,13 @@ Content Type: ${content_type}`;
|
|
|
1145
1167
|
"Optional project ID to load platform voice overrides from brand profile."
|
|
1146
1168
|
)
|
|
1147
1169
|
},
|
|
1170
|
+
{
|
|
1171
|
+
title: "Adapt Content",
|
|
1172
|
+
readOnlyHint: false,
|
|
1173
|
+
destructiveHint: false,
|
|
1174
|
+
idempotentHint: false,
|
|
1175
|
+
openWorldHint: true
|
|
1176
|
+
},
|
|
1148
1177
|
async ({
|
|
1149
1178
|
content,
|
|
1150
1179
|
source_platform,
|
|
@@ -1304,7 +1333,7 @@ function sanitizeDbError(error) {
|
|
|
1304
1333
|
init_request_context();
|
|
1305
1334
|
|
|
1306
1335
|
// src/lib/version.ts
|
|
1307
|
-
var MCP_VERSION = "1.
|
|
1336
|
+
var MCP_VERSION = "1.5.1";
|
|
1308
1337
|
|
|
1309
1338
|
// src/tools/content.ts
|
|
1310
1339
|
var MAX_CREDITS_PER_RUN = Math.max(
|
|
@@ -1412,10 +1441,10 @@ function checkAssetBudget() {
|
|
|
1412
1441
|
function registerContentTools(server) {
|
|
1413
1442
|
server.tool(
|
|
1414
1443
|
"generate_video",
|
|
1415
|
-
"Start an AI video generation job
|
|
1444
|
+
"Start an async AI video generation job \u2014 returns a job_id immediately. Poll with check_status every 10-30s until complete. Cost varies by model: veo3-fast (~15 credits/5s), kling-3 (~30 credits/5s), sora2-pro (~60 credits/10s). Check get_credit_balance first for expensive generations.",
|
|
1416
1445
|
{
|
|
1417
1446
|
prompt: z2.string().max(2500).describe(
|
|
1418
|
-
|
|
1447
|
+
'Video prompt \u2014 be specific about visual style, camera movement, lighting, and mood. Example: "Aerial drone shot of coastal cliffs at golden hour, slow dolly forward, cinematic 24fps, warm color grading." Vague prompts produce generic results.'
|
|
1419
1448
|
),
|
|
1420
1449
|
model: z2.enum([
|
|
1421
1450
|
"veo3-fast",
|
|
@@ -1427,13 +1456,13 @@ function registerContentTools(server) {
|
|
|
1427
1456
|
"kling-3",
|
|
1428
1457
|
"kling-3-pro"
|
|
1429
1458
|
]).describe(
|
|
1430
|
-
"Video
|
|
1459
|
+
"Video model. veo3-fast: fastest (~15 credits/5s, ~60s render). veo3-quality: highest quality (~20 credits/5s, ~120s). sora2-pro: OpenAI premium (~60 credits/10s). kling-3: 4K with audio (~30 credits/5s). kling-3-pro: best Kling quality (~40 credits/5s)."
|
|
1431
1460
|
),
|
|
1432
1461
|
duration: z2.number().min(3).max(30).optional().describe(
|
|
1433
1462
|
"Video duration in seconds. kling: 5-30s, kling-3/kling-3-pro: 3-15s, sora2: 10-15s. Defaults to 5 seconds."
|
|
1434
1463
|
),
|
|
1435
1464
|
aspect_ratio: z2.enum(["16:9", "9:16", "1:1"]).optional().describe(
|
|
1436
|
-
"
|
|
1465
|
+
"Video aspect ratio. 16:9 for YouTube/landscape, 9:16 for TikTok/Reels/Shorts, 1:1 for Instagram feed/square. Defaults to 16:9."
|
|
1437
1466
|
),
|
|
1438
1467
|
enable_audio: z2.boolean().optional().describe(
|
|
1439
1468
|
"Enable native audio generation. Kling 2.6: doubles cost. Kling 3.0: 50% more (std 30/sec, pro 40/sec). 5+ languages."
|
|
@@ -1446,6 +1475,13 @@ function registerContentTools(server) {
|
|
|
1446
1475
|
),
|
|
1447
1476
|
response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
1448
1477
|
},
|
|
1478
|
+
{
|
|
1479
|
+
title: "Generate Video",
|
|
1480
|
+
readOnlyHint: false,
|
|
1481
|
+
destructiveHint: false,
|
|
1482
|
+
idempotentHint: false,
|
|
1483
|
+
openWorldHint: true
|
|
1484
|
+
},
|
|
1449
1485
|
async ({
|
|
1450
1486
|
prompt,
|
|
1451
1487
|
model,
|
|
@@ -1620,7 +1656,7 @@ function registerContentTools(server) {
|
|
|
1620
1656
|
);
|
|
1621
1657
|
server.tool(
|
|
1622
1658
|
"generate_image",
|
|
1623
|
-
"Start an AI image generation job
|
|
1659
|
+
"Start an async AI image generation job \u2014 returns a job_id immediately. Poll with check_status every 5-15s until complete. Costs 2-10 credits depending on model. Use for social media posts, carousel slides, or as input to generate_video (image-to-video).",
|
|
1624
1660
|
{
|
|
1625
1661
|
prompt: z2.string().max(2e3).describe(
|
|
1626
1662
|
"Text prompt describing the image to generate. Be specific about style, composition, colors, lighting, and subject matter."
|
|
@@ -1644,6 +1680,13 @@ function registerContentTools(server) {
|
|
|
1644
1680
|
),
|
|
1645
1681
|
response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
1646
1682
|
},
|
|
1683
|
+
{
|
|
1684
|
+
title: "Generate Image",
|
|
1685
|
+
readOnlyHint: false,
|
|
1686
|
+
destructiveHint: false,
|
|
1687
|
+
idempotentHint: false,
|
|
1688
|
+
openWorldHint: true
|
|
1689
|
+
},
|
|
1647
1690
|
async ({ prompt, model, aspect_ratio, image_url, response_format }) => {
|
|
1648
1691
|
const format = response_format ?? "text";
|
|
1649
1692
|
const startedAt = Date.now();
|
|
@@ -1800,13 +1843,20 @@ function registerContentTools(server) {
|
|
|
1800
1843
|
);
|
|
1801
1844
|
server.tool(
|
|
1802
1845
|
"check_status",
|
|
1803
|
-
|
|
1846
|
+
'Poll an async job started by generate_video or generate_image. Returns status (queued/processing/completed/failed), progress %, and result URL on completion. Poll every 10-30s for video, 5-15s for images. On "failed" status, the error field explains why \u2014 check credits or try a different model.',
|
|
1804
1847
|
{
|
|
1805
1848
|
job_id: z2.string().describe(
|
|
1806
1849
|
"The job ID returned by generate_video or generate_image. This is the asyncJobId or taskId value."
|
|
1807
1850
|
),
|
|
1808
1851
|
response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
1809
1852
|
},
|
|
1853
|
+
{
|
|
1854
|
+
title: "Check Job Status",
|
|
1855
|
+
readOnlyHint: true,
|
|
1856
|
+
destructiveHint: false,
|
|
1857
|
+
idempotentHint: true,
|
|
1858
|
+
openWorldHint: true
|
|
1859
|
+
},
|
|
1810
1860
|
async ({ job_id, response_format }) => {
|
|
1811
1861
|
const format = response_format ?? "text";
|
|
1812
1862
|
const startedAt = Date.now();
|
|
@@ -1976,7 +2026,7 @@ function registerContentTools(server) {
|
|
|
1976
2026
|
);
|
|
1977
2027
|
server.tool(
|
|
1978
2028
|
"create_storyboard",
|
|
1979
|
-
"
|
|
2029
|
+
"Plan a multi-scene video storyboard with AI-generated prompts, durations, captions, and voiceover text per frame. Use before generate_video or generate_image to create cohesive multi-shot content. Include brand_context from get_brand_profile for consistent visual branding across frames.",
|
|
1980
2030
|
{
|
|
1981
2031
|
concept: z2.string().max(2e3).describe(
|
|
1982
2032
|
'The video concept/idea. Include: hook, key messages, target audience, and desired outcome (e.g., "TikTok ad for VPN app targeting privacy-conscious millennials, hook with shocking stat about data leaks").'
|
|
@@ -2004,6 +2054,13 @@ function registerContentTools(server) {
|
|
|
2004
2054
|
"Response format. Defaults to json for structured storyboard data."
|
|
2005
2055
|
)
|
|
2006
2056
|
},
|
|
2057
|
+
{
|
|
2058
|
+
title: "Create Storyboard",
|
|
2059
|
+
readOnlyHint: false,
|
|
2060
|
+
destructiveHint: false,
|
|
2061
|
+
idempotentHint: false,
|
|
2062
|
+
openWorldHint: true
|
|
2063
|
+
},
|
|
2007
2064
|
async ({
|
|
2008
2065
|
concept,
|
|
2009
2066
|
brand_context,
|
|
@@ -2163,7 +2220,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
2163
2220
|
);
|
|
2164
2221
|
server.tool(
|
|
2165
2222
|
"generate_voiceover",
|
|
2166
|
-
"Generate a
|
|
2223
|
+
"Generate a voiceover audio file for video narration. Returns an R2-hosted audio URL. Use after create_storyboard to add narration to each scene, or standalone for podcast intros and ad reads. Costs ~2 credits per generation.",
|
|
2167
2224
|
{
|
|
2168
2225
|
text: z2.string().max(5e3).describe("The script/text to convert to speech."),
|
|
2169
2226
|
voice: z2.enum([
|
|
@@ -2184,6 +2241,13 @@ Return ONLY valid JSON in this exact format:
|
|
|
2184
2241
|
speed: z2.number().min(0.5).max(2).optional().describe("Speech speed multiplier. 1.0 is normal. Defaults to 1.0."),
|
|
2185
2242
|
response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
2186
2243
|
},
|
|
2244
|
+
{
|
|
2245
|
+
title: "Generate Voiceover",
|
|
2246
|
+
readOnlyHint: false,
|
|
2247
|
+
destructiveHint: false,
|
|
2248
|
+
idempotentHint: false,
|
|
2249
|
+
openWorldHint: true
|
|
2250
|
+
},
|
|
2187
2251
|
async ({ text, voice, speed, response_format }) => {
|
|
2188
2252
|
const format = response_format ?? "text";
|
|
2189
2253
|
const startedAt = Date.now();
|
|
@@ -2314,10 +2378,10 @@ Return ONLY valid JSON in this exact format:
|
|
|
2314
2378
|
);
|
|
2315
2379
|
server.tool(
|
|
2316
2380
|
"generate_carousel",
|
|
2317
|
-
"Generate
|
|
2381
|
+
"Generate carousel slide content (headlines, body text, emphasis words per slide). Supports Hormozi-style authority format and educational templates. Returns structured slide data \u2014 render visually then publish via schedule_post with media_type=CAROUSEL_ALBUM and 2-10 media_urls on Instagram.",
|
|
2318
2382
|
{
|
|
2319
2383
|
topic: z2.string().max(200).describe(
|
|
2320
|
-
'
|
|
2384
|
+
'Carousel hook/angle \u2014 specific beats general. Example: "5 pricing mistakes that kill SaaS startups" beats "SaaS tips". Include a curiosity gap or strong opinion for better Hook Strength scores.'
|
|
2321
2385
|
),
|
|
2322
2386
|
template_id: z2.enum([
|
|
2323
2387
|
"educational-series",
|
|
@@ -2342,6 +2406,13 @@ Return ONLY valid JSON in this exact format:
|
|
|
2342
2406
|
project_id: z2.string().optional().describe("Project ID to associate the carousel with."),
|
|
2343
2407
|
response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to json.")
|
|
2344
2408
|
},
|
|
2409
|
+
{
|
|
2410
|
+
title: "Generate Carousel",
|
|
2411
|
+
readOnlyHint: false,
|
|
2412
|
+
destructiveHint: false,
|
|
2413
|
+
idempotentHint: false,
|
|
2414
|
+
openWorldHint: true
|
|
2415
|
+
},
|
|
2345
2416
|
async ({
|
|
2346
2417
|
topic,
|
|
2347
2418
|
template_id,
|
|
@@ -2645,14 +2716,12 @@ function asEnvelope2(data) {
|
|
|
2645
2716
|
function registerDistributionTools(server) {
|
|
2646
2717
|
server.tool(
|
|
2647
2718
|
"schedule_post",
|
|
2648
|
-
|
|
2719
|
+
'Publish or schedule a post to connected social platforms. Check list_connected_accounts first to verify active OAuth for each target platform. For Instagram carousels: use media_type=CAROUSEL_ALBUM with 2-10 media_urls. For YouTube: title is required. schedule_at uses ISO 8601 (e.g. "2026-03-20T14:00:00Z") \u2014 omit to post immediately.',
|
|
2649
2720
|
{
|
|
2650
2721
|
media_url: z3.string().optional().describe(
|
|
2651
2722
|
"Optional URL of the media file (video or image) to post. This should be a publicly accessible URL or a Cloudflare R2 signed URL from a previous generation. Required for platforms that enforce media uploads. Not needed if media_urls is provided."
|
|
2652
2723
|
),
|
|
2653
|
-
media_urls: z3.array(z3.string()).optional().describe(
|
|
2654
|
-
"Array of image URLs for Instagram carousel posts (2-10 images). Each URL should be publicly accessible or a Cloudflare R2 URL. When provided with media_type=CAROUSEL_ALBUM, creates an Instagram carousel."
|
|
2655
|
-
),
|
|
2724
|
+
media_urls: z3.array(z3.string()).optional().describe("Array of 2-10 image URLs for Instagram carousel posts. Each URL must be publicly accessible or a Cloudflare R2 signed URL. Use with media_type=CAROUSEL_ALBUM."),
|
|
2656
2725
|
media_type: z3.enum(["IMAGE", "VIDEO", "CAROUSEL_ALBUM"]).optional().describe(
|
|
2657
2726
|
"Media type. Set to CAROUSEL_ALBUM with media_urls for Instagram carousels. Default: auto-detected from media_url."
|
|
2658
2727
|
),
|
|
@@ -2668,22 +2737,23 @@ function registerDistributionTools(server) {
|
|
|
2668
2737
|
"threads",
|
|
2669
2738
|
"bluesky"
|
|
2670
2739
|
])
|
|
2671
|
-
).min(1).describe(
|
|
2672
|
-
"Target platforms to post to. Each must have an active OAuth connection."
|
|
2673
|
-
),
|
|
2740
|
+
).min(1).describe("Target platforms (array). Each must have active OAuth \u2014 check list_connected_accounts first. Values: youtube, tiktok, instagram, twitter, linkedin, facebook, threads, bluesky."),
|
|
2674
2741
|
title: z3.string().optional().describe("Post title (used by YouTube and some other platforms)."),
|
|
2675
|
-
hashtags: z3.array(z3.string()).optional().describe(
|
|
2676
|
-
|
|
2677
|
-
),
|
|
2678
|
-
schedule_at: z3.string().optional().describe(
|
|
2679
|
-
'ISO 8601 datetime for scheduled posting (e.g. "2026-03-15T14:00:00Z"). Omit for immediate posting.'
|
|
2680
|
-
),
|
|
2742
|
+
hashtags: z3.array(z3.string()).optional().describe('Hashtags to append to caption. Include or omit the "#" prefix \u2014 both work. Example: ["ai", "contentcreator"] or ["#ai", "#contentcreator"].'),
|
|
2743
|
+
schedule_at: z3.string().optional().describe('ISO 8601 UTC datetime for scheduled posting (e.g. "2026-03-20T14:00:00Z"). Omit to post immediately. Must be in the future.'),
|
|
2681
2744
|
project_id: z3.string().optional().describe("Social Neuron project ID to associate this post with."),
|
|
2682
2745
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
|
|
2683
2746
|
attribution: z3.boolean().optional().describe(
|
|
2684
2747
|
'If true, appends "Created with Social Neuron" to the caption. Default: false.'
|
|
2685
2748
|
)
|
|
2686
2749
|
},
|
|
2750
|
+
{
|
|
2751
|
+
title: "Schedule Post",
|
|
2752
|
+
readOnlyHint: false,
|
|
2753
|
+
destructiveHint: false,
|
|
2754
|
+
idempotentHint: false,
|
|
2755
|
+
openWorldHint: true
|
|
2756
|
+
},
|
|
2687
2757
|
async ({
|
|
2688
2758
|
media_url,
|
|
2689
2759
|
media_urls,
|
|
@@ -2831,10 +2901,17 @@ Created with Social Neuron`;
|
|
|
2831
2901
|
);
|
|
2832
2902
|
server.tool(
|
|
2833
2903
|
"list_connected_accounts",
|
|
2834
|
-
"
|
|
2904
|
+
"Check which social platforms have active OAuth connections for posting. Call this before schedule_post to verify credentials. If a platform is missing or expired, the user needs to reconnect at socialneuron.com/settings/connections.",
|
|
2835
2905
|
{
|
|
2836
2906
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
2837
2907
|
},
|
|
2908
|
+
{
|
|
2909
|
+
title: "List Connected Accounts",
|
|
2910
|
+
readOnlyHint: true,
|
|
2911
|
+
destructiveHint: false,
|
|
2912
|
+
idempotentHint: true,
|
|
2913
|
+
openWorldHint: false
|
|
2914
|
+
},
|
|
2838
2915
|
async ({ response_format }) => {
|
|
2839
2916
|
const format = response_format ?? "text";
|
|
2840
2917
|
const supabase = getSupabaseClient();
|
|
@@ -2896,7 +2973,7 @@ Created with Social Neuron`;
|
|
|
2896
2973
|
);
|
|
2897
2974
|
server.tool(
|
|
2898
2975
|
"list_recent_posts",
|
|
2899
|
-
"List recent
|
|
2976
|
+
"List recent published and scheduled posts with status, platform, title, and timestamps. Use to check what has been posted before planning new content, or to find post IDs for fetch_analytics. Filter by platform or status to narrow results.",
|
|
2900
2977
|
{
|
|
2901
2978
|
platform: z3.enum([
|
|
2902
2979
|
"youtube",
|
|
@@ -2913,6 +2990,13 @@ Created with Social Neuron`;
|
|
|
2913
2990
|
limit: z3.number().min(1).max(50).optional().describe("Maximum number of posts to return. Defaults to 20."),
|
|
2914
2991
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
2915
2992
|
},
|
|
2993
|
+
{
|
|
2994
|
+
title: "List Recent Posts",
|
|
2995
|
+
readOnlyHint: true,
|
|
2996
|
+
destructiveHint: false,
|
|
2997
|
+
idempotentHint: true,
|
|
2998
|
+
openWorldHint: false
|
|
2999
|
+
},
|
|
2916
3000
|
async ({ platform: platform2, status, days, limit, response_format }) => {
|
|
2917
3001
|
const format = response_format ?? "text";
|
|
2918
3002
|
const supabase = getSupabaseClient();
|
|
@@ -3012,7 +3096,7 @@ Created with Social Neuron`;
|
|
|
3012
3096
|
};
|
|
3013
3097
|
server.tool(
|
|
3014
3098
|
"find_next_slots",
|
|
3015
|
-
"Find
|
|
3099
|
+
"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.",
|
|
3016
3100
|
{
|
|
3017
3101
|
platforms: z3.array(
|
|
3018
3102
|
z3.enum([
|
|
@@ -3025,11 +3109,18 @@ Created with Social Neuron`;
|
|
|
3025
3109
|
"threads",
|
|
3026
3110
|
"bluesky"
|
|
3027
3111
|
])
|
|
3028
|
-
).min(1),
|
|
3112
|
+
).min(1).describe("Platforms to find posting slots for."),
|
|
3029
3113
|
count: z3.number().min(1).max(20).default(7).describe("Number of slots to find"),
|
|
3030
3114
|
start_after: z3.string().optional().describe("ISO datetime, defaults to now"),
|
|
3031
3115
|
min_gap_hours: z3.number().min(1).max(24).default(4).describe("Minimum gap between posts on same platform"),
|
|
3032
|
-
response_format: z3.enum(["text", "json"]).default("text")
|
|
3116
|
+
response_format: z3.enum(["text", "json"]).default("text").describe("Response format. Defaults to text.")
|
|
3117
|
+
},
|
|
3118
|
+
{
|
|
3119
|
+
title: "Find Next Posting Slots",
|
|
3120
|
+
readOnlyHint: true,
|
|
3121
|
+
destructiveHint: false,
|
|
3122
|
+
idempotentHint: true,
|
|
3123
|
+
openWorldHint: false
|
|
3033
3124
|
},
|
|
3034
3125
|
async ({
|
|
3035
3126
|
platforms,
|
|
@@ -3149,20 +3240,20 @@ Created with Social Neuron`;
|
|
|
3149
3240
|
plan: z3.object({
|
|
3150
3241
|
posts: z3.array(
|
|
3151
3242
|
z3.object({
|
|
3152
|
-
id: z3.string(),
|
|
3153
|
-
caption: z3.string(),
|
|
3154
|
-
platform: z3.string(),
|
|
3155
|
-
title: z3.string().optional(),
|
|
3156
|
-
media_url: z3.string().optional(),
|
|
3157
|
-
schedule_at: z3.string().optional(),
|
|
3158
|
-
hashtags: z3.array(z3.string()).optional()
|
|
3243
|
+
id: z3.string().describe("Unique post identifier from the content plan."),
|
|
3244
|
+
caption: z3.string().describe("Post caption/body text."),
|
|
3245
|
+
platform: z3.string().describe("Target platform name (e.g. instagram, youtube)."),
|
|
3246
|
+
title: z3.string().optional().describe("Post title, required for YouTube."),
|
|
3247
|
+
media_url: z3.string().optional().describe("Public or R2 signed URL for the post media."),
|
|
3248
|
+
schedule_at: z3.string().optional().describe("ISO 8601 UTC datetime to publish (e.g. 2026-03-20T14:00:00Z)."),
|
|
3249
|
+
hashtags: z3.array(z3.string()).optional().describe("Hashtags to append to the caption.")
|
|
3159
3250
|
})
|
|
3160
3251
|
)
|
|
3161
|
-
}).passthrough().optional(),
|
|
3252
|
+
}).passthrough().optional().describe("Inline content plan object with a posts array. Provide this or plan_id."),
|
|
3162
3253
|
plan_id: z3.string().uuid().optional().describe("Persisted content plan ID from content_plans table"),
|
|
3163
3254
|
auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
|
|
3164
3255
|
dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
|
|
3165
|
-
response_format: z3.enum(["text", "json"]).default("text"),
|
|
3256
|
+
response_format: z3.enum(["text", "json"]).default("text").describe("Response format. Defaults to text."),
|
|
3166
3257
|
enforce_quality: z3.boolean().default(true).describe(
|
|
3167
3258
|
"When true, block scheduling for posts that fail quality checks."
|
|
3168
3259
|
),
|
|
@@ -3172,6 +3263,13 @@ Created with Social Neuron`;
|
|
|
3172
3263
|
batch_size: z3.number().int().min(1).max(10).default(4).describe("Concurrent schedule calls per platform batch."),
|
|
3173
3264
|
idempotency_seed: z3.string().max(128).optional().describe("Optional stable seed used when building idempotency keys.")
|
|
3174
3265
|
},
|
|
3266
|
+
{
|
|
3267
|
+
title: "Schedule Content Plan",
|
|
3268
|
+
readOnlyHint: false,
|
|
3269
|
+
destructiveHint: false,
|
|
3270
|
+
idempotentHint: false,
|
|
3271
|
+
openWorldHint: true
|
|
3272
|
+
},
|
|
3175
3273
|
async ({
|
|
3176
3274
|
plan,
|
|
3177
3275
|
plan_id,
|
|
@@ -3735,13 +3833,14 @@ function registerAnalyticsTools(server) {
|
|
|
3735
3833
|
"threads",
|
|
3736
3834
|
"bluesky"
|
|
3737
3835
|
]).optional().describe("Filter analytics to a specific platform."),
|
|
3738
|
-
days: z4.number().min(1).max(365).optional().describe("
|
|
3836
|
+
days: z4.number().min(1).max(365).optional().describe("Lookback window in days (1-365). Default 30. Use 7 for weekly review, 30 for monthly summary, 90 for quarterly trends."),
|
|
3739
3837
|
content_id: z4.string().uuid().optional().describe(
|
|
3740
3838
|
"Filter to a specific content_history ID to see performance of one piece of content."
|
|
3741
3839
|
),
|
|
3742
3840
|
limit: z4.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
|
|
3743
3841
|
response_format: z4.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
3744
3842
|
},
|
|
3843
|
+
{ title: "Fetch Analytics", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
3745
3844
|
async ({ platform: platform2, days, content_id, limit, response_format }) => {
|
|
3746
3845
|
const format = response_format ?? "text";
|
|
3747
3846
|
const supabase = getSupabaseClient();
|
|
@@ -3915,10 +4014,17 @@ function registerAnalyticsTools(server) {
|
|
|
3915
4014
|
);
|
|
3916
4015
|
server.tool(
|
|
3917
4016
|
"refresh_platform_analytics",
|
|
3918
|
-
"
|
|
4017
|
+
"Queue analytics refresh jobs for all posts from the last 7 days across connected platforms. Call this before fetch_analytics if you need fresh data. Returns immediately \u2014 data updates asynchronously over the next 1-5 minutes.",
|
|
3919
4018
|
{
|
|
3920
4019
|
response_format: z4.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
3921
4020
|
},
|
|
4021
|
+
{
|
|
4022
|
+
title: "Refresh Platform Analytics",
|
|
4023
|
+
readOnlyHint: false,
|
|
4024
|
+
destructiveHint: false,
|
|
4025
|
+
idempotentHint: false,
|
|
4026
|
+
openWorldHint: true
|
|
4027
|
+
},
|
|
3922
4028
|
async ({ response_format }) => {
|
|
3923
4029
|
const format = response_format ?? "text";
|
|
3924
4030
|
const startedAt = Date.now();
|
|
@@ -4318,6 +4424,13 @@ function registerBrandTools(server) {
|
|
|
4318
4424
|
),
|
|
4319
4425
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
4320
4426
|
},
|
|
4427
|
+
{
|
|
4428
|
+
title: "Extract Brand",
|
|
4429
|
+
readOnlyHint: true,
|
|
4430
|
+
destructiveHint: false,
|
|
4431
|
+
idempotentHint: true,
|
|
4432
|
+
openWorldHint: true
|
|
4433
|
+
},
|
|
4321
4434
|
async ({ url, response_format }) => {
|
|
4322
4435
|
const ssrfCheck = await validateUrlForSSRF(url);
|
|
4323
4436
|
if (!ssrfCheck.isValid) {
|
|
@@ -4401,6 +4514,13 @@ function registerBrandTools(server) {
|
|
|
4401
4514
|
project_id: z5.string().uuid().optional().describe("Project ID. Defaults to active project context."),
|
|
4402
4515
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
4403
4516
|
},
|
|
4517
|
+
{
|
|
4518
|
+
title: "Get Brand Profile",
|
|
4519
|
+
readOnlyHint: true,
|
|
4520
|
+
destructiveHint: false,
|
|
4521
|
+
idempotentHint: true,
|
|
4522
|
+
openWorldHint: false
|
|
4523
|
+
},
|
|
4404
4524
|
async ({ project_id, response_format }) => {
|
|
4405
4525
|
const supabase = getSupabaseClient();
|
|
4406
4526
|
const userId = await getDefaultUserId();
|
|
@@ -4498,8 +4618,17 @@ function registerBrandTools(server) {
|
|
|
4498
4618
|
"product_showcase"
|
|
4499
4619
|
]).optional().describe("Extraction method metadata."),
|
|
4500
4620
|
overall_confidence: z5.number().min(0).max(1).optional().describe("Optional overall confidence score in range 0..1."),
|
|
4501
|
-
extraction_metadata: z5.record(z5.string(), z5.unknown()).optional()
|
|
4502
|
-
|
|
4621
|
+
extraction_metadata: z5.record(z5.string(), z5.unknown()).optional().describe(
|
|
4622
|
+
"Arbitrary key-value metadata about the extraction process."
|
|
4623
|
+
),
|
|
4624
|
+
response_format: z5.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
4625
|
+
},
|
|
4626
|
+
{
|
|
4627
|
+
title: "Save Brand Profile",
|
|
4628
|
+
readOnlyHint: false,
|
|
4629
|
+
destructiveHint: false,
|
|
4630
|
+
idempotentHint: false,
|
|
4631
|
+
openWorldHint: false
|
|
4503
4632
|
},
|
|
4504
4633
|
async ({
|
|
4505
4634
|
project_id,
|
|
@@ -4612,15 +4741,32 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
4612
4741
|
"facebook",
|
|
4613
4742
|
"threads",
|
|
4614
4743
|
"bluesky"
|
|
4615
|
-
]),
|
|
4744
|
+
]).describe("Social platform to set voice overrides for."),
|
|
4616
4745
|
project_id: z5.string().uuid().optional().describe("Project ID. Defaults to active project context."),
|
|
4617
4746
|
samples: z5.string().max(3e3).optional().describe("3-5 real platform post examples for style anchoring."),
|
|
4618
|
-
tone: z5.array(z5.string()).optional()
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4747
|
+
tone: z5.array(z5.string()).optional().describe(
|
|
4748
|
+
'Tone descriptors for this platform (e.g. ["casual", "witty", "informative"]).'
|
|
4749
|
+
),
|
|
4750
|
+
style: z5.array(z5.string()).optional().describe(
|
|
4751
|
+
'Writing style tags (e.g. ["short-form", "emoji-heavy", "storytelling"]).'
|
|
4752
|
+
),
|
|
4753
|
+
avoid_patterns: z5.array(z5.string()).optional().describe(
|
|
4754
|
+
'Phrases or patterns the brand should never use on this platform (e.g. ["click here", "buy now"]).'
|
|
4755
|
+
),
|
|
4756
|
+
hashtag_strategy: z5.string().max(300).optional().describe(
|
|
4757
|
+
'Hashtag usage guidelines for this platform (e.g. "3-5 niche hashtags, no generic tags").'
|
|
4758
|
+
),
|
|
4759
|
+
cta_style: z5.string().max(300).optional().describe(
|
|
4760
|
+
'Preferred call-to-action style (e.g. "soft CTA with question" or "direct link in bio").'
|
|
4761
|
+
),
|
|
4762
|
+
response_format: z5.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
4763
|
+
},
|
|
4764
|
+
{
|
|
4765
|
+
title: "Update Platform Voice",
|
|
4766
|
+
readOnlyHint: false,
|
|
4767
|
+
destructiveHint: false,
|
|
4768
|
+
idempotentHint: false,
|
|
4769
|
+
openWorldHint: false
|
|
4624
4770
|
},
|
|
4625
4771
|
async ({
|
|
4626
4772
|
platform: platform2,
|
|
@@ -4869,6 +5015,13 @@ function registerScreenshotTools(server) {
|
|
|
4869
5015
|
"Extra milliseconds to wait after page load before capturing. Useful for animations. Defaults to 2000."
|
|
4870
5016
|
)
|
|
4871
5017
|
},
|
|
5018
|
+
{
|
|
5019
|
+
title: "Capture App Page",
|
|
5020
|
+
readOnlyHint: true,
|
|
5021
|
+
destructiveHint: false,
|
|
5022
|
+
idempotentHint: false,
|
|
5023
|
+
openWorldHint: false
|
|
5024
|
+
},
|
|
4872
5025
|
async ({ page: pageName, viewport, theme, selector, wait_ms }) => {
|
|
4873
5026
|
const startedAt = Date.now();
|
|
4874
5027
|
let rateLimitKey = "anonymous";
|
|
@@ -4989,6 +5142,13 @@ function registerScreenshotTools(server) {
|
|
|
4989
5142
|
),
|
|
4990
5143
|
wait_ms: z6.number().min(0).max(3e4).optional().describe("Extra milliseconds to wait after page load before capturing. Defaults to 1000.")
|
|
4991
5144
|
},
|
|
5145
|
+
{
|
|
5146
|
+
title: "Capture Screenshot",
|
|
5147
|
+
readOnlyHint: true,
|
|
5148
|
+
destructiveHint: false,
|
|
5149
|
+
idempotentHint: false,
|
|
5150
|
+
openWorldHint: true
|
|
5151
|
+
},
|
|
4992
5152
|
async ({ url, viewport, selector, output_path, wait_ms }) => {
|
|
4993
5153
|
const startedAt = Date.now();
|
|
4994
5154
|
let rateLimitKey = "anonymous";
|
|
@@ -5251,6 +5411,13 @@ function registerRemotionTools(server) {
|
|
|
5251
5411
|
"list_compositions",
|
|
5252
5412
|
"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.",
|
|
5253
5413
|
{},
|
|
5414
|
+
{
|
|
5415
|
+
title: "List Compositions",
|
|
5416
|
+
readOnlyHint: true,
|
|
5417
|
+
destructiveHint: false,
|
|
5418
|
+
idempotentHint: true,
|
|
5419
|
+
openWorldHint: false
|
|
5420
|
+
},
|
|
5254
5421
|
async () => {
|
|
5255
5422
|
const lines = [`${COMPOSITIONS.length} Remotion compositions available:`, ""];
|
|
5256
5423
|
for (const comp of COMPOSITIONS) {
|
|
@@ -5279,6 +5446,13 @@ function registerRemotionTools(server) {
|
|
|
5279
5446
|
"JSON string of input props to pass to the composition. Each composition accepts different props. Omit for defaults."
|
|
5280
5447
|
)
|
|
5281
5448
|
},
|
|
5449
|
+
{
|
|
5450
|
+
title: "Render Demo Video",
|
|
5451
|
+
readOnlyHint: false,
|
|
5452
|
+
destructiveHint: false,
|
|
5453
|
+
idempotentHint: false,
|
|
5454
|
+
openWorldHint: false
|
|
5455
|
+
},
|
|
5282
5456
|
async ({ composition_id, output_format, props }) => {
|
|
5283
5457
|
const startedAt = Date.now();
|
|
5284
5458
|
const userId = await getDefaultUserId();
|
|
@@ -5459,6 +5633,13 @@ function registerInsightsTools(server) {
|
|
|
5459
5633
|
limit: z8.number().min(1).max(50).optional().describe("Maximum number of insights to return. Defaults to 10."),
|
|
5460
5634
|
response_format: z8.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
5461
5635
|
},
|
|
5636
|
+
{
|
|
5637
|
+
title: "Get Performance Insights",
|
|
5638
|
+
readOnlyHint: true,
|
|
5639
|
+
destructiveHint: false,
|
|
5640
|
+
idempotentHint: true,
|
|
5641
|
+
openWorldHint: false
|
|
5642
|
+
},
|
|
5462
5643
|
async ({ insight_type, days, limit, response_format }) => {
|
|
5463
5644
|
const format = response_format ?? "text";
|
|
5464
5645
|
const supabase = getSupabaseClient();
|
|
@@ -5586,6 +5767,13 @@ function registerInsightsTools(server) {
|
|
|
5586
5767
|
days: z8.number().min(1).max(90).optional().describe("Number of days to analyze. Defaults to 30. Max 90."),
|
|
5587
5768
|
response_format: z8.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
5588
5769
|
},
|
|
5770
|
+
{
|
|
5771
|
+
title: "Get Best Posting Times",
|
|
5772
|
+
readOnlyHint: true,
|
|
5773
|
+
destructiveHint: false,
|
|
5774
|
+
idempotentHint: true,
|
|
5775
|
+
openWorldHint: false
|
|
5776
|
+
},
|
|
5589
5777
|
async ({ platform: platform2, days, response_format }) => {
|
|
5590
5778
|
const format = response_format ?? "text";
|
|
5591
5779
|
const supabase = getSupabaseClient();
|
|
@@ -5734,6 +5922,13 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
5734
5922
|
video_id: z9.string().optional().describe('YouTube video ID. Required when action is "video".'),
|
|
5735
5923
|
max_results: z9.number().min(1).max(50).optional().describe('Max videos to return for "topVideos" action. Defaults to 10.')
|
|
5736
5924
|
},
|
|
5925
|
+
{
|
|
5926
|
+
title: "Fetch YouTube Analytics",
|
|
5927
|
+
readOnlyHint: true,
|
|
5928
|
+
destructiveHint: false,
|
|
5929
|
+
idempotentHint: true,
|
|
5930
|
+
openWorldHint: true
|
|
5931
|
+
},
|
|
5737
5932
|
async ({ action, start_date, end_date, video_id, max_results }) => {
|
|
5738
5933
|
if (action === "video" && !video_id) {
|
|
5739
5934
|
return {
|
|
@@ -5845,15 +6040,20 @@ function asEnvelope6(data) {
|
|
|
5845
6040
|
function registerCommentsTools(server) {
|
|
5846
6041
|
server.tool(
|
|
5847
6042
|
"list_comments",
|
|
5848
|
-
|
|
6043
|
+
'List YouTube comments \u2014 pass video_id (11-char string, e.g. "dQw4w9WgXcQ") for a specific video, or omit for recent comments across all channel videos. Returns comment text, author, like count, and reply count. Use page_token from previous response for pagination.',
|
|
5849
6044
|
{
|
|
5850
|
-
video_id: z10.string().optional().describe(
|
|
5851
|
-
"YouTube video ID. If omitted, returns comments across all channel videos."
|
|
5852
|
-
),
|
|
6045
|
+
video_id: z10.string().optional().describe('YouTube video ID \u2014 the 11-character string from the URL (e.g. "dQw4w9WgXcQ" from youtube.com/watch?v=dQw4w9WgXcQ). Omit to get recent comments across all channel videos.'),
|
|
5853
6046
|
max_results: z10.number().min(1).max(100).optional().describe("Maximum number of comments to return. Defaults to 50."),
|
|
5854
|
-
page_token: z10.string().optional().describe("Pagination
|
|
6047
|
+
page_token: z10.string().optional().describe("Pagination cursor from previous list_comments response nextPageToken field. Omit for first page of results."),
|
|
5855
6048
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
5856
6049
|
},
|
|
6050
|
+
{
|
|
6051
|
+
title: "List Comments",
|
|
6052
|
+
readOnlyHint: true,
|
|
6053
|
+
destructiveHint: false,
|
|
6054
|
+
idempotentHint: true,
|
|
6055
|
+
openWorldHint: true
|
|
6056
|
+
},
|
|
5857
6057
|
async ({ video_id, max_results, page_token, response_format }) => {
|
|
5858
6058
|
const format = response_format ?? "text";
|
|
5859
6059
|
const { data, error } = await callEdgeFunction("youtube-comments", {
|
|
@@ -5916,7 +6116,7 @@ function registerCommentsTools(server) {
|
|
|
5916
6116
|
);
|
|
5917
6117
|
server.tool(
|
|
5918
6118
|
"reply_to_comment",
|
|
5919
|
-
"Reply to a YouTube comment.
|
|
6119
|
+
"Reply to a YouTube comment. Get the parent_id from list_comments results. Reply appears as the authenticated channel. Use for community engagement after checking list_comments for questions or feedback.",
|
|
5920
6120
|
{
|
|
5921
6121
|
parent_id: z10.string().describe(
|
|
5922
6122
|
"The ID of the parent comment to reply to (from list_comments)."
|
|
@@ -5924,6 +6124,13 @@ function registerCommentsTools(server) {
|
|
|
5924
6124
|
text: z10.string().min(1).describe("The reply text."),
|
|
5925
6125
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
5926
6126
|
},
|
|
6127
|
+
{
|
|
6128
|
+
title: "Reply to Comment",
|
|
6129
|
+
readOnlyHint: false,
|
|
6130
|
+
destructiveHint: false,
|
|
6131
|
+
idempotentHint: false,
|
|
6132
|
+
openWorldHint: true
|
|
6133
|
+
},
|
|
5927
6134
|
async ({ parent_id, text, response_format }) => {
|
|
5928
6135
|
const format = response_format ?? "text";
|
|
5929
6136
|
const startedAt = Date.now();
|
|
@@ -6005,6 +6212,13 @@ function registerCommentsTools(server) {
|
|
|
6005
6212
|
text: z10.string().min(1).describe("The comment text."),
|
|
6006
6213
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6007
6214
|
},
|
|
6215
|
+
{
|
|
6216
|
+
title: "Post Comment",
|
|
6217
|
+
readOnlyHint: false,
|
|
6218
|
+
destructiveHint: false,
|
|
6219
|
+
idempotentHint: false,
|
|
6220
|
+
openWorldHint: true
|
|
6221
|
+
},
|
|
6008
6222
|
async ({ video_id, text, response_format }) => {
|
|
6009
6223
|
const format = response_format ?? "text";
|
|
6010
6224
|
const startedAt = Date.now();
|
|
@@ -6083,6 +6297,13 @@ function registerCommentsTools(server) {
|
|
|
6083
6297
|
moderation_status: z10.enum(["published", "rejected"]).describe('"published" to approve, "rejected" to hide.'),
|
|
6084
6298
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6085
6299
|
},
|
|
6300
|
+
{
|
|
6301
|
+
title: "Moderate Comment",
|
|
6302
|
+
readOnlyHint: false,
|
|
6303
|
+
destructiveHint: false,
|
|
6304
|
+
idempotentHint: true,
|
|
6305
|
+
openWorldHint: true
|
|
6306
|
+
},
|
|
6086
6307
|
async ({ comment_id, moderation_status, response_format }) => {
|
|
6087
6308
|
const format = response_format ?? "text";
|
|
6088
6309
|
const startedAt = Date.now();
|
|
@@ -6168,6 +6389,13 @@ function registerCommentsTools(server) {
|
|
|
6168
6389
|
comment_id: z10.string().describe("The comment ID to delete."),
|
|
6169
6390
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6170
6391
|
},
|
|
6392
|
+
{
|
|
6393
|
+
title: "Delete Comment",
|
|
6394
|
+
readOnlyHint: false,
|
|
6395
|
+
destructiveHint: true,
|
|
6396
|
+
idempotentHint: true,
|
|
6397
|
+
openWorldHint: true
|
|
6398
|
+
},
|
|
6171
6399
|
async ({ comment_id, response_format }) => {
|
|
6172
6400
|
const format = response_format ?? "text";
|
|
6173
6401
|
const startedAt = Date.now();
|
|
@@ -6316,12 +6544,19 @@ function asEnvelope7(data) {
|
|
|
6316
6544
|
function registerIdeationContextTools(server) {
|
|
6317
6545
|
server.tool(
|
|
6318
6546
|
"get_ideation_context",
|
|
6319
|
-
"
|
|
6547
|
+
"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.",
|
|
6320
6548
|
{
|
|
6321
6549
|
project_id: z11.string().uuid().optional().describe("Project ID to scope insights."),
|
|
6322
6550
|
days: z11.number().min(1).max(90).optional().describe("Lookback window for insights. Defaults to 30 days."),
|
|
6323
6551
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional output format. Defaults to text.")
|
|
6324
6552
|
},
|
|
6553
|
+
{
|
|
6554
|
+
title: "Get Ideation Context",
|
|
6555
|
+
readOnlyHint: true,
|
|
6556
|
+
destructiveHint: false,
|
|
6557
|
+
idempotentHint: true,
|
|
6558
|
+
openWorldHint: false
|
|
6559
|
+
},
|
|
6325
6560
|
async ({ project_id, days, response_format }) => {
|
|
6326
6561
|
const supabase = getSupabaseClient();
|
|
6327
6562
|
const userId = await getDefaultUserId();
|
|
@@ -6447,10 +6682,17 @@ function asEnvelope8(data) {
|
|
|
6447
6682
|
function registerCreditsTools(server) {
|
|
6448
6683
|
server.tool(
|
|
6449
6684
|
"get_credit_balance",
|
|
6450
|
-
"
|
|
6685
|
+
"Check remaining credits, monthly limit, spending cap, and plan tier. Call this before expensive operations \u2014 generate_video costs 15-80 credits, generate_image costs 2-10. Returns current balance, monthly allocation, and spending cap (2.5x allocation).",
|
|
6451
6686
|
{
|
|
6452
6687
|
response_format: z12.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6453
6688
|
},
|
|
6689
|
+
{
|
|
6690
|
+
title: "Get Credit Balance",
|
|
6691
|
+
readOnlyHint: true,
|
|
6692
|
+
destructiveHint: false,
|
|
6693
|
+
idempotentHint: true,
|
|
6694
|
+
openWorldHint: false
|
|
6695
|
+
},
|
|
6454
6696
|
async ({ response_format }) => {
|
|
6455
6697
|
const supabase = getSupabaseClient();
|
|
6456
6698
|
const userId = await getDefaultUserId();
|
|
@@ -6500,10 +6742,17 @@ Monthly used: ${payload.monthlyUsed}` + (payload.monthlyLimit ? ` / ${payload.mo
|
|
|
6500
6742
|
);
|
|
6501
6743
|
server.tool(
|
|
6502
6744
|
"get_budget_status",
|
|
6503
|
-
"
|
|
6745
|
+
"Check how much of the per-session budget has been consumed. Tracks credits spent and assets created in this MCP session against configured limits. Use to avoid hitting budget caps mid-workflow.",
|
|
6504
6746
|
{
|
|
6505
6747
|
response_format: z12.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6506
6748
|
},
|
|
6749
|
+
{
|
|
6750
|
+
title: "Get Budget Status",
|
|
6751
|
+
readOnlyHint: true,
|
|
6752
|
+
destructiveHint: false,
|
|
6753
|
+
idempotentHint: true,
|
|
6754
|
+
openWorldHint: false
|
|
6755
|
+
},
|
|
6507
6756
|
async ({ response_format }) => {
|
|
6508
6757
|
const budget = getCurrentBudgetStatus();
|
|
6509
6758
|
const payload = {
|
|
@@ -6557,11 +6806,18 @@ function asEnvelope9(data) {
|
|
|
6557
6806
|
function registerLoopSummaryTools(server) {
|
|
6558
6807
|
server.tool(
|
|
6559
6808
|
"get_loop_summary",
|
|
6560
|
-
"Get a
|
|
6809
|
+
"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.",
|
|
6561
6810
|
{
|
|
6562
6811
|
project_id: z13.string().uuid().optional().describe("Project ID. Defaults to active project context."),
|
|
6563
6812
|
response_format: z13.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6564
6813
|
},
|
|
6814
|
+
{
|
|
6815
|
+
title: "Get Loop Summary",
|
|
6816
|
+
readOnlyHint: true,
|
|
6817
|
+
destructiveHint: false,
|
|
6818
|
+
idempotentHint: true,
|
|
6819
|
+
openWorldHint: false
|
|
6820
|
+
},
|
|
6565
6821
|
async ({ project_id, response_format }) => {
|
|
6566
6822
|
const supabase = getSupabaseClient();
|
|
6567
6823
|
const userId = await getDefaultUserId();
|
|
@@ -6659,6 +6915,13 @@ function registerUsageTools(server) {
|
|
|
6659
6915
|
{
|
|
6660
6916
|
response_format: z14.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6661
6917
|
},
|
|
6918
|
+
{
|
|
6919
|
+
title: "Get MCP Usage",
|
|
6920
|
+
readOnlyHint: true,
|
|
6921
|
+
destructiveHint: false,
|
|
6922
|
+
idempotentHint: true,
|
|
6923
|
+
openWorldHint: false
|
|
6924
|
+
},
|
|
6662
6925
|
async ({ response_format }) => {
|
|
6663
6926
|
const format = response_format ?? "text";
|
|
6664
6927
|
const supabase = getSupabaseClient();
|
|
@@ -6743,11 +7006,18 @@ function asEnvelope11(data) {
|
|
|
6743
7006
|
function registerAutopilotTools(server) {
|
|
6744
7007
|
server.tool(
|
|
6745
7008
|
"list_autopilot_configs",
|
|
6746
|
-
"List
|
|
7009
|
+
"List autopilot configurations showing schedules, credit budgets, last run times, and active/inactive status. Use to check what is automated before creating new configs, or to find config_id for update_autopilot_config.",
|
|
6747
7010
|
{
|
|
6748
7011
|
active_only: z15.boolean().optional().describe("If true, only return active configs. Defaults to false (show all)."),
|
|
6749
7012
|
response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6750
7013
|
},
|
|
7014
|
+
{
|
|
7015
|
+
title: "List Autopilot Configs",
|
|
7016
|
+
readOnlyHint: true,
|
|
7017
|
+
destructiveHint: false,
|
|
7018
|
+
idempotentHint: true,
|
|
7019
|
+
openWorldHint: false
|
|
7020
|
+
},
|
|
6751
7021
|
async ({ active_only, response_format }) => {
|
|
6752
7022
|
const format = response_format ?? "text";
|
|
6753
7023
|
const supabase = getSupabaseClient();
|
|
@@ -6825,11 +7095,18 @@ ${"=".repeat(40)}
|
|
|
6825
7095
|
{
|
|
6826
7096
|
config_id: z15.string().uuid().describe("The autopilot config ID to update."),
|
|
6827
7097
|
is_active: z15.boolean().optional().describe("Enable or disable this autopilot config."),
|
|
6828
|
-
schedule_days: z15.array(z15.enum(["mon", "tue", "wed", "thu", "fri", "sat", "sun"])).optional().describe('Days of the week to run (e.g
|
|
7098
|
+
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"]).'),
|
|
6829
7099
|
schedule_time: z15.string().optional().describe('Time to run in HH:MM format (24h, user timezone). E.g., "09:00".'),
|
|
6830
7100
|
max_credits_per_run: z15.number().optional().describe("Maximum credits per execution."),
|
|
6831
7101
|
max_credits_per_week: z15.number().optional().describe("Maximum credits per week.")
|
|
6832
7102
|
},
|
|
7103
|
+
{
|
|
7104
|
+
title: "Update Autopilot Config",
|
|
7105
|
+
readOnlyHint: false,
|
|
7106
|
+
destructiveHint: false,
|
|
7107
|
+
idempotentHint: true,
|
|
7108
|
+
openWorldHint: false
|
|
7109
|
+
},
|
|
6833
7110
|
async ({
|
|
6834
7111
|
config_id,
|
|
6835
7112
|
is_active,
|
|
@@ -6889,10 +7166,17 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
|
|
|
6889
7166
|
);
|
|
6890
7167
|
server.tool(
|
|
6891
7168
|
"get_autopilot_status",
|
|
6892
|
-
"Get
|
|
7169
|
+
"Get autopilot system overview: active config count, recent execution results, credits consumed, and next scheduled run time. Use as a dashboard check before modifying autopilot settings.",
|
|
6893
7170
|
{
|
|
6894
7171
|
response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6895
7172
|
},
|
|
7173
|
+
{
|
|
7174
|
+
title: "Get Autopilot Status",
|
|
7175
|
+
readOnlyHint: true,
|
|
7176
|
+
destructiveHint: false,
|
|
7177
|
+
idempotentHint: true,
|
|
7178
|
+
openWorldHint: false
|
|
7179
|
+
},
|
|
6896
7180
|
async ({ response_format }) => {
|
|
6897
7181
|
const format = response_format ?? "text";
|
|
6898
7182
|
const supabase = getSupabaseClient();
|
|
@@ -7006,13 +7290,20 @@ ${content.suggested_hooks.map((h) => ` - ${h}`).join("\n")}`
|
|
|
7006
7290
|
function registerExtractionTools(server) {
|
|
7007
7291
|
server.tool(
|
|
7008
7292
|
"extract_url_content",
|
|
7009
|
-
"Extract content from
|
|
7293
|
+
"Extract text content from any URL \u2014 YouTube video transcripts, article text, or product page features/benefits/USP. YouTube URLs auto-route to transcript extraction with optional comments. Use before generate_content to repurpose existing content, or before plan_content_week to base a content plan on a source URL.",
|
|
7010
7294
|
{
|
|
7011
7295
|
url: z16.string().url().describe("URL to extract content from"),
|
|
7012
7296
|
extract_type: z16.enum(["auto", "transcript", "article", "product"]).default("auto").describe("Type of extraction"),
|
|
7013
7297
|
include_comments: z16.boolean().default(false).describe("Include top comments (YouTube only)"),
|
|
7014
7298
|
max_results: z16.number().min(1).max(100).default(10).describe("Max comments to include"),
|
|
7015
|
-
response_format: z16.enum(["text", "json"]).default("text")
|
|
7299
|
+
response_format: z16.enum(["text", "json"]).default("text").describe("Response format. Defaults to text.")
|
|
7300
|
+
},
|
|
7301
|
+
{
|
|
7302
|
+
title: "Extract URL Content",
|
|
7303
|
+
readOnlyHint: true,
|
|
7304
|
+
destructiveHint: false,
|
|
7305
|
+
idempotentHint: true,
|
|
7306
|
+
openWorldHint: true
|
|
7016
7307
|
},
|
|
7017
7308
|
async ({
|
|
7018
7309
|
url,
|
|
@@ -7204,9 +7495,9 @@ function asEnvelope13(data) {
|
|
|
7204
7495
|
function registerQualityTools(server) {
|
|
7205
7496
|
server.tool(
|
|
7206
7497
|
"quality_check",
|
|
7207
|
-
"Score
|
|
7498
|
+
"Score post quality across 7 categories: Hook Strength, Message Clarity, Platform Fit, Brand Alignment, Novelty, CTA Strength, and Safety/Claims. Each scored 0-5, total 35. Default pass threshold is 26 (~75%). Run after generate_content and before schedule_post. Include hashtags in caption if they will be published \u2014 they affect Platform Fit and Safety scores.",
|
|
7208
7499
|
{
|
|
7209
|
-
caption: z17.string().describe("
|
|
7500
|
+
caption: z17.string().describe("The post text to score. Include hashtags if they will be published \u2014 they affect Platform Fit and Safety/Claims scores."),
|
|
7210
7501
|
title: z17.string().optional().describe("Post title (important for YouTube)"),
|
|
7211
7502
|
platforms: z17.array(
|
|
7212
7503
|
z17.enum([
|
|
@@ -7220,11 +7511,18 @@ function registerQualityTools(server) {
|
|
|
7220
7511
|
"bluesky"
|
|
7221
7512
|
])
|
|
7222
7513
|
).min(1).describe("Target platforms"),
|
|
7223
|
-
threshold: z17.number().min(0).max(35).default(26).describe("Minimum total score to pass"),
|
|
7514
|
+
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."),
|
|
7224
7515
|
brand_keyword: z17.string().optional().describe("Brand keyword for alignment check"),
|
|
7225
|
-
brand_avoid_patterns: z17.array(z17.string()).optional(),
|
|
7226
|
-
custom_banned_terms: z17.array(z17.string()).optional(),
|
|
7227
|
-
response_format: z17.enum(["text", "json"]).default("text")
|
|
7516
|
+
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."),
|
|
7517
|
+
custom_banned_terms: z17.array(z17.string()).optional().describe("Additional banned words beyond the built-in safety list. Useful for industry-specific compliance terms."),
|
|
7518
|
+
response_format: z17.enum(["text", "json"]).default("text").describe("'text' for human-readable report, 'json' for structured scores suitable for pipeline automation.")
|
|
7519
|
+
},
|
|
7520
|
+
{
|
|
7521
|
+
title: "Quality Check",
|
|
7522
|
+
readOnlyHint: true,
|
|
7523
|
+
destructiveHint: false,
|
|
7524
|
+
idempotentHint: true,
|
|
7525
|
+
openWorldHint: false
|
|
7228
7526
|
},
|
|
7229
7527
|
async ({
|
|
7230
7528
|
caption,
|
|
@@ -7291,20 +7589,27 @@ function registerQualityTools(server) {
|
|
|
7291
7589
|
);
|
|
7292
7590
|
server.tool(
|
|
7293
7591
|
"quality_check_plan",
|
|
7294
|
-
"
|
|
7592
|
+
"Batch quality check all posts in a content plan. Returns per-post scores and aggregate pass/fail summary. Use after plan_content_week and before schedule_content_plan to catch low-quality posts before publishing.",
|
|
7295
7593
|
{
|
|
7296
7594
|
plan: z17.object({
|
|
7297
7595
|
posts: z17.array(
|
|
7298
7596
|
z17.object({
|
|
7299
|
-
id: z17.string(),
|
|
7300
|
-
caption: z17.string(),
|
|
7301
|
-
title: z17.string().optional(),
|
|
7302
|
-
platform: z17.string()
|
|
7597
|
+
id: z17.string().describe("Unique post identifier."),
|
|
7598
|
+
caption: z17.string().describe("Post caption/body text to quality-check."),
|
|
7599
|
+
title: z17.string().optional().describe("Post title (important for YouTube)."),
|
|
7600
|
+
platform: z17.string().describe("Target platform (e.g. instagram, youtube).")
|
|
7303
7601
|
})
|
|
7304
7602
|
)
|
|
7305
|
-
}).passthrough().describe("Content plan with posts array"),
|
|
7306
|
-
threshold: z17.number().min(0).max(35).default(26).describe("Minimum total score to pass"),
|
|
7307
|
-
response_format: z17.enum(["text", "json"]).default("text")
|
|
7603
|
+
}).passthrough().describe("Content plan with posts array."),
|
|
7604
|
+
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."),
|
|
7605
|
+
response_format: z17.enum(["text", "json"]).default("text").describe("Response format. Defaults to text.")
|
|
7606
|
+
},
|
|
7607
|
+
{
|
|
7608
|
+
title: "Quality Check Plan",
|
|
7609
|
+
readOnlyHint: true,
|
|
7610
|
+
destructiveHint: false,
|
|
7611
|
+
idempotentHint: true,
|
|
7612
|
+
openWorldHint: false
|
|
7308
7613
|
},
|
|
7309
7614
|
async ({ plan, threshold, response_format }) => {
|
|
7310
7615
|
const startedAt = Date.now();
|
|
@@ -7484,7 +7789,7 @@ function formatPlanAsText(plan) {
|
|
|
7484
7789
|
function registerPlanningTools(server) {
|
|
7485
7790
|
server.tool(
|
|
7486
7791
|
"plan_content_week",
|
|
7487
|
-
"Generate a full
|
|
7792
|
+
"Generate a full content plan with platform-specific drafts, hooks, angles, and optimal schedule times. Pass a topic or source_url \u2014 brand context and performance insights auto-load via project_id. Output feeds directly into quality_check_plan then schedule_content_plan. Costs ~5-15 credits depending on post count.",
|
|
7488
7793
|
{
|
|
7489
7794
|
topic: z18.string().describe("Main topic or content theme"),
|
|
7490
7795
|
source_url: z18.string().optional().describe("URL to extract content from (YouTube, article)"),
|
|
@@ -7505,7 +7810,14 @@ function registerPlanningTools(server) {
|
|
|
7505
7810
|
start_date: z18.string().optional().describe("ISO date, defaults to tomorrow"),
|
|
7506
7811
|
brand_voice: z18.string().optional().describe("Override brand voice description"),
|
|
7507
7812
|
project_id: z18.string().optional().describe("Project ID for brand/insights context"),
|
|
7508
|
-
response_format: z18.enum(["text", "json"]).default("json")
|
|
7813
|
+
response_format: z18.enum(["text", "json"]).default("json").describe("Response format. Defaults to json.")
|
|
7814
|
+
},
|
|
7815
|
+
{
|
|
7816
|
+
title: "Plan Content Week",
|
|
7817
|
+
readOnlyHint: false,
|
|
7818
|
+
destructiveHint: false,
|
|
7819
|
+
idempotentHint: false,
|
|
7820
|
+
openWorldHint: true
|
|
7509
7821
|
},
|
|
7510
7822
|
async ({
|
|
7511
7823
|
topic,
|
|
@@ -7820,15 +8132,22 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7820
8132
|
);
|
|
7821
8133
|
server.tool(
|
|
7822
8134
|
"save_content_plan",
|
|
7823
|
-
"
|
|
8135
|
+
"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.",
|
|
7824
8136
|
{
|
|
7825
8137
|
plan: z18.object({
|
|
7826
|
-
topic: z18.string(),
|
|
7827
|
-
posts: z18.array(z18.record(z18.string(), z18.unknown()))
|
|
7828
|
-
}).passthrough(),
|
|
7829
|
-
project_id: z18.string().uuid().optional(),
|
|
7830
|
-
status: z18.enum(["draft", "in_review", "approved", "scheduled", "completed"]).default("draft"),
|
|
7831
|
-
response_format: z18.enum(["text", "json"]).default("json")
|
|
8138
|
+
topic: z18.string().describe("Content plan topic or theme."),
|
|
8139
|
+
posts: z18.array(z18.record(z18.string(), z18.unknown())).describe("Array of post objects to save.")
|
|
8140
|
+
}).passthrough().describe("Content plan object with topic and posts array."),
|
|
8141
|
+
project_id: z18.string().uuid().optional().describe("Project ID. Defaults to active project context."),
|
|
8142
|
+
status: z18.enum(["draft", "in_review", "approved", "scheduled", "completed"]).default("draft").describe("Initial plan status. Defaults to draft."),
|
|
8143
|
+
response_format: z18.enum(["text", "json"]).default("json").describe("Response format. Defaults to json.")
|
|
8144
|
+
},
|
|
8145
|
+
{
|
|
8146
|
+
title: "Save Content Plan",
|
|
8147
|
+
readOnlyHint: false,
|
|
8148
|
+
destructiveHint: false,
|
|
8149
|
+
idempotentHint: false,
|
|
8150
|
+
openWorldHint: false
|
|
7832
8151
|
},
|
|
7833
8152
|
async ({ plan, project_id, status, response_format }) => {
|
|
7834
8153
|
const startedAt = Date.now();
|
|
@@ -7926,10 +8245,17 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7926
8245
|
);
|
|
7927
8246
|
server.tool(
|
|
7928
8247
|
"get_content_plan",
|
|
7929
|
-
"Retrieve a
|
|
8248
|
+
"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.",
|
|
7930
8249
|
{
|
|
7931
8250
|
plan_id: z18.string().uuid().describe("Persisted content plan ID"),
|
|
7932
|
-
response_format: z18.enum(["text", "json"]).default("json")
|
|
8251
|
+
response_format: z18.enum(["text", "json"]).default("json").describe("Response format. Defaults to json.")
|
|
8252
|
+
},
|
|
8253
|
+
{
|
|
8254
|
+
title: "Get Content Plan",
|
|
8255
|
+
readOnlyHint: true,
|
|
8256
|
+
destructiveHint: false,
|
|
8257
|
+
idempotentHint: true,
|
|
8258
|
+
openWorldHint: false
|
|
7933
8259
|
},
|
|
7934
8260
|
async ({ plan_id, response_format }) => {
|
|
7935
8261
|
const supabase = getSupabaseClient();
|
|
@@ -7994,25 +8320,32 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7994
8320
|
);
|
|
7995
8321
|
server.tool(
|
|
7996
8322
|
"update_content_plan",
|
|
7997
|
-
"
|
|
8323
|
+
"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.",
|
|
7998
8324
|
{
|
|
7999
|
-
plan_id: z18.string().uuid(),
|
|
8325
|
+
plan_id: z18.string().uuid().describe("Content plan ID to update."),
|
|
8000
8326
|
post_updates: z18.array(
|
|
8001
8327
|
z18.object({
|
|
8002
|
-
post_id: z18.string(),
|
|
8003
|
-
caption: z18.string().optional(),
|
|
8004
|
-
title: z18.string().optional(),
|
|
8005
|
-
hashtags: z18.array(z18.string()).optional(),
|
|
8006
|
-
hook: z18.string().optional(),
|
|
8007
|
-
angle: z18.string().optional(),
|
|
8008
|
-
visual_direction: z18.string().optional(),
|
|
8009
|
-
media_url: z18.string().optional(),
|
|
8010
|
-
schedule_at: z18.string().optional(),
|
|
8011
|
-
platform: z18.string().optional(),
|
|
8012
|
-
status: z18.enum(["approved", "rejected", "needs_edit"]).optional()
|
|
8328
|
+
post_id: z18.string().describe("ID of the post to update within this plan."),
|
|
8329
|
+
caption: z18.string().optional().describe("Revised caption/body text."),
|
|
8330
|
+
title: z18.string().optional().describe("Revised post title."),
|
|
8331
|
+
hashtags: z18.array(z18.string()).optional().describe("Revised hashtags array."),
|
|
8332
|
+
hook: z18.string().optional().describe("Revised attention-grabbing opening line."),
|
|
8333
|
+
angle: z18.string().optional().describe("Revised content angle or perspective."),
|
|
8334
|
+
visual_direction: z18.string().optional().describe("Revised visual/media direction notes."),
|
|
8335
|
+
media_url: z18.string().optional().describe("Revised media URL (public or R2 signed URL)."),
|
|
8336
|
+
schedule_at: z18.string().optional().describe("Revised ISO 8601 UTC publish datetime."),
|
|
8337
|
+
platform: z18.string().optional().describe("Revised target platform."),
|
|
8338
|
+
status: z18.enum(["approved", "rejected", "needs_edit"]).optional().describe("Review status for this post.")
|
|
8013
8339
|
})
|
|
8014
|
-
).min(1),
|
|
8015
|
-
response_format: z18.enum(["text", "json"]).default("json")
|
|
8340
|
+
).min(1).describe("Array of post-level updates to apply."),
|
|
8341
|
+
response_format: z18.enum(["text", "json"]).default("json").describe("Response format. Defaults to json.")
|
|
8342
|
+
},
|
|
8343
|
+
{
|
|
8344
|
+
title: "Update Content Plan",
|
|
8345
|
+
readOnlyHint: false,
|
|
8346
|
+
destructiveHint: false,
|
|
8347
|
+
idempotentHint: true,
|
|
8348
|
+
openWorldHint: false
|
|
8016
8349
|
},
|
|
8017
8350
|
async ({ plan_id, post_updates, response_format }) => {
|
|
8018
8351
|
const supabase = getSupabaseClient();
|
|
@@ -8113,10 +8446,17 @@ ${rawText.slice(0, 1e3)}`
|
|
|
8113
8446
|
);
|
|
8114
8447
|
server.tool(
|
|
8115
8448
|
"submit_content_plan_for_approval",
|
|
8116
|
-
"
|
|
8449
|
+
"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.",
|
|
8117
8450
|
{
|
|
8118
|
-
plan_id: z18.string().uuid(),
|
|
8119
|
-
response_format: z18.enum(["text", "json"]).default("json")
|
|
8451
|
+
plan_id: z18.string().uuid().describe("Content plan ID to submit for review."),
|
|
8452
|
+
response_format: z18.enum(["text", "json"]).default("json").describe("Response format. Defaults to json.")
|
|
8453
|
+
},
|
|
8454
|
+
{
|
|
8455
|
+
title: "Submit Plan for Approval",
|
|
8456
|
+
readOnlyHint: false,
|
|
8457
|
+
destructiveHint: false,
|
|
8458
|
+
idempotentHint: true,
|
|
8459
|
+
openWorldHint: false
|
|
8120
8460
|
},
|
|
8121
8461
|
async ({ plan_id, response_format }) => {
|
|
8122
8462
|
const supabase = getSupabaseClient();
|
|
@@ -8240,21 +8580,28 @@ async function assertProjectAccess(supabase, userId, projectId) {
|
|
|
8240
8580
|
function registerPlanApprovalTools(server) {
|
|
8241
8581
|
server.tool(
|
|
8242
8582
|
"create_plan_approvals",
|
|
8243
|
-
"Create
|
|
8583
|
+
"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.",
|
|
8244
8584
|
{
|
|
8245
8585
|
plan_id: z19.string().uuid().describe("Content plan ID"),
|
|
8246
8586
|
posts: z19.array(
|
|
8247
8587
|
z19.object({
|
|
8248
|
-
id: z19.string(),
|
|
8249
|
-
platform: z19.string().optional(),
|
|
8250
|
-
caption: z19.string().optional(),
|
|
8251
|
-
title: z19.string().optional(),
|
|
8252
|
-
media_url: z19.string().optional(),
|
|
8253
|
-
schedule_at: z19.string().optional()
|
|
8588
|
+
id: z19.string().describe("Unique post identifier from the content plan."),
|
|
8589
|
+
platform: z19.string().optional().describe("Target platform (e.g. instagram, youtube)."),
|
|
8590
|
+
caption: z19.string().optional().describe("Post caption/body text."),
|
|
8591
|
+
title: z19.string().optional().describe("Post title, used by YouTube and LinkedIn articles."),
|
|
8592
|
+
media_url: z19.string().optional().describe("Public or R2 signed URL for the post media."),
|
|
8593
|
+
schedule_at: z19.string().optional().describe("ISO 8601 UTC datetime to publish (e.g. 2026-03-20T14:00:00Z).")
|
|
8254
8594
|
}).passthrough()
|
|
8255
8595
|
).min(1).describe("Posts to create approval entries for."),
|
|
8256
8596
|
project_id: z19.string().uuid().optional().describe("Project ID. Defaults to active project context."),
|
|
8257
|
-
response_format: z19.enum(["text", "json"]).optional()
|
|
8597
|
+
response_format: z19.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
8598
|
+
},
|
|
8599
|
+
{
|
|
8600
|
+
title: "Create Plan Approvals",
|
|
8601
|
+
readOnlyHint: false,
|
|
8602
|
+
destructiveHint: false,
|
|
8603
|
+
idempotentHint: false,
|
|
8604
|
+
openWorldHint: false
|
|
8258
8605
|
},
|
|
8259
8606
|
async ({ plan_id, posts, project_id, response_format }) => {
|
|
8260
8607
|
const supabase = getSupabaseClient();
|
|
@@ -8334,8 +8681,15 @@ function registerPlanApprovalTools(server) {
|
|
|
8334
8681
|
"List MCP-native approval items for a specific content plan.",
|
|
8335
8682
|
{
|
|
8336
8683
|
plan_id: z19.string().uuid().describe("Content plan ID"),
|
|
8337
|
-
status: z19.enum(["pending", "approved", "rejected", "edited"]).optional(),
|
|
8338
|
-
response_format: z19.enum(["text", "json"]).optional()
|
|
8684
|
+
status: z19.enum(["pending", "approved", "rejected", "edited"]).optional().describe("Filter approvals by status. Omit to return all statuses."),
|
|
8685
|
+
response_format: z19.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
8686
|
+
},
|
|
8687
|
+
{
|
|
8688
|
+
title: "List Plan Approvals",
|
|
8689
|
+
readOnlyHint: true,
|
|
8690
|
+
destructiveHint: false,
|
|
8691
|
+
idempotentHint: true,
|
|
8692
|
+
openWorldHint: false
|
|
8339
8693
|
},
|
|
8340
8694
|
async ({ plan_id, status, response_format }) => {
|
|
8341
8695
|
const supabase = getSupabaseClient();
|
|
@@ -8402,10 +8756,19 @@ function registerPlanApprovalTools(server) {
|
|
|
8402
8756
|
"Approve, reject, or edit a pending plan approval item.",
|
|
8403
8757
|
{
|
|
8404
8758
|
approval_id: z19.string().uuid().describe("Approval item ID"),
|
|
8405
|
-
decision: z19.enum(["approved", "rejected", "edited"]),
|
|
8406
|
-
edited_post: z19.record(z19.string(), z19.unknown()).optional()
|
|
8407
|
-
|
|
8408
|
-
|
|
8759
|
+
decision: z19.enum(["approved", "rejected", "edited"]).describe("Approval decision for this post."),
|
|
8760
|
+
edited_post: z19.record(z19.string(), z19.unknown()).optional().describe(
|
|
8761
|
+
'Revised post fields when decision is "edited" (e.g. {caption: "...", hashtags: [...]}).'
|
|
8762
|
+
),
|
|
8763
|
+
reason: z19.string().max(1e3).optional().describe("Optional reason for the decision, visible to the plan author."),
|
|
8764
|
+
response_format: z19.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
8765
|
+
},
|
|
8766
|
+
{
|
|
8767
|
+
title: "Respond to Plan Approval",
|
|
8768
|
+
readOnlyHint: false,
|
|
8769
|
+
destructiveHint: false,
|
|
8770
|
+
idempotentHint: true,
|
|
8771
|
+
openWorldHint: false
|
|
8409
8772
|
},
|
|
8410
8773
|
async ({ approval_id, decision, edited_post, reason, response_format }) => {
|
|
8411
8774
|
const supabase = getSupabaseClient();
|
|
@@ -8833,7 +9196,7 @@ function searchTools(query) {
|
|
|
8833
9196
|
function registerDiscoveryTools(server) {
|
|
8834
9197
|
server.tool(
|
|
8835
9198
|
"search_tools",
|
|
8836
|
-
'Search
|
|
9199
|
+
'Search available tools by name, description, module, or scope. Use "name" detail (~50 tokens) for quick lookup, "summary" (~500 tokens) for descriptions, "full" for complete input schemas. Start here if unsure which tool to call \u2014 filter by module (e.g. "planning", "content", "analytics") to narrow results.',
|
|
8837
9200
|
{
|
|
8838
9201
|
query: z20.string().optional().describe("Search query to filter tools by name or description"),
|
|
8839
9202
|
module: z20.string().optional().describe('Filter by module name (e.g. "planning", "content", "analytics")'),
|
|
@@ -8842,6 +9205,13 @@ function registerDiscoveryTools(server) {
|
|
|
8842
9205
|
'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
|
|
8843
9206
|
)
|
|
8844
9207
|
},
|
|
9208
|
+
{
|
|
9209
|
+
title: "Search Tools",
|
|
9210
|
+
readOnlyHint: true,
|
|
9211
|
+
destructiveHint: false,
|
|
9212
|
+
idempotentHint: true,
|
|
9213
|
+
openWorldHint: false
|
|
9214
|
+
},
|
|
8845
9215
|
async ({ query, module, scope, detail }) => {
|
|
8846
9216
|
let results = [...TOOL_CATALOG];
|
|
8847
9217
|
if (query) {
|
|
@@ -8960,7 +9330,7 @@ function getJWKS(supabaseUrl) {
|
|
|
8960
9330
|
return jwks;
|
|
8961
9331
|
}
|
|
8962
9332
|
var apiKeyCache = /* @__PURE__ */ new Map();
|
|
8963
|
-
var API_KEY_CACHE_TTL_MS =
|
|
9333
|
+
var API_KEY_CACHE_TTL_MS = 1e4;
|
|
8964
9334
|
function createTokenVerifier(options) {
|
|
8965
9335
|
const { supabaseUrl, supabaseAnonKey } = options;
|
|
8966
9336
|
return {
|
|
@@ -9061,7 +9431,30 @@ var PORT = parseInt(process.env.PORT ?? "8080", 10);
|
|
|
9061
9431
|
var SUPABASE_URL2 = process.env.SUPABASE_URL ?? "";
|
|
9062
9432
|
var SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY ?? "";
|
|
9063
9433
|
var MCP_SERVER_URL = process.env.MCP_SERVER_URL ?? `http://localhost:${PORT}/mcp`;
|
|
9434
|
+
var APP_BASE_URL = process.env.APP_BASE_URL ?? "https://www.socialneuron.com";
|
|
9064
9435
|
var NODE_ENV = process.env.NODE_ENV ?? "development";
|
|
9436
|
+
function deriveOAuthIssuerUrl() {
|
|
9437
|
+
if (process.env.OAUTH_ISSUER_URL) {
|
|
9438
|
+
return process.env.OAUTH_ISSUER_URL;
|
|
9439
|
+
}
|
|
9440
|
+
try {
|
|
9441
|
+
const mcpUrl = new URL(MCP_SERVER_URL);
|
|
9442
|
+
const isLocalhost = mcpUrl.hostname === "localhost" || mcpUrl.hostname === "127.0.0.1";
|
|
9443
|
+
if (isLocalhost) {
|
|
9444
|
+
if (NODE_ENV === "development") {
|
|
9445
|
+
return `${mcpUrl.protocol}//${mcpUrl.host}`;
|
|
9446
|
+
}
|
|
9447
|
+
} else {
|
|
9448
|
+
return `${mcpUrl.protocol}//${mcpUrl.host}`;
|
|
9449
|
+
}
|
|
9450
|
+
} catch {
|
|
9451
|
+
}
|
|
9452
|
+
if (APP_BASE_URL && !APP_BASE_URL.includes("localhost")) {
|
|
9453
|
+
return APP_BASE_URL;
|
|
9454
|
+
}
|
|
9455
|
+
return "https://mcp.socialneuron.com";
|
|
9456
|
+
}
|
|
9457
|
+
var OAUTH_ISSUER_URL = deriveOAuthIssuerUrl();
|
|
9065
9458
|
if (!SUPABASE_URL2 || !SUPABASE_ANON_KEY) {
|
|
9066
9459
|
console.error("[MCP HTTP] Missing SUPABASE_URL or SUPABASE_ANON_KEY");
|
|
9067
9460
|
process.exit(1);
|