@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/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.
|
|
17
|
+
MCP_VERSION = "1.5.1";
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
20
|
|
|
@@ -400,7 +400,9 @@ async function getDefaultUserId() {
|
|
|
400
400
|
if (authenticatedUserId) return authenticatedUserId;
|
|
401
401
|
const envUserId = process.env.SOCIALNEURON_USER_ID;
|
|
402
402
|
if (envUserId) return envUserId;
|
|
403
|
-
throw new Error(
|
|
403
|
+
throw new Error(
|
|
404
|
+
"No user ID available. Set SOCIALNEURON_USER_ID or authenticate via API key."
|
|
405
|
+
);
|
|
404
406
|
}
|
|
405
407
|
async function getDefaultProjectId() {
|
|
406
408
|
const userId = await getDefaultUserId().catch(() => null);
|
|
@@ -444,7 +446,9 @@ async function initializeAuth() {
|
|
|
444
446
|
console.error("[MCP] Scopes: " + authenticatedScopes.join(", "));
|
|
445
447
|
if (authenticatedExpiresAt) {
|
|
446
448
|
const expiresMs = new Date(authenticatedExpiresAt).getTime();
|
|
447
|
-
const daysLeft = Math.ceil(
|
|
449
|
+
const daysLeft = Math.ceil(
|
|
450
|
+
(expiresMs - Date.now()) / (1e3 * 60 * 60 * 24)
|
|
451
|
+
);
|
|
448
452
|
console.error("[MCP] Key expires: " + authenticatedExpiresAt);
|
|
449
453
|
if (daysLeft <= 7) {
|
|
450
454
|
console.error(
|
|
@@ -467,8 +471,12 @@ async function initializeAuth() {
|
|
|
467
471
|
console.error(
|
|
468
472
|
"[MCP] \u26A0 DEPRECATED: Service role keys grant full admin access to your database."
|
|
469
473
|
);
|
|
470
|
-
console.error(
|
|
471
|
-
|
|
474
|
+
console.error(
|
|
475
|
+
"[MCP] Migrate to API key auth: npx @socialneuron/mcp-server setup"
|
|
476
|
+
);
|
|
477
|
+
console.error(
|
|
478
|
+
"[MCP] Then remove SOCIALNEURON_SERVICE_KEY from your environment."
|
|
479
|
+
);
|
|
472
480
|
if (!process.env.SOCIALNEURON_USER_ID) {
|
|
473
481
|
console.error(
|
|
474
482
|
"[MCP] Warning: SOCIALNEURON_USER_ID not set. Tools requiring a user will fail."
|
|
@@ -2947,7 +2955,9 @@ __export(setup_exports, {
|
|
|
2947
2955
|
runSetup: () => runSetup
|
|
2948
2956
|
});
|
|
2949
2957
|
import { createHash as createHash4, randomBytes, randomUUID as randomUUID3 } from "node:crypto";
|
|
2950
|
-
import {
|
|
2958
|
+
import {
|
|
2959
|
+
createServer
|
|
2960
|
+
} from "node:http";
|
|
2951
2961
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2952
2962
|
import { homedir as homedir3, platform as platform2 } from "node:os";
|
|
2953
2963
|
import { join as join3 } from "node:path";
|
|
@@ -2962,7 +2972,7 @@ function generatePKCE() {
|
|
|
2962
2972
|
return { codeVerifier, codeChallenge };
|
|
2963
2973
|
}
|
|
2964
2974
|
function getAppBaseUrl() {
|
|
2965
|
-
return process.env.SOCIALNEURON_APP_URL || "https://
|
|
2975
|
+
return process.env.SOCIALNEURON_APP_URL || "https://www.socialneuron.com";
|
|
2966
2976
|
}
|
|
2967
2977
|
function getDefaultSupabaseUrl() {
|
|
2968
2978
|
return process.env.SOCIALNEURON_SUPABASE_URL || process.env.SUPABASE_URL || CLOUD_SUPABASE_URL;
|
|
@@ -3033,11 +3043,14 @@ function readBody(req) {
|
|
|
3033
3043
|
async function completePkceExchange(codeVerifier, state) {
|
|
3034
3044
|
const supabaseUrl = getDefaultSupabaseUrl();
|
|
3035
3045
|
try {
|
|
3036
|
-
const response = await fetch(
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3046
|
+
const response = await fetch(
|
|
3047
|
+
`${supabaseUrl}/functions/v1/mcp-auth?action=exchange-key`,
|
|
3048
|
+
{
|
|
3049
|
+
method: "POST",
|
|
3050
|
+
headers: { "Content-Type": "application/json" },
|
|
3051
|
+
body: JSON.stringify({ code_verifier: codeVerifier, state })
|
|
3052
|
+
}
|
|
3053
|
+
);
|
|
3041
3054
|
if (!response.ok) {
|
|
3042
3055
|
const text = await response.text();
|
|
3043
3056
|
console.error(` PKCE exchange failed: ${text}`);
|
|
@@ -3046,7 +3059,9 @@ async function completePkceExchange(codeVerifier, state) {
|
|
|
3046
3059
|
const data = await response.json();
|
|
3047
3060
|
return data.success === true;
|
|
3048
3061
|
} catch (err) {
|
|
3049
|
-
console.error(
|
|
3062
|
+
console.error(
|
|
3063
|
+
` PKCE exchange error: ${err instanceof Error ? err.message : String(err)}`
|
|
3064
|
+
);
|
|
3050
3065
|
return false;
|
|
3051
3066
|
}
|
|
3052
3067
|
}
|
|
@@ -3057,9 +3072,13 @@ async function runSetup() {
|
|
|
3057
3072
|
console.error("");
|
|
3058
3073
|
console.error(" Privacy Notice:");
|
|
3059
3074
|
console.error(" - Your API key is stored locally in your OS keychain");
|
|
3060
|
-
console.error(
|
|
3075
|
+
console.error(
|
|
3076
|
+
" - Tool invocations are logged for usage metering (no content stored)"
|
|
3077
|
+
);
|
|
3061
3078
|
console.error(" - Set DO_NOT_TRACK=1 to disable telemetry");
|
|
3062
|
-
console.error(
|
|
3079
|
+
console.error(
|
|
3080
|
+
" - Data export/delete: https://www.socialneuron.com/settings"
|
|
3081
|
+
);
|
|
3063
3082
|
console.error("");
|
|
3064
3083
|
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
3065
3084
|
const state = randomUUID3();
|
|
@@ -3092,49 +3111,54 @@ async function runSetup() {
|
|
|
3092
3111
|
console.error("");
|
|
3093
3112
|
console.error(" Waiting for authorization (timeout: 120s)...");
|
|
3094
3113
|
}
|
|
3095
|
-
const result = await new Promise(
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
try {
|
|
3111
|
-
const body = await readBody(req);
|
|
3112
|
-
const data = JSON.parse(body);
|
|
3113
|
-
if (data.state !== state) {
|
|
3114
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3115
|
-
res.end(JSON.stringify({ error: "State mismatch" }));
|
|
3114
|
+
const result = await new Promise(
|
|
3115
|
+
(resolve3) => {
|
|
3116
|
+
const timeout = setTimeout(() => {
|
|
3117
|
+
server2.close();
|
|
3118
|
+
resolve3({ error: "Authorization timed out after 120 seconds." });
|
|
3119
|
+
}, 12e4);
|
|
3120
|
+
server2.on(
|
|
3121
|
+
"request",
|
|
3122
|
+
async (req, res) => {
|
|
3123
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
3124
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
3125
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
3126
|
+
if (req.method === "OPTIONS") {
|
|
3127
|
+
res.writeHead(204);
|
|
3128
|
+
res.end();
|
|
3116
3129
|
return;
|
|
3117
3130
|
}
|
|
3118
|
-
if (
|
|
3119
|
-
|
|
3120
|
-
|
|
3131
|
+
if (req.method === "POST" && req.url === "/callback") {
|
|
3132
|
+
try {
|
|
3133
|
+
const body = await readBody(req);
|
|
3134
|
+
const data = JSON.parse(body);
|
|
3135
|
+
if (data.state !== state) {
|
|
3136
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3137
|
+
res.end(JSON.stringify({ error: "State mismatch" }));
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
if (!data.api_key) {
|
|
3141
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3142
|
+
res.end(JSON.stringify({ error: "Missing api_key" }));
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3146
|
+
res.end(JSON.stringify({ success: true }));
|
|
3147
|
+
clearTimeout(timeout);
|
|
3148
|
+
server2.close();
|
|
3149
|
+
resolve3({ apiKey: data.api_key });
|
|
3150
|
+
} catch {
|
|
3151
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3152
|
+
res.end(JSON.stringify({ error: "Invalid request" }));
|
|
3153
|
+
}
|
|
3121
3154
|
return;
|
|
3122
3155
|
}
|
|
3123
|
-
res.writeHead(
|
|
3124
|
-
res.end(
|
|
3125
|
-
clearTimeout(timeout);
|
|
3126
|
-
server2.close();
|
|
3127
|
-
resolve3({ apiKey: data.api_key });
|
|
3128
|
-
} catch {
|
|
3129
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3130
|
-
res.end(JSON.stringify({ error: "Invalid request" }));
|
|
3156
|
+
res.writeHead(404);
|
|
3157
|
+
res.end("Not found");
|
|
3131
3158
|
}
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
res.end("Not found");
|
|
3136
|
-
});
|
|
3137
|
-
});
|
|
3159
|
+
);
|
|
3160
|
+
}
|
|
3161
|
+
);
|
|
3138
3162
|
if ("error" in result) {
|
|
3139
3163
|
console.error("");
|
|
3140
3164
|
console.error(` Error: ${result.error}`);
|
|
@@ -3146,7 +3170,9 @@ async function runSetup() {
|
|
|
3146
3170
|
const exchangeSuccess = await completePkceExchange(codeVerifier, state);
|
|
3147
3171
|
if (!exchangeSuccess) {
|
|
3148
3172
|
console.error(" Warning: PKCE exchange failed. Key may not be activated.");
|
|
3149
|
-
console.error(
|
|
3173
|
+
console.error(
|
|
3174
|
+
" The key will still work if the server was in legacy mode."
|
|
3175
|
+
);
|
|
3150
3176
|
} else {
|
|
3151
3177
|
console.error(" PKCE verification complete.");
|
|
3152
3178
|
}
|
|
@@ -3167,7 +3193,9 @@ async function runSetup() {
|
|
|
3167
3193
|
}
|
|
3168
3194
|
if (!configured) {
|
|
3169
3195
|
console.error("");
|
|
3170
|
-
console.error(
|
|
3196
|
+
console.error(
|
|
3197
|
+
" No MCP client config found. Add this to your MCP config manually:"
|
|
3198
|
+
);
|
|
3171
3199
|
console.error("");
|
|
3172
3200
|
console.error(' "socialneuron": {');
|
|
3173
3201
|
console.error(' "command": "npx",');
|
|
@@ -3870,10 +3898,10 @@ init_supabase();
|
|
|
3870
3898
|
function registerIdeationTools(server2) {
|
|
3871
3899
|
server2.tool(
|
|
3872
3900
|
"generate_content",
|
|
3873
|
-
"
|
|
3901
|
+
"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.",
|
|
3874
3902
|
{
|
|
3875
3903
|
prompt: z.string().max(1e4).describe(
|
|
3876
|
-
|
|
3904
|
+
'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.'
|
|
3877
3905
|
),
|
|
3878
3906
|
content_type: z.enum(["script", "caption", "blog", "hook"]).describe(
|
|
3879
3907
|
'Type of content to generate. "script" for video scripts, "caption" for social media captions, "blog" for blog posts, "hook" for attention-grabbing hooks.'
|
|
@@ -3891,7 +3919,7 @@ function registerIdeationTools(server2) {
|
|
|
3891
3919
|
"Target social media platform. Helps tailor tone, length, and format."
|
|
3892
3920
|
),
|
|
3893
3921
|
brand_voice: z.string().max(500).optional().describe(
|
|
3894
|
-
'
|
|
3922
|
+
'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.'
|
|
3895
3923
|
),
|
|
3896
3924
|
model: z.enum(["gemini-2.0-flash", "gemini-2.5-flash", "gemini-2.5-pro"]).optional().describe(
|
|
3897
3925
|
"AI model to use. Defaults to gemini-2.5-flash. Use gemini-2.5-pro for highest quality."
|
|
@@ -3900,6 +3928,13 @@ function registerIdeationTools(server2) {
|
|
|
3900
3928
|
"Project ID to auto-load brand profile and performance context for prompt enrichment."
|
|
3901
3929
|
)
|
|
3902
3930
|
},
|
|
3931
|
+
{
|
|
3932
|
+
title: "Generate Content",
|
|
3933
|
+
readOnlyHint: false,
|
|
3934
|
+
destructiveHint: false,
|
|
3935
|
+
idempotentHint: false,
|
|
3936
|
+
openWorldHint: true
|
|
3937
|
+
},
|
|
3903
3938
|
async ({
|
|
3904
3939
|
prompt: prompt2,
|
|
3905
3940
|
content_type,
|
|
@@ -4047,7 +4082,7 @@ Content Type: ${content_type}`;
|
|
|
4047
4082
|
);
|
|
4048
4083
|
server2.tool(
|
|
4049
4084
|
"fetch_trends",
|
|
4050
|
-
|
|
4085
|
+
'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.',
|
|
4051
4086
|
{
|
|
4052
4087
|
source: z.enum(["youtube", "google_trends", "rss", "url"]).describe(
|
|
4053
4088
|
'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.'
|
|
@@ -4063,6 +4098,13 @@ Content Type: ${content_type}`;
|
|
|
4063
4098
|
),
|
|
4064
4099
|
force_refresh: z.boolean().optional().describe("Skip the server-side cache and fetch fresh data.")
|
|
4065
4100
|
},
|
|
4101
|
+
{
|
|
4102
|
+
title: "Fetch Trends",
|
|
4103
|
+
readOnlyHint: true,
|
|
4104
|
+
destructiveHint: false,
|
|
4105
|
+
idempotentHint: false,
|
|
4106
|
+
openWorldHint: true
|
|
4107
|
+
},
|
|
4066
4108
|
async ({ source, category, niche, url, force_refresh }) => {
|
|
4067
4109
|
if ((source === "rss" || source === "url") && !url) {
|
|
4068
4110
|
return {
|
|
@@ -4133,7 +4175,7 @@ Content Type: ${content_type}`;
|
|
|
4133
4175
|
);
|
|
4134
4176
|
server2.tool(
|
|
4135
4177
|
"adapt_content",
|
|
4136
|
-
"
|
|
4178
|
+
"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.",
|
|
4137
4179
|
{
|
|
4138
4180
|
content: z.string().max(5e3).describe(
|
|
4139
4181
|
"The content to adapt. Can be a caption, script, blog excerpt, or any text."
|
|
@@ -4167,6 +4209,13 @@ Content Type: ${content_type}`;
|
|
|
4167
4209
|
"Optional project ID to load platform voice overrides from brand profile."
|
|
4168
4210
|
)
|
|
4169
4211
|
},
|
|
4212
|
+
{
|
|
4213
|
+
title: "Adapt Content",
|
|
4214
|
+
readOnlyHint: false,
|
|
4215
|
+
destructiveHint: false,
|
|
4216
|
+
idempotentHint: false,
|
|
4217
|
+
openWorldHint: true
|
|
4218
|
+
},
|
|
4170
4219
|
async ({
|
|
4171
4220
|
content,
|
|
4172
4221
|
source_platform,
|
|
@@ -4431,10 +4480,10 @@ function checkAssetBudget() {
|
|
|
4431
4480
|
function registerContentTools(server2) {
|
|
4432
4481
|
server2.tool(
|
|
4433
4482
|
"generate_video",
|
|
4434
|
-
"Start an AI video generation job
|
|
4483
|
+
"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.",
|
|
4435
4484
|
{
|
|
4436
4485
|
prompt: z2.string().max(2500).describe(
|
|
4437
|
-
|
|
4486
|
+
'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.'
|
|
4438
4487
|
),
|
|
4439
4488
|
model: z2.enum([
|
|
4440
4489
|
"veo3-fast",
|
|
@@ -4446,13 +4495,13 @@ function registerContentTools(server2) {
|
|
|
4446
4495
|
"kling-3",
|
|
4447
4496
|
"kling-3-pro"
|
|
4448
4497
|
]).describe(
|
|
4449
|
-
"Video
|
|
4498
|
+
"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)."
|
|
4450
4499
|
),
|
|
4451
4500
|
duration: z2.number().min(3).max(30).optional().describe(
|
|
4452
4501
|
"Video duration in seconds. kling: 5-30s, kling-3/kling-3-pro: 3-15s, sora2: 10-15s. Defaults to 5 seconds."
|
|
4453
4502
|
),
|
|
4454
4503
|
aspect_ratio: z2.enum(["16:9", "9:16", "1:1"]).optional().describe(
|
|
4455
|
-
"
|
|
4504
|
+
"Video aspect ratio. 16:9 for YouTube/landscape, 9:16 for TikTok/Reels/Shorts, 1:1 for Instagram feed/square. Defaults to 16:9."
|
|
4456
4505
|
),
|
|
4457
4506
|
enable_audio: z2.boolean().optional().describe(
|
|
4458
4507
|
"Enable native audio generation. Kling 2.6: doubles cost. Kling 3.0: 50% more (std 30/sec, pro 40/sec). 5+ languages."
|
|
@@ -4465,6 +4514,13 @@ function registerContentTools(server2) {
|
|
|
4465
4514
|
),
|
|
4466
4515
|
response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
4467
4516
|
},
|
|
4517
|
+
{
|
|
4518
|
+
title: "Generate Video",
|
|
4519
|
+
readOnlyHint: false,
|
|
4520
|
+
destructiveHint: false,
|
|
4521
|
+
idempotentHint: false,
|
|
4522
|
+
openWorldHint: true
|
|
4523
|
+
},
|
|
4468
4524
|
async ({
|
|
4469
4525
|
prompt: prompt2,
|
|
4470
4526
|
model,
|
|
@@ -4639,7 +4695,7 @@ function registerContentTools(server2) {
|
|
|
4639
4695
|
);
|
|
4640
4696
|
server2.tool(
|
|
4641
4697
|
"generate_image",
|
|
4642
|
-
"Start an AI image generation job
|
|
4698
|
+
"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).",
|
|
4643
4699
|
{
|
|
4644
4700
|
prompt: z2.string().max(2e3).describe(
|
|
4645
4701
|
"Text prompt describing the image to generate. Be specific about style, composition, colors, lighting, and subject matter."
|
|
@@ -4663,6 +4719,13 @@ function registerContentTools(server2) {
|
|
|
4663
4719
|
),
|
|
4664
4720
|
response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
4665
4721
|
},
|
|
4722
|
+
{
|
|
4723
|
+
title: "Generate Image",
|
|
4724
|
+
readOnlyHint: false,
|
|
4725
|
+
destructiveHint: false,
|
|
4726
|
+
idempotentHint: false,
|
|
4727
|
+
openWorldHint: true
|
|
4728
|
+
},
|
|
4666
4729
|
async ({ prompt: prompt2, model, aspect_ratio, image_url, response_format }) => {
|
|
4667
4730
|
const format = response_format ?? "text";
|
|
4668
4731
|
const startedAt = Date.now();
|
|
@@ -4819,13 +4882,20 @@ function registerContentTools(server2) {
|
|
|
4819
4882
|
);
|
|
4820
4883
|
server2.tool(
|
|
4821
4884
|
"check_status",
|
|
4822
|
-
|
|
4885
|
+
'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.',
|
|
4823
4886
|
{
|
|
4824
4887
|
job_id: z2.string().describe(
|
|
4825
4888
|
"The job ID returned by generate_video or generate_image. This is the asyncJobId or taskId value."
|
|
4826
4889
|
),
|
|
4827
4890
|
response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
4828
4891
|
},
|
|
4892
|
+
{
|
|
4893
|
+
title: "Check Job Status",
|
|
4894
|
+
readOnlyHint: true,
|
|
4895
|
+
destructiveHint: false,
|
|
4896
|
+
idempotentHint: true,
|
|
4897
|
+
openWorldHint: true
|
|
4898
|
+
},
|
|
4829
4899
|
async ({ job_id, response_format }) => {
|
|
4830
4900
|
const format = response_format ?? "text";
|
|
4831
4901
|
const startedAt = Date.now();
|
|
@@ -4995,7 +5065,7 @@ function registerContentTools(server2) {
|
|
|
4995
5065
|
);
|
|
4996
5066
|
server2.tool(
|
|
4997
5067
|
"create_storyboard",
|
|
4998
|
-
"
|
|
5068
|
+
"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.",
|
|
4999
5069
|
{
|
|
5000
5070
|
concept: z2.string().max(2e3).describe(
|
|
5001
5071
|
'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").'
|
|
@@ -5023,6 +5093,13 @@ function registerContentTools(server2) {
|
|
|
5023
5093
|
"Response format. Defaults to json for structured storyboard data."
|
|
5024
5094
|
)
|
|
5025
5095
|
},
|
|
5096
|
+
{
|
|
5097
|
+
title: "Create Storyboard",
|
|
5098
|
+
readOnlyHint: false,
|
|
5099
|
+
destructiveHint: false,
|
|
5100
|
+
idempotentHint: false,
|
|
5101
|
+
openWorldHint: true
|
|
5102
|
+
},
|
|
5026
5103
|
async ({
|
|
5027
5104
|
concept,
|
|
5028
5105
|
brand_context,
|
|
@@ -5182,7 +5259,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
5182
5259
|
);
|
|
5183
5260
|
server2.tool(
|
|
5184
5261
|
"generate_voiceover",
|
|
5185
|
-
"Generate a
|
|
5262
|
+
"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.",
|
|
5186
5263
|
{
|
|
5187
5264
|
text: z2.string().max(5e3).describe("The script/text to convert to speech."),
|
|
5188
5265
|
voice: z2.enum([
|
|
@@ -5203,6 +5280,13 @@ Return ONLY valid JSON in this exact format:
|
|
|
5203
5280
|
speed: z2.number().min(0.5).max(2).optional().describe("Speech speed multiplier. 1.0 is normal. Defaults to 1.0."),
|
|
5204
5281
|
response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
5205
5282
|
},
|
|
5283
|
+
{
|
|
5284
|
+
title: "Generate Voiceover",
|
|
5285
|
+
readOnlyHint: false,
|
|
5286
|
+
destructiveHint: false,
|
|
5287
|
+
idempotentHint: false,
|
|
5288
|
+
openWorldHint: true
|
|
5289
|
+
},
|
|
5206
5290
|
async ({ text, voice, speed, response_format }) => {
|
|
5207
5291
|
const format = response_format ?? "text";
|
|
5208
5292
|
const startedAt = Date.now();
|
|
@@ -5333,10 +5417,10 @@ Return ONLY valid JSON in this exact format:
|
|
|
5333
5417
|
);
|
|
5334
5418
|
server2.tool(
|
|
5335
5419
|
"generate_carousel",
|
|
5336
|
-
"Generate
|
|
5420
|
+
"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.",
|
|
5337
5421
|
{
|
|
5338
5422
|
topic: z2.string().max(200).describe(
|
|
5339
|
-
'
|
|
5423
|
+
'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.'
|
|
5340
5424
|
),
|
|
5341
5425
|
template_id: z2.enum([
|
|
5342
5426
|
"educational-series",
|
|
@@ -5361,6 +5445,13 @@ Return ONLY valid JSON in this exact format:
|
|
|
5361
5445
|
project_id: z2.string().optional().describe("Project ID to associate the carousel with."),
|
|
5362
5446
|
response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to json.")
|
|
5363
5447
|
},
|
|
5448
|
+
{
|
|
5449
|
+
title: "Generate Carousel",
|
|
5450
|
+
readOnlyHint: false,
|
|
5451
|
+
destructiveHint: false,
|
|
5452
|
+
idempotentHint: false,
|
|
5453
|
+
openWorldHint: true
|
|
5454
|
+
},
|
|
5364
5455
|
async ({
|
|
5365
5456
|
topic,
|
|
5366
5457
|
template_id,
|
|
@@ -5543,14 +5634,12 @@ function asEnvelope2(data) {
|
|
|
5543
5634
|
function registerDistributionTools(server2) {
|
|
5544
5635
|
server2.tool(
|
|
5545
5636
|
"schedule_post",
|
|
5546
|
-
|
|
5637
|
+
'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.',
|
|
5547
5638
|
{
|
|
5548
5639
|
media_url: z3.string().optional().describe(
|
|
5549
5640
|
"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."
|
|
5550
5641
|
),
|
|
5551
|
-
media_urls: z3.array(z3.string()).optional().describe(
|
|
5552
|
-
"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."
|
|
5553
|
-
),
|
|
5642
|
+
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."),
|
|
5554
5643
|
media_type: z3.enum(["IMAGE", "VIDEO", "CAROUSEL_ALBUM"]).optional().describe(
|
|
5555
5644
|
"Media type. Set to CAROUSEL_ALBUM with media_urls for Instagram carousels. Default: auto-detected from media_url."
|
|
5556
5645
|
),
|
|
@@ -5566,22 +5655,23 @@ function registerDistributionTools(server2) {
|
|
|
5566
5655
|
"threads",
|
|
5567
5656
|
"bluesky"
|
|
5568
5657
|
])
|
|
5569
|
-
).min(1).describe(
|
|
5570
|
-
"Target platforms to post to. Each must have an active OAuth connection."
|
|
5571
|
-
),
|
|
5658
|
+
).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."),
|
|
5572
5659
|
title: z3.string().optional().describe("Post title (used by YouTube and some other platforms)."),
|
|
5573
|
-
hashtags: z3.array(z3.string()).optional().describe(
|
|
5574
|
-
|
|
5575
|
-
),
|
|
5576
|
-
schedule_at: z3.string().optional().describe(
|
|
5577
|
-
'ISO 8601 datetime for scheduled posting (e.g. "2026-03-15T14:00:00Z"). Omit for immediate posting.'
|
|
5578
|
-
),
|
|
5660
|
+
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"].'),
|
|
5661
|
+
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.'),
|
|
5579
5662
|
project_id: z3.string().optional().describe("Social Neuron project ID to associate this post with."),
|
|
5580
5663
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
|
|
5581
5664
|
attribution: z3.boolean().optional().describe(
|
|
5582
5665
|
'If true, appends "Created with Social Neuron" to the caption. Default: false.'
|
|
5583
5666
|
)
|
|
5584
5667
|
},
|
|
5668
|
+
{
|
|
5669
|
+
title: "Schedule Post",
|
|
5670
|
+
readOnlyHint: false,
|
|
5671
|
+
destructiveHint: false,
|
|
5672
|
+
idempotentHint: false,
|
|
5673
|
+
openWorldHint: true
|
|
5674
|
+
},
|
|
5585
5675
|
async ({
|
|
5586
5676
|
media_url,
|
|
5587
5677
|
media_urls,
|
|
@@ -5729,10 +5819,17 @@ Created with Social Neuron`;
|
|
|
5729
5819
|
);
|
|
5730
5820
|
server2.tool(
|
|
5731
5821
|
"list_connected_accounts",
|
|
5732
|
-
"
|
|
5822
|
+
"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.",
|
|
5733
5823
|
{
|
|
5734
5824
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
5735
5825
|
},
|
|
5826
|
+
{
|
|
5827
|
+
title: "List Connected Accounts",
|
|
5828
|
+
readOnlyHint: true,
|
|
5829
|
+
destructiveHint: false,
|
|
5830
|
+
idempotentHint: true,
|
|
5831
|
+
openWorldHint: false
|
|
5832
|
+
},
|
|
5736
5833
|
async ({ response_format }) => {
|
|
5737
5834
|
const format = response_format ?? "text";
|
|
5738
5835
|
const supabase = getSupabaseClient();
|
|
@@ -5794,7 +5891,7 @@ Created with Social Neuron`;
|
|
|
5794
5891
|
);
|
|
5795
5892
|
server2.tool(
|
|
5796
5893
|
"list_recent_posts",
|
|
5797
|
-
"List recent
|
|
5894
|
+
"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.",
|
|
5798
5895
|
{
|
|
5799
5896
|
platform: z3.enum([
|
|
5800
5897
|
"youtube",
|
|
@@ -5811,6 +5908,13 @@ Created with Social Neuron`;
|
|
|
5811
5908
|
limit: z3.number().min(1).max(50).optional().describe("Maximum number of posts to return. Defaults to 20."),
|
|
5812
5909
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
5813
5910
|
},
|
|
5911
|
+
{
|
|
5912
|
+
title: "List Recent Posts",
|
|
5913
|
+
readOnlyHint: true,
|
|
5914
|
+
destructiveHint: false,
|
|
5915
|
+
idempotentHint: true,
|
|
5916
|
+
openWorldHint: false
|
|
5917
|
+
},
|
|
5814
5918
|
async ({ platform: platform3, status, days, limit, response_format }) => {
|
|
5815
5919
|
const format = response_format ?? "text";
|
|
5816
5920
|
const supabase = getSupabaseClient();
|
|
@@ -5910,7 +6014,7 @@ Created with Social Neuron`;
|
|
|
5910
6014
|
};
|
|
5911
6015
|
server2.tool(
|
|
5912
6016
|
"find_next_slots",
|
|
5913
|
-
"Find
|
|
6017
|
+
"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.",
|
|
5914
6018
|
{
|
|
5915
6019
|
platforms: z3.array(
|
|
5916
6020
|
z3.enum([
|
|
@@ -5923,11 +6027,18 @@ Created with Social Neuron`;
|
|
|
5923
6027
|
"threads",
|
|
5924
6028
|
"bluesky"
|
|
5925
6029
|
])
|
|
5926
|
-
).min(1),
|
|
6030
|
+
).min(1).describe("Platforms to find posting slots for."),
|
|
5927
6031
|
count: z3.number().min(1).max(20).default(7).describe("Number of slots to find"),
|
|
5928
6032
|
start_after: z3.string().optional().describe("ISO datetime, defaults to now"),
|
|
5929
6033
|
min_gap_hours: z3.number().min(1).max(24).default(4).describe("Minimum gap between posts on same platform"),
|
|
5930
|
-
response_format: z3.enum(["text", "json"]).default("text")
|
|
6034
|
+
response_format: z3.enum(["text", "json"]).default("text").describe("Response format. Defaults to text.")
|
|
6035
|
+
},
|
|
6036
|
+
{
|
|
6037
|
+
title: "Find Next Posting Slots",
|
|
6038
|
+
readOnlyHint: true,
|
|
6039
|
+
destructiveHint: false,
|
|
6040
|
+
idempotentHint: true,
|
|
6041
|
+
openWorldHint: false
|
|
5931
6042
|
},
|
|
5932
6043
|
async ({
|
|
5933
6044
|
platforms,
|
|
@@ -6047,20 +6158,20 @@ Created with Social Neuron`;
|
|
|
6047
6158
|
plan: z3.object({
|
|
6048
6159
|
posts: z3.array(
|
|
6049
6160
|
z3.object({
|
|
6050
|
-
id: z3.string(),
|
|
6051
|
-
caption: z3.string(),
|
|
6052
|
-
platform: z3.string(),
|
|
6053
|
-
title: z3.string().optional(),
|
|
6054
|
-
media_url: z3.string().optional(),
|
|
6055
|
-
schedule_at: z3.string().optional(),
|
|
6056
|
-
hashtags: z3.array(z3.string()).optional()
|
|
6161
|
+
id: z3.string().describe("Unique post identifier from the content plan."),
|
|
6162
|
+
caption: z3.string().describe("Post caption/body text."),
|
|
6163
|
+
platform: z3.string().describe("Target platform name (e.g. instagram, youtube)."),
|
|
6164
|
+
title: z3.string().optional().describe("Post title, required for YouTube."),
|
|
6165
|
+
media_url: z3.string().optional().describe("Public or R2 signed URL for the post media."),
|
|
6166
|
+
schedule_at: z3.string().optional().describe("ISO 8601 UTC datetime to publish (e.g. 2026-03-20T14:00:00Z)."),
|
|
6167
|
+
hashtags: z3.array(z3.string()).optional().describe("Hashtags to append to the caption.")
|
|
6057
6168
|
})
|
|
6058
6169
|
)
|
|
6059
|
-
}).passthrough().optional(),
|
|
6170
|
+
}).passthrough().optional().describe("Inline content plan object with a posts array. Provide this or plan_id."),
|
|
6060
6171
|
plan_id: z3.string().uuid().optional().describe("Persisted content plan ID from content_plans table"),
|
|
6061
6172
|
auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
|
|
6062
6173
|
dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
|
|
6063
|
-
response_format: z3.enum(["text", "json"]).default("text"),
|
|
6174
|
+
response_format: z3.enum(["text", "json"]).default("text").describe("Response format. Defaults to text."),
|
|
6064
6175
|
enforce_quality: z3.boolean().default(true).describe(
|
|
6065
6176
|
"When true, block scheduling for posts that fail quality checks."
|
|
6066
6177
|
),
|
|
@@ -6070,6 +6181,13 @@ Created with Social Neuron`;
|
|
|
6070
6181
|
batch_size: z3.number().int().min(1).max(10).default(4).describe("Concurrent schedule calls per platform batch."),
|
|
6071
6182
|
idempotency_seed: z3.string().max(128).optional().describe("Optional stable seed used when building idempotency keys.")
|
|
6072
6183
|
},
|
|
6184
|
+
{
|
|
6185
|
+
title: "Schedule Content Plan",
|
|
6186
|
+
readOnlyHint: false,
|
|
6187
|
+
destructiveHint: false,
|
|
6188
|
+
idempotentHint: false,
|
|
6189
|
+
openWorldHint: true
|
|
6190
|
+
},
|
|
6073
6191
|
async ({
|
|
6074
6192
|
plan,
|
|
6075
6193
|
plan_id,
|
|
@@ -6635,13 +6753,14 @@ function registerAnalyticsTools(server2) {
|
|
|
6635
6753
|
"threads",
|
|
6636
6754
|
"bluesky"
|
|
6637
6755
|
]).optional().describe("Filter analytics to a specific platform."),
|
|
6638
|
-
days: z4.number().min(1).max(365).optional().describe("
|
|
6756
|
+
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."),
|
|
6639
6757
|
content_id: z4.string().uuid().optional().describe(
|
|
6640
6758
|
"Filter to a specific content_history ID to see performance of one piece of content."
|
|
6641
6759
|
),
|
|
6642
6760
|
limit: z4.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
|
|
6643
6761
|
response_format: z4.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6644
6762
|
},
|
|
6763
|
+
{ title: "Fetch Analytics", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
6645
6764
|
async ({ platform: platform3, days, content_id, limit, response_format }) => {
|
|
6646
6765
|
const format = response_format ?? "text";
|
|
6647
6766
|
const supabase = getSupabaseClient();
|
|
@@ -6815,10 +6934,17 @@ function registerAnalyticsTools(server2) {
|
|
|
6815
6934
|
);
|
|
6816
6935
|
server2.tool(
|
|
6817
6936
|
"refresh_platform_analytics",
|
|
6818
|
-
"
|
|
6937
|
+
"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.",
|
|
6819
6938
|
{
|
|
6820
6939
|
response_format: z4.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6821
6940
|
},
|
|
6941
|
+
{
|
|
6942
|
+
title: "Refresh Platform Analytics",
|
|
6943
|
+
readOnlyHint: false,
|
|
6944
|
+
destructiveHint: false,
|
|
6945
|
+
idempotentHint: false,
|
|
6946
|
+
openWorldHint: true
|
|
6947
|
+
},
|
|
6822
6948
|
async ({ response_format }) => {
|
|
6823
6949
|
const format = response_format ?? "text";
|
|
6824
6950
|
const startedAt = Date.now();
|
|
@@ -7220,6 +7346,13 @@ function registerBrandTools(server2) {
|
|
|
7220
7346
|
),
|
|
7221
7347
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
7222
7348
|
},
|
|
7349
|
+
{
|
|
7350
|
+
title: "Extract Brand",
|
|
7351
|
+
readOnlyHint: true,
|
|
7352
|
+
destructiveHint: false,
|
|
7353
|
+
idempotentHint: true,
|
|
7354
|
+
openWorldHint: true
|
|
7355
|
+
},
|
|
7223
7356
|
async ({ url, response_format }) => {
|
|
7224
7357
|
const ssrfCheck = await validateUrlForSSRF(url);
|
|
7225
7358
|
if (!ssrfCheck.isValid) {
|
|
@@ -7303,6 +7436,13 @@ function registerBrandTools(server2) {
|
|
|
7303
7436
|
project_id: z5.string().uuid().optional().describe("Project ID. Defaults to active project context."),
|
|
7304
7437
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
7305
7438
|
},
|
|
7439
|
+
{
|
|
7440
|
+
title: "Get Brand Profile",
|
|
7441
|
+
readOnlyHint: true,
|
|
7442
|
+
destructiveHint: false,
|
|
7443
|
+
idempotentHint: true,
|
|
7444
|
+
openWorldHint: false
|
|
7445
|
+
},
|
|
7306
7446
|
async ({ project_id, response_format }) => {
|
|
7307
7447
|
const supabase = getSupabaseClient();
|
|
7308
7448
|
const userId = await getDefaultUserId();
|
|
@@ -7400,8 +7540,17 @@ function registerBrandTools(server2) {
|
|
|
7400
7540
|
"product_showcase"
|
|
7401
7541
|
]).optional().describe("Extraction method metadata."),
|
|
7402
7542
|
overall_confidence: z5.number().min(0).max(1).optional().describe("Optional overall confidence score in range 0..1."),
|
|
7403
|
-
extraction_metadata: z5.record(z5.string(), z5.unknown()).optional()
|
|
7404
|
-
|
|
7543
|
+
extraction_metadata: z5.record(z5.string(), z5.unknown()).optional().describe(
|
|
7544
|
+
"Arbitrary key-value metadata about the extraction process."
|
|
7545
|
+
),
|
|
7546
|
+
response_format: z5.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
7547
|
+
},
|
|
7548
|
+
{
|
|
7549
|
+
title: "Save Brand Profile",
|
|
7550
|
+
readOnlyHint: false,
|
|
7551
|
+
destructiveHint: false,
|
|
7552
|
+
idempotentHint: false,
|
|
7553
|
+
openWorldHint: false
|
|
7405
7554
|
},
|
|
7406
7555
|
async ({
|
|
7407
7556
|
project_id,
|
|
@@ -7514,15 +7663,32 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
7514
7663
|
"facebook",
|
|
7515
7664
|
"threads",
|
|
7516
7665
|
"bluesky"
|
|
7517
|
-
]),
|
|
7666
|
+
]).describe("Social platform to set voice overrides for."),
|
|
7518
7667
|
project_id: z5.string().uuid().optional().describe("Project ID. Defaults to active project context."),
|
|
7519
7668
|
samples: z5.string().max(3e3).optional().describe("3-5 real platform post examples for style anchoring."),
|
|
7520
|
-
tone: z5.array(z5.string()).optional()
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7669
|
+
tone: z5.array(z5.string()).optional().describe(
|
|
7670
|
+
'Tone descriptors for this platform (e.g. ["casual", "witty", "informative"]).'
|
|
7671
|
+
),
|
|
7672
|
+
style: z5.array(z5.string()).optional().describe(
|
|
7673
|
+
'Writing style tags (e.g. ["short-form", "emoji-heavy", "storytelling"]).'
|
|
7674
|
+
),
|
|
7675
|
+
avoid_patterns: z5.array(z5.string()).optional().describe(
|
|
7676
|
+
'Phrases or patterns the brand should never use on this platform (e.g. ["click here", "buy now"]).'
|
|
7677
|
+
),
|
|
7678
|
+
hashtag_strategy: z5.string().max(300).optional().describe(
|
|
7679
|
+
'Hashtag usage guidelines for this platform (e.g. "3-5 niche hashtags, no generic tags").'
|
|
7680
|
+
),
|
|
7681
|
+
cta_style: z5.string().max(300).optional().describe(
|
|
7682
|
+
'Preferred call-to-action style (e.g. "soft CTA with question" or "direct link in bio").'
|
|
7683
|
+
),
|
|
7684
|
+
response_format: z5.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
7685
|
+
},
|
|
7686
|
+
{
|
|
7687
|
+
title: "Update Platform Voice",
|
|
7688
|
+
readOnlyHint: false,
|
|
7689
|
+
destructiveHint: false,
|
|
7690
|
+
idempotentHint: false,
|
|
7691
|
+
openWorldHint: false
|
|
7526
7692
|
},
|
|
7527
7693
|
async ({
|
|
7528
7694
|
platform: platform3,
|
|
@@ -7771,6 +7937,13 @@ function registerScreenshotTools(server2) {
|
|
|
7771
7937
|
"Extra milliseconds to wait after page load before capturing. Useful for animations. Defaults to 2000."
|
|
7772
7938
|
)
|
|
7773
7939
|
},
|
|
7940
|
+
{
|
|
7941
|
+
title: "Capture App Page",
|
|
7942
|
+
readOnlyHint: true,
|
|
7943
|
+
destructiveHint: false,
|
|
7944
|
+
idempotentHint: false,
|
|
7945
|
+
openWorldHint: false
|
|
7946
|
+
},
|
|
7774
7947
|
async ({ page: pageName, viewport, theme, selector, wait_ms }) => {
|
|
7775
7948
|
const startedAt = Date.now();
|
|
7776
7949
|
let rateLimitKey = "anonymous";
|
|
@@ -7891,6 +8064,13 @@ function registerScreenshotTools(server2) {
|
|
|
7891
8064
|
),
|
|
7892
8065
|
wait_ms: z6.number().min(0).max(3e4).optional().describe("Extra milliseconds to wait after page load before capturing. Defaults to 1000.")
|
|
7893
8066
|
},
|
|
8067
|
+
{
|
|
8068
|
+
title: "Capture Screenshot",
|
|
8069
|
+
readOnlyHint: true,
|
|
8070
|
+
destructiveHint: false,
|
|
8071
|
+
idempotentHint: false,
|
|
8072
|
+
openWorldHint: true
|
|
8073
|
+
},
|
|
7894
8074
|
async ({ url, viewport, selector, output_path, wait_ms }) => {
|
|
7895
8075
|
const startedAt = Date.now();
|
|
7896
8076
|
let rateLimitKey = "anonymous";
|
|
@@ -8153,6 +8333,13 @@ function registerRemotionTools(server2) {
|
|
|
8153
8333
|
"list_compositions",
|
|
8154
8334
|
"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.",
|
|
8155
8335
|
{},
|
|
8336
|
+
{
|
|
8337
|
+
title: "List Compositions",
|
|
8338
|
+
readOnlyHint: true,
|
|
8339
|
+
destructiveHint: false,
|
|
8340
|
+
idempotentHint: true,
|
|
8341
|
+
openWorldHint: false
|
|
8342
|
+
},
|
|
8156
8343
|
async () => {
|
|
8157
8344
|
const lines = [`${COMPOSITIONS.length} Remotion compositions available:`, ""];
|
|
8158
8345
|
for (const comp of COMPOSITIONS) {
|
|
@@ -8181,6 +8368,13 @@ function registerRemotionTools(server2) {
|
|
|
8181
8368
|
"JSON string of input props to pass to the composition. Each composition accepts different props. Omit for defaults."
|
|
8182
8369
|
)
|
|
8183
8370
|
},
|
|
8371
|
+
{
|
|
8372
|
+
title: "Render Demo Video",
|
|
8373
|
+
readOnlyHint: false,
|
|
8374
|
+
destructiveHint: false,
|
|
8375
|
+
idempotentHint: false,
|
|
8376
|
+
openWorldHint: false
|
|
8377
|
+
},
|
|
8184
8378
|
async ({ composition_id, output_format, props }) => {
|
|
8185
8379
|
const startedAt = Date.now();
|
|
8186
8380
|
const userId = await getDefaultUserId();
|
|
@@ -8362,6 +8556,13 @@ function registerInsightsTools(server2) {
|
|
|
8362
8556
|
limit: z8.number().min(1).max(50).optional().describe("Maximum number of insights to return. Defaults to 10."),
|
|
8363
8557
|
response_format: z8.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8364
8558
|
},
|
|
8559
|
+
{
|
|
8560
|
+
title: "Get Performance Insights",
|
|
8561
|
+
readOnlyHint: true,
|
|
8562
|
+
destructiveHint: false,
|
|
8563
|
+
idempotentHint: true,
|
|
8564
|
+
openWorldHint: false
|
|
8565
|
+
},
|
|
8365
8566
|
async ({ insight_type, days, limit, response_format }) => {
|
|
8366
8567
|
const format = response_format ?? "text";
|
|
8367
8568
|
const supabase = getSupabaseClient();
|
|
@@ -8489,6 +8690,13 @@ function registerInsightsTools(server2) {
|
|
|
8489
8690
|
days: z8.number().min(1).max(90).optional().describe("Number of days to analyze. Defaults to 30. Max 90."),
|
|
8490
8691
|
response_format: z8.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8491
8692
|
},
|
|
8693
|
+
{
|
|
8694
|
+
title: "Get Best Posting Times",
|
|
8695
|
+
readOnlyHint: true,
|
|
8696
|
+
destructiveHint: false,
|
|
8697
|
+
idempotentHint: true,
|
|
8698
|
+
openWorldHint: false
|
|
8699
|
+
},
|
|
8492
8700
|
async ({ platform: platform3, days, response_format }) => {
|
|
8493
8701
|
const format = response_format ?? "text";
|
|
8494
8702
|
const supabase = getSupabaseClient();
|
|
@@ -8638,6 +8846,13 @@ function registerYouTubeAnalyticsTools(server2) {
|
|
|
8638
8846
|
video_id: z9.string().optional().describe('YouTube video ID. Required when action is "video".'),
|
|
8639
8847
|
max_results: z9.number().min(1).max(50).optional().describe('Max videos to return for "topVideos" action. Defaults to 10.')
|
|
8640
8848
|
},
|
|
8849
|
+
{
|
|
8850
|
+
title: "Fetch YouTube Analytics",
|
|
8851
|
+
readOnlyHint: true,
|
|
8852
|
+
destructiveHint: false,
|
|
8853
|
+
idempotentHint: true,
|
|
8854
|
+
openWorldHint: true
|
|
8855
|
+
},
|
|
8641
8856
|
async ({ action, start_date, end_date, video_id, max_results }) => {
|
|
8642
8857
|
if (action === "video" && !video_id) {
|
|
8643
8858
|
return {
|
|
@@ -8751,15 +8966,20 @@ function asEnvelope6(data) {
|
|
|
8751
8966
|
function registerCommentsTools(server2) {
|
|
8752
8967
|
server2.tool(
|
|
8753
8968
|
"list_comments",
|
|
8754
|
-
|
|
8969
|
+
'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.',
|
|
8755
8970
|
{
|
|
8756
|
-
video_id: z10.string().optional().describe(
|
|
8757
|
-
"YouTube video ID. If omitted, returns comments across all channel videos."
|
|
8758
|
-
),
|
|
8971
|
+
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.'),
|
|
8759
8972
|
max_results: z10.number().min(1).max(100).optional().describe("Maximum number of comments to return. Defaults to 50."),
|
|
8760
|
-
page_token: z10.string().optional().describe("Pagination
|
|
8973
|
+
page_token: z10.string().optional().describe("Pagination cursor from previous list_comments response nextPageToken field. Omit for first page of results."),
|
|
8761
8974
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8762
8975
|
},
|
|
8976
|
+
{
|
|
8977
|
+
title: "List Comments",
|
|
8978
|
+
readOnlyHint: true,
|
|
8979
|
+
destructiveHint: false,
|
|
8980
|
+
idempotentHint: true,
|
|
8981
|
+
openWorldHint: true
|
|
8982
|
+
},
|
|
8763
8983
|
async ({ video_id, max_results, page_token, response_format }) => {
|
|
8764
8984
|
const format = response_format ?? "text";
|
|
8765
8985
|
const { data, error } = await callEdgeFunction("youtube-comments", {
|
|
@@ -8822,7 +9042,7 @@ function registerCommentsTools(server2) {
|
|
|
8822
9042
|
);
|
|
8823
9043
|
server2.tool(
|
|
8824
9044
|
"reply_to_comment",
|
|
8825
|
-
"Reply to a YouTube comment.
|
|
9045
|
+
"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.",
|
|
8826
9046
|
{
|
|
8827
9047
|
parent_id: z10.string().describe(
|
|
8828
9048
|
"The ID of the parent comment to reply to (from list_comments)."
|
|
@@ -8830,6 +9050,13 @@ function registerCommentsTools(server2) {
|
|
|
8830
9050
|
text: z10.string().min(1).describe("The reply text."),
|
|
8831
9051
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8832
9052
|
},
|
|
9053
|
+
{
|
|
9054
|
+
title: "Reply to Comment",
|
|
9055
|
+
readOnlyHint: false,
|
|
9056
|
+
destructiveHint: false,
|
|
9057
|
+
idempotentHint: false,
|
|
9058
|
+
openWorldHint: true
|
|
9059
|
+
},
|
|
8833
9060
|
async ({ parent_id, text, response_format }) => {
|
|
8834
9061
|
const format = response_format ?? "text";
|
|
8835
9062
|
const startedAt = Date.now();
|
|
@@ -8911,6 +9138,13 @@ function registerCommentsTools(server2) {
|
|
|
8911
9138
|
text: z10.string().min(1).describe("The comment text."),
|
|
8912
9139
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8913
9140
|
},
|
|
9141
|
+
{
|
|
9142
|
+
title: "Post Comment",
|
|
9143
|
+
readOnlyHint: false,
|
|
9144
|
+
destructiveHint: false,
|
|
9145
|
+
idempotentHint: false,
|
|
9146
|
+
openWorldHint: true
|
|
9147
|
+
},
|
|
8914
9148
|
async ({ video_id, text, response_format }) => {
|
|
8915
9149
|
const format = response_format ?? "text";
|
|
8916
9150
|
const startedAt = Date.now();
|
|
@@ -8989,6 +9223,13 @@ function registerCommentsTools(server2) {
|
|
|
8989
9223
|
moderation_status: z10.enum(["published", "rejected"]).describe('"published" to approve, "rejected" to hide.'),
|
|
8990
9224
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8991
9225
|
},
|
|
9226
|
+
{
|
|
9227
|
+
title: "Moderate Comment",
|
|
9228
|
+
readOnlyHint: false,
|
|
9229
|
+
destructiveHint: false,
|
|
9230
|
+
idempotentHint: true,
|
|
9231
|
+
openWorldHint: true
|
|
9232
|
+
},
|
|
8992
9233
|
async ({ comment_id, moderation_status, response_format }) => {
|
|
8993
9234
|
const format = response_format ?? "text";
|
|
8994
9235
|
const startedAt = Date.now();
|
|
@@ -9074,6 +9315,13 @@ function registerCommentsTools(server2) {
|
|
|
9074
9315
|
comment_id: z10.string().describe("The comment ID to delete."),
|
|
9075
9316
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9076
9317
|
},
|
|
9318
|
+
{
|
|
9319
|
+
title: "Delete Comment",
|
|
9320
|
+
readOnlyHint: false,
|
|
9321
|
+
destructiveHint: true,
|
|
9322
|
+
idempotentHint: true,
|
|
9323
|
+
openWorldHint: true
|
|
9324
|
+
},
|
|
9077
9325
|
async ({ comment_id, response_format }) => {
|
|
9078
9326
|
const format = response_format ?? "text";
|
|
9079
9327
|
const startedAt = Date.now();
|
|
@@ -9223,12 +9471,19 @@ function asEnvelope7(data) {
|
|
|
9223
9471
|
function registerIdeationContextTools(server2) {
|
|
9224
9472
|
server2.tool(
|
|
9225
9473
|
"get_ideation_context",
|
|
9226
|
-
"
|
|
9474
|
+
"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.",
|
|
9227
9475
|
{
|
|
9228
9476
|
project_id: z11.string().uuid().optional().describe("Project ID to scope insights."),
|
|
9229
9477
|
days: z11.number().min(1).max(90).optional().describe("Lookback window for insights. Defaults to 30 days."),
|
|
9230
9478
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional output format. Defaults to text.")
|
|
9231
9479
|
},
|
|
9480
|
+
{
|
|
9481
|
+
title: "Get Ideation Context",
|
|
9482
|
+
readOnlyHint: true,
|
|
9483
|
+
destructiveHint: false,
|
|
9484
|
+
idempotentHint: true,
|
|
9485
|
+
openWorldHint: false
|
|
9486
|
+
},
|
|
9232
9487
|
async ({ project_id, days, response_format }) => {
|
|
9233
9488
|
const supabase = getSupabaseClient();
|
|
9234
9489
|
const userId = await getDefaultUserId();
|
|
@@ -9355,10 +9610,17 @@ function asEnvelope8(data) {
|
|
|
9355
9610
|
function registerCreditsTools(server2) {
|
|
9356
9611
|
server2.tool(
|
|
9357
9612
|
"get_credit_balance",
|
|
9358
|
-
"
|
|
9613
|
+
"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).",
|
|
9359
9614
|
{
|
|
9360
9615
|
response_format: z12.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9361
9616
|
},
|
|
9617
|
+
{
|
|
9618
|
+
title: "Get Credit Balance",
|
|
9619
|
+
readOnlyHint: true,
|
|
9620
|
+
destructiveHint: false,
|
|
9621
|
+
idempotentHint: true,
|
|
9622
|
+
openWorldHint: false
|
|
9623
|
+
},
|
|
9362
9624
|
async ({ response_format }) => {
|
|
9363
9625
|
const supabase = getSupabaseClient();
|
|
9364
9626
|
const userId = await getDefaultUserId();
|
|
@@ -9408,10 +9670,17 @@ Monthly used: ${payload.monthlyUsed}` + (payload.monthlyLimit ? ` / ${payload.mo
|
|
|
9408
9670
|
);
|
|
9409
9671
|
server2.tool(
|
|
9410
9672
|
"get_budget_status",
|
|
9411
|
-
"
|
|
9673
|
+
"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.",
|
|
9412
9674
|
{
|
|
9413
9675
|
response_format: z12.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9414
9676
|
},
|
|
9677
|
+
{
|
|
9678
|
+
title: "Get Budget Status",
|
|
9679
|
+
readOnlyHint: true,
|
|
9680
|
+
destructiveHint: false,
|
|
9681
|
+
idempotentHint: true,
|
|
9682
|
+
openWorldHint: false
|
|
9683
|
+
},
|
|
9415
9684
|
async ({ response_format }) => {
|
|
9416
9685
|
const budget = getCurrentBudgetStatus();
|
|
9417
9686
|
const payload = {
|
|
@@ -9466,11 +9735,18 @@ function asEnvelope9(data) {
|
|
|
9466
9735
|
function registerLoopSummaryTools(server2) {
|
|
9467
9736
|
server2.tool(
|
|
9468
9737
|
"get_loop_summary",
|
|
9469
|
-
"Get a
|
|
9738
|
+
"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.",
|
|
9470
9739
|
{
|
|
9471
9740
|
project_id: z13.string().uuid().optional().describe("Project ID. Defaults to active project context."),
|
|
9472
9741
|
response_format: z13.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9473
9742
|
},
|
|
9743
|
+
{
|
|
9744
|
+
title: "Get Loop Summary",
|
|
9745
|
+
readOnlyHint: true,
|
|
9746
|
+
destructiveHint: false,
|
|
9747
|
+
idempotentHint: true,
|
|
9748
|
+
openWorldHint: false
|
|
9749
|
+
},
|
|
9474
9750
|
async ({ project_id, response_format }) => {
|
|
9475
9751
|
const supabase = getSupabaseClient();
|
|
9476
9752
|
const userId = await getDefaultUserId();
|
|
@@ -9569,6 +9845,13 @@ function registerUsageTools(server2) {
|
|
|
9569
9845
|
{
|
|
9570
9846
|
response_format: z14.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9571
9847
|
},
|
|
9848
|
+
{
|
|
9849
|
+
title: "Get MCP Usage",
|
|
9850
|
+
readOnlyHint: true,
|
|
9851
|
+
destructiveHint: false,
|
|
9852
|
+
idempotentHint: true,
|
|
9853
|
+
openWorldHint: false
|
|
9854
|
+
},
|
|
9572
9855
|
async ({ response_format }) => {
|
|
9573
9856
|
const format = response_format ?? "text";
|
|
9574
9857
|
const supabase = getSupabaseClient();
|
|
@@ -9654,11 +9937,18 @@ function asEnvelope11(data) {
|
|
|
9654
9937
|
function registerAutopilotTools(server2) {
|
|
9655
9938
|
server2.tool(
|
|
9656
9939
|
"list_autopilot_configs",
|
|
9657
|
-
"List
|
|
9940
|
+
"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.",
|
|
9658
9941
|
{
|
|
9659
9942
|
active_only: z15.boolean().optional().describe("If true, only return active configs. Defaults to false (show all)."),
|
|
9660
9943
|
response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9661
9944
|
},
|
|
9945
|
+
{
|
|
9946
|
+
title: "List Autopilot Configs",
|
|
9947
|
+
readOnlyHint: true,
|
|
9948
|
+
destructiveHint: false,
|
|
9949
|
+
idempotentHint: true,
|
|
9950
|
+
openWorldHint: false
|
|
9951
|
+
},
|
|
9662
9952
|
async ({ active_only, response_format }) => {
|
|
9663
9953
|
const format = response_format ?? "text";
|
|
9664
9954
|
const supabase = getSupabaseClient();
|
|
@@ -9736,11 +10026,18 @@ ${"=".repeat(40)}
|
|
|
9736
10026
|
{
|
|
9737
10027
|
config_id: z15.string().uuid().describe("The autopilot config ID to update."),
|
|
9738
10028
|
is_active: z15.boolean().optional().describe("Enable or disable this autopilot config."),
|
|
9739
|
-
schedule_days: z15.array(z15.enum(["mon", "tue", "wed", "thu", "fri", "sat", "sun"])).optional().describe('Days of the week to run (e.g
|
|
10029
|
+
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"]).'),
|
|
9740
10030
|
schedule_time: z15.string().optional().describe('Time to run in HH:MM format (24h, user timezone). E.g., "09:00".'),
|
|
9741
10031
|
max_credits_per_run: z15.number().optional().describe("Maximum credits per execution."),
|
|
9742
10032
|
max_credits_per_week: z15.number().optional().describe("Maximum credits per week.")
|
|
9743
10033
|
},
|
|
10034
|
+
{
|
|
10035
|
+
title: "Update Autopilot Config",
|
|
10036
|
+
readOnlyHint: false,
|
|
10037
|
+
destructiveHint: false,
|
|
10038
|
+
idempotentHint: true,
|
|
10039
|
+
openWorldHint: false
|
|
10040
|
+
},
|
|
9744
10041
|
async ({
|
|
9745
10042
|
config_id,
|
|
9746
10043
|
is_active,
|
|
@@ -9800,10 +10097,17 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
|
|
|
9800
10097
|
);
|
|
9801
10098
|
server2.tool(
|
|
9802
10099
|
"get_autopilot_status",
|
|
9803
|
-
"Get
|
|
10100
|
+
"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.",
|
|
9804
10101
|
{
|
|
9805
10102
|
response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9806
10103
|
},
|
|
10104
|
+
{
|
|
10105
|
+
title: "Get Autopilot Status",
|
|
10106
|
+
readOnlyHint: true,
|
|
10107
|
+
destructiveHint: false,
|
|
10108
|
+
idempotentHint: true,
|
|
10109
|
+
openWorldHint: false
|
|
10110
|
+
},
|
|
9807
10111
|
async ({ response_format }) => {
|
|
9808
10112
|
const format = response_format ?? "text";
|
|
9809
10113
|
const supabase = getSupabaseClient();
|
|
@@ -9919,13 +10223,20 @@ ${content.suggested_hooks.map((h) => ` - ${h}`).join("\n")}`
|
|
|
9919
10223
|
function registerExtractionTools(server2) {
|
|
9920
10224
|
server2.tool(
|
|
9921
10225
|
"extract_url_content",
|
|
9922
|
-
"Extract content from
|
|
10226
|
+
"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.",
|
|
9923
10227
|
{
|
|
9924
10228
|
url: z16.string().url().describe("URL to extract content from"),
|
|
9925
10229
|
extract_type: z16.enum(["auto", "transcript", "article", "product"]).default("auto").describe("Type of extraction"),
|
|
9926
10230
|
include_comments: z16.boolean().default(false).describe("Include top comments (YouTube only)"),
|
|
9927
10231
|
max_results: z16.number().min(1).max(100).default(10).describe("Max comments to include"),
|
|
9928
|
-
response_format: z16.enum(["text", "json"]).default("text")
|
|
10232
|
+
response_format: z16.enum(["text", "json"]).default("text").describe("Response format. Defaults to text.")
|
|
10233
|
+
},
|
|
10234
|
+
{
|
|
10235
|
+
title: "Extract URL Content",
|
|
10236
|
+
readOnlyHint: true,
|
|
10237
|
+
destructiveHint: false,
|
|
10238
|
+
idempotentHint: true,
|
|
10239
|
+
openWorldHint: true
|
|
9929
10240
|
},
|
|
9930
10241
|
async ({
|
|
9931
10242
|
url,
|
|
@@ -10119,9 +10430,9 @@ function asEnvelope13(data) {
|
|
|
10119
10430
|
function registerQualityTools(server2) {
|
|
10120
10431
|
server2.tool(
|
|
10121
10432
|
"quality_check",
|
|
10122
|
-
"Score
|
|
10433
|
+
"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.",
|
|
10123
10434
|
{
|
|
10124
|
-
caption: z17.string().describe("
|
|
10435
|
+
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."),
|
|
10125
10436
|
title: z17.string().optional().describe("Post title (important for YouTube)"),
|
|
10126
10437
|
platforms: z17.array(
|
|
10127
10438
|
z17.enum([
|
|
@@ -10135,11 +10446,18 @@ function registerQualityTools(server2) {
|
|
|
10135
10446
|
"bluesky"
|
|
10136
10447
|
])
|
|
10137
10448
|
).min(1).describe("Target platforms"),
|
|
10138
|
-
threshold: z17.number().min(0).max(35).default(26).describe("Minimum total score to pass"),
|
|
10449
|
+
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."),
|
|
10139
10450
|
brand_keyword: z17.string().optional().describe("Brand keyword for alignment check"),
|
|
10140
|
-
brand_avoid_patterns: z17.array(z17.string()).optional(),
|
|
10141
|
-
custom_banned_terms: z17.array(z17.string()).optional(),
|
|
10142
|
-
response_format: z17.enum(["text", "json"]).default("text")
|
|
10451
|
+
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."),
|
|
10452
|
+
custom_banned_terms: z17.array(z17.string()).optional().describe("Additional banned words beyond the built-in safety list. Useful for industry-specific compliance terms."),
|
|
10453
|
+
response_format: z17.enum(["text", "json"]).default("text").describe("'text' for human-readable report, 'json' for structured scores suitable for pipeline automation.")
|
|
10454
|
+
},
|
|
10455
|
+
{
|
|
10456
|
+
title: "Quality Check",
|
|
10457
|
+
readOnlyHint: true,
|
|
10458
|
+
destructiveHint: false,
|
|
10459
|
+
idempotentHint: true,
|
|
10460
|
+
openWorldHint: false
|
|
10143
10461
|
},
|
|
10144
10462
|
async ({
|
|
10145
10463
|
caption,
|
|
@@ -10206,20 +10524,27 @@ function registerQualityTools(server2) {
|
|
|
10206
10524
|
);
|
|
10207
10525
|
server2.tool(
|
|
10208
10526
|
"quality_check_plan",
|
|
10209
|
-
"
|
|
10527
|
+
"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.",
|
|
10210
10528
|
{
|
|
10211
10529
|
plan: z17.object({
|
|
10212
10530
|
posts: z17.array(
|
|
10213
10531
|
z17.object({
|
|
10214
|
-
id: z17.string(),
|
|
10215
|
-
caption: z17.string(),
|
|
10216
|
-
title: z17.string().optional(),
|
|
10217
|
-
platform: z17.string()
|
|
10532
|
+
id: z17.string().describe("Unique post identifier."),
|
|
10533
|
+
caption: z17.string().describe("Post caption/body text to quality-check."),
|
|
10534
|
+
title: z17.string().optional().describe("Post title (important for YouTube)."),
|
|
10535
|
+
platform: z17.string().describe("Target platform (e.g. instagram, youtube).")
|
|
10218
10536
|
})
|
|
10219
10537
|
)
|
|
10220
|
-
}).passthrough().describe("Content plan with posts array"),
|
|
10221
|
-
threshold: z17.number().min(0).max(35).default(26).describe("Minimum total score to pass"),
|
|
10222
|
-
response_format: z17.enum(["text", "json"]).default("text")
|
|
10538
|
+
}).passthrough().describe("Content plan with posts array."),
|
|
10539
|
+
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."),
|
|
10540
|
+
response_format: z17.enum(["text", "json"]).default("text").describe("Response format. Defaults to text.")
|
|
10541
|
+
},
|
|
10542
|
+
{
|
|
10543
|
+
title: "Quality Check Plan",
|
|
10544
|
+
readOnlyHint: true,
|
|
10545
|
+
destructiveHint: false,
|
|
10546
|
+
idempotentHint: true,
|
|
10547
|
+
openWorldHint: false
|
|
10223
10548
|
},
|
|
10224
10549
|
async ({ plan, threshold, response_format }) => {
|
|
10225
10550
|
const startedAt = Date.now();
|
|
@@ -10401,7 +10726,7 @@ function formatPlanAsText(plan) {
|
|
|
10401
10726
|
function registerPlanningTools(server2) {
|
|
10402
10727
|
server2.tool(
|
|
10403
10728
|
"plan_content_week",
|
|
10404
|
-
"Generate a full
|
|
10729
|
+
"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.",
|
|
10405
10730
|
{
|
|
10406
10731
|
topic: z18.string().describe("Main topic or content theme"),
|
|
10407
10732
|
source_url: z18.string().optional().describe("URL to extract content from (YouTube, article)"),
|
|
@@ -10422,7 +10747,14 @@ function registerPlanningTools(server2) {
|
|
|
10422
10747
|
start_date: z18.string().optional().describe("ISO date, defaults to tomorrow"),
|
|
10423
10748
|
brand_voice: z18.string().optional().describe("Override brand voice description"),
|
|
10424
10749
|
project_id: z18.string().optional().describe("Project ID for brand/insights context"),
|
|
10425
|
-
response_format: z18.enum(["text", "json"]).default("json")
|
|
10750
|
+
response_format: z18.enum(["text", "json"]).default("json").describe("Response format. Defaults to json.")
|
|
10751
|
+
},
|
|
10752
|
+
{
|
|
10753
|
+
title: "Plan Content Week",
|
|
10754
|
+
readOnlyHint: false,
|
|
10755
|
+
destructiveHint: false,
|
|
10756
|
+
idempotentHint: false,
|
|
10757
|
+
openWorldHint: true
|
|
10426
10758
|
},
|
|
10427
10759
|
async ({
|
|
10428
10760
|
topic,
|
|
@@ -10737,15 +11069,22 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10737
11069
|
);
|
|
10738
11070
|
server2.tool(
|
|
10739
11071
|
"save_content_plan",
|
|
10740
|
-
"
|
|
11072
|
+
"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.",
|
|
10741
11073
|
{
|
|
10742
11074
|
plan: z18.object({
|
|
10743
|
-
topic: z18.string(),
|
|
10744
|
-
posts: z18.array(z18.record(z18.string(), z18.unknown()))
|
|
10745
|
-
}).passthrough(),
|
|
10746
|
-
project_id: z18.string().uuid().optional(),
|
|
10747
|
-
status: z18.enum(["draft", "in_review", "approved", "scheduled", "completed"]).default("draft"),
|
|
10748
|
-
response_format: z18.enum(["text", "json"]).default("json")
|
|
11075
|
+
topic: z18.string().describe("Content plan topic or theme."),
|
|
11076
|
+
posts: z18.array(z18.record(z18.string(), z18.unknown())).describe("Array of post objects to save.")
|
|
11077
|
+
}).passthrough().describe("Content plan object with topic and posts array."),
|
|
11078
|
+
project_id: z18.string().uuid().optional().describe("Project ID. Defaults to active project context."),
|
|
11079
|
+
status: z18.enum(["draft", "in_review", "approved", "scheduled", "completed"]).default("draft").describe("Initial plan status. Defaults to draft."),
|
|
11080
|
+
response_format: z18.enum(["text", "json"]).default("json").describe("Response format. Defaults to json.")
|
|
11081
|
+
},
|
|
11082
|
+
{
|
|
11083
|
+
title: "Save Content Plan",
|
|
11084
|
+
readOnlyHint: false,
|
|
11085
|
+
destructiveHint: false,
|
|
11086
|
+
idempotentHint: false,
|
|
11087
|
+
openWorldHint: false
|
|
10749
11088
|
},
|
|
10750
11089
|
async ({ plan, project_id, status, response_format }) => {
|
|
10751
11090
|
const startedAt = Date.now();
|
|
@@ -10843,10 +11182,17 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10843
11182
|
);
|
|
10844
11183
|
server2.tool(
|
|
10845
11184
|
"get_content_plan",
|
|
10846
|
-
"Retrieve a
|
|
11185
|
+
"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.",
|
|
10847
11186
|
{
|
|
10848
11187
|
plan_id: z18.string().uuid().describe("Persisted content plan ID"),
|
|
10849
|
-
response_format: z18.enum(["text", "json"]).default("json")
|
|
11188
|
+
response_format: z18.enum(["text", "json"]).default("json").describe("Response format. Defaults to json.")
|
|
11189
|
+
},
|
|
11190
|
+
{
|
|
11191
|
+
title: "Get Content Plan",
|
|
11192
|
+
readOnlyHint: true,
|
|
11193
|
+
destructiveHint: false,
|
|
11194
|
+
idempotentHint: true,
|
|
11195
|
+
openWorldHint: false
|
|
10850
11196
|
},
|
|
10851
11197
|
async ({ plan_id, response_format }) => {
|
|
10852
11198
|
const supabase = getSupabaseClient();
|
|
@@ -10911,25 +11257,32 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10911
11257
|
);
|
|
10912
11258
|
server2.tool(
|
|
10913
11259
|
"update_content_plan",
|
|
10914
|
-
"
|
|
11260
|
+
"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.",
|
|
10915
11261
|
{
|
|
10916
|
-
plan_id: z18.string().uuid(),
|
|
11262
|
+
plan_id: z18.string().uuid().describe("Content plan ID to update."),
|
|
10917
11263
|
post_updates: z18.array(
|
|
10918
11264
|
z18.object({
|
|
10919
|
-
post_id: z18.string(),
|
|
10920
|
-
caption: z18.string().optional(),
|
|
10921
|
-
title: z18.string().optional(),
|
|
10922
|
-
hashtags: z18.array(z18.string()).optional(),
|
|
10923
|
-
hook: z18.string().optional(),
|
|
10924
|
-
angle: z18.string().optional(),
|
|
10925
|
-
visual_direction: z18.string().optional(),
|
|
10926
|
-
media_url: z18.string().optional(),
|
|
10927
|
-
schedule_at: z18.string().optional(),
|
|
10928
|
-
platform: z18.string().optional(),
|
|
10929
|
-
status: z18.enum(["approved", "rejected", "needs_edit"]).optional()
|
|
11265
|
+
post_id: z18.string().describe("ID of the post to update within this plan."),
|
|
11266
|
+
caption: z18.string().optional().describe("Revised caption/body text."),
|
|
11267
|
+
title: z18.string().optional().describe("Revised post title."),
|
|
11268
|
+
hashtags: z18.array(z18.string()).optional().describe("Revised hashtags array."),
|
|
11269
|
+
hook: z18.string().optional().describe("Revised attention-grabbing opening line."),
|
|
11270
|
+
angle: z18.string().optional().describe("Revised content angle or perspective."),
|
|
11271
|
+
visual_direction: z18.string().optional().describe("Revised visual/media direction notes."),
|
|
11272
|
+
media_url: z18.string().optional().describe("Revised media URL (public or R2 signed URL)."),
|
|
11273
|
+
schedule_at: z18.string().optional().describe("Revised ISO 8601 UTC publish datetime."),
|
|
11274
|
+
platform: z18.string().optional().describe("Revised target platform."),
|
|
11275
|
+
status: z18.enum(["approved", "rejected", "needs_edit"]).optional().describe("Review status for this post.")
|
|
10930
11276
|
})
|
|
10931
|
-
).min(1),
|
|
10932
|
-
response_format: z18.enum(["text", "json"]).default("json")
|
|
11277
|
+
).min(1).describe("Array of post-level updates to apply."),
|
|
11278
|
+
response_format: z18.enum(["text", "json"]).default("json").describe("Response format. Defaults to json.")
|
|
11279
|
+
},
|
|
11280
|
+
{
|
|
11281
|
+
title: "Update Content Plan",
|
|
11282
|
+
readOnlyHint: false,
|
|
11283
|
+
destructiveHint: false,
|
|
11284
|
+
idempotentHint: true,
|
|
11285
|
+
openWorldHint: false
|
|
10933
11286
|
},
|
|
10934
11287
|
async ({ plan_id, post_updates, response_format }) => {
|
|
10935
11288
|
const supabase = getSupabaseClient();
|
|
@@ -11030,10 +11383,17 @@ ${rawText.slice(0, 1e3)}`
|
|
|
11030
11383
|
);
|
|
11031
11384
|
server2.tool(
|
|
11032
11385
|
"submit_content_plan_for_approval",
|
|
11033
|
-
"
|
|
11386
|
+
"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.",
|
|
11387
|
+
{
|
|
11388
|
+
plan_id: z18.string().uuid().describe("Content plan ID to submit for review."),
|
|
11389
|
+
response_format: z18.enum(["text", "json"]).default("json").describe("Response format. Defaults to json.")
|
|
11390
|
+
},
|
|
11034
11391
|
{
|
|
11035
|
-
|
|
11036
|
-
|
|
11392
|
+
title: "Submit Plan for Approval",
|
|
11393
|
+
readOnlyHint: false,
|
|
11394
|
+
destructiveHint: false,
|
|
11395
|
+
idempotentHint: true,
|
|
11396
|
+
openWorldHint: false
|
|
11037
11397
|
},
|
|
11038
11398
|
async ({ plan_id, response_format }) => {
|
|
11039
11399
|
const supabase = getSupabaseClient();
|
|
@@ -11158,21 +11518,28 @@ async function assertProjectAccess(supabase, userId, projectId) {
|
|
|
11158
11518
|
function registerPlanApprovalTools(server2) {
|
|
11159
11519
|
server2.tool(
|
|
11160
11520
|
"create_plan_approvals",
|
|
11161
|
-
"Create
|
|
11521
|
+
"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.",
|
|
11162
11522
|
{
|
|
11163
11523
|
plan_id: z19.string().uuid().describe("Content plan ID"),
|
|
11164
11524
|
posts: z19.array(
|
|
11165
11525
|
z19.object({
|
|
11166
|
-
id: z19.string(),
|
|
11167
|
-
platform: z19.string().optional(),
|
|
11168
|
-
caption: z19.string().optional(),
|
|
11169
|
-
title: z19.string().optional(),
|
|
11170
|
-
media_url: z19.string().optional(),
|
|
11171
|
-
schedule_at: z19.string().optional()
|
|
11526
|
+
id: z19.string().describe("Unique post identifier from the content plan."),
|
|
11527
|
+
platform: z19.string().optional().describe("Target platform (e.g. instagram, youtube)."),
|
|
11528
|
+
caption: z19.string().optional().describe("Post caption/body text."),
|
|
11529
|
+
title: z19.string().optional().describe("Post title, used by YouTube and LinkedIn articles."),
|
|
11530
|
+
media_url: z19.string().optional().describe("Public or R2 signed URL for the post media."),
|
|
11531
|
+
schedule_at: z19.string().optional().describe("ISO 8601 UTC datetime to publish (e.g. 2026-03-20T14:00:00Z).")
|
|
11172
11532
|
}).passthrough()
|
|
11173
11533
|
).min(1).describe("Posts to create approval entries for."),
|
|
11174
11534
|
project_id: z19.string().uuid().optional().describe("Project ID. Defaults to active project context."),
|
|
11175
|
-
response_format: z19.enum(["text", "json"]).optional()
|
|
11535
|
+
response_format: z19.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
11536
|
+
},
|
|
11537
|
+
{
|
|
11538
|
+
title: "Create Plan Approvals",
|
|
11539
|
+
readOnlyHint: false,
|
|
11540
|
+
destructiveHint: false,
|
|
11541
|
+
idempotentHint: false,
|
|
11542
|
+
openWorldHint: false
|
|
11176
11543
|
},
|
|
11177
11544
|
async ({ plan_id, posts, project_id, response_format }) => {
|
|
11178
11545
|
const supabase = getSupabaseClient();
|
|
@@ -11252,8 +11619,15 @@ function registerPlanApprovalTools(server2) {
|
|
|
11252
11619
|
"List MCP-native approval items for a specific content plan.",
|
|
11253
11620
|
{
|
|
11254
11621
|
plan_id: z19.string().uuid().describe("Content plan ID"),
|
|
11255
|
-
status: z19.enum(["pending", "approved", "rejected", "edited"]).optional(),
|
|
11256
|
-
response_format: z19.enum(["text", "json"]).optional()
|
|
11622
|
+
status: z19.enum(["pending", "approved", "rejected", "edited"]).optional().describe("Filter approvals by status. Omit to return all statuses."),
|
|
11623
|
+
response_format: z19.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
11624
|
+
},
|
|
11625
|
+
{
|
|
11626
|
+
title: "List Plan Approvals",
|
|
11627
|
+
readOnlyHint: true,
|
|
11628
|
+
destructiveHint: false,
|
|
11629
|
+
idempotentHint: true,
|
|
11630
|
+
openWorldHint: false
|
|
11257
11631
|
},
|
|
11258
11632
|
async ({ plan_id, status, response_format }) => {
|
|
11259
11633
|
const supabase = getSupabaseClient();
|
|
@@ -11320,10 +11694,19 @@ function registerPlanApprovalTools(server2) {
|
|
|
11320
11694
|
"Approve, reject, or edit a pending plan approval item.",
|
|
11321
11695
|
{
|
|
11322
11696
|
approval_id: z19.string().uuid().describe("Approval item ID"),
|
|
11323
|
-
decision: z19.enum(["approved", "rejected", "edited"]),
|
|
11324
|
-
edited_post: z19.record(z19.string(), z19.unknown()).optional()
|
|
11325
|
-
|
|
11326
|
-
|
|
11697
|
+
decision: z19.enum(["approved", "rejected", "edited"]).describe("Approval decision for this post."),
|
|
11698
|
+
edited_post: z19.record(z19.string(), z19.unknown()).optional().describe(
|
|
11699
|
+
'Revised post fields when decision is "edited" (e.g. {caption: "...", hashtags: [...]}).'
|
|
11700
|
+
),
|
|
11701
|
+
reason: z19.string().max(1e3).optional().describe("Optional reason for the decision, visible to the plan author."),
|
|
11702
|
+
response_format: z19.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
11703
|
+
},
|
|
11704
|
+
{
|
|
11705
|
+
title: "Respond to Plan Approval",
|
|
11706
|
+
readOnlyHint: false,
|
|
11707
|
+
destructiveHint: false,
|
|
11708
|
+
idempotentHint: true,
|
|
11709
|
+
openWorldHint: false
|
|
11327
11710
|
},
|
|
11328
11711
|
async ({ approval_id, decision, edited_post, reason, response_format }) => {
|
|
11329
11712
|
const supabase = getSupabaseClient();
|
|
@@ -11402,7 +11785,7 @@ import { z as z20 } from "zod";
|
|
|
11402
11785
|
function registerDiscoveryTools(server2) {
|
|
11403
11786
|
server2.tool(
|
|
11404
11787
|
"search_tools",
|
|
11405
|
-
'Search
|
|
11788
|
+
'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.',
|
|
11406
11789
|
{
|
|
11407
11790
|
query: z20.string().optional().describe("Search query to filter tools by name or description"),
|
|
11408
11791
|
module: z20.string().optional().describe('Filter by module name (e.g. "planning", "content", "analytics")'),
|
|
@@ -11411,6 +11794,13 @@ function registerDiscoveryTools(server2) {
|
|
|
11411
11794
|
'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
|
|
11412
11795
|
)
|
|
11413
11796
|
},
|
|
11797
|
+
{
|
|
11798
|
+
title: "Search Tools",
|
|
11799
|
+
readOnlyHint: true,
|
|
11800
|
+
destructiveHint: false,
|
|
11801
|
+
idempotentHint: true,
|
|
11802
|
+
openWorldHint: false
|
|
11803
|
+
},
|
|
11414
11804
|
async ({ query, module, scope, detail }) => {
|
|
11415
11805
|
let results = [...TOOL_CATALOG];
|
|
11416
11806
|
if (query) {
|