@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/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("No user ID available. Set SOCIALNEURON_USER_ID or authenticate via API key.");
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((expiresMs - Date.now()) / (1e3 * 60 * 60 * 24));
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("[MCP] Migrate to API key auth: npx @socialneuron/mcp-server setup");
464
- console.error("[MCP] Then remove SOCIALNEURON_SERVICE_KEY from your environment.");
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
- "Generate AI-powered content (scripts, captions, hooks, blog posts) using Google Gemini or Anthropic Claude. Provide a detailed prompt describing what you need, choose the content type, and optionally specify a target platform and brand voice guidelines.",
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
- "Detailed prompt describing the content to generate. Include context like topic, angle, audience, and any specific requirements."
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
- 'Brand voice guidelines to follow (e.g. "professional and empathetic", "playful and Gen-Z"). Leave blank to use a neutral tone.'
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
- "Fetch current trending topics from YouTube, Google Trends, RSS feeds, or a custom URL. Results are cached for efficiency. Use this to discover what is popular right now for content ideation.",
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
- "Adapt existing content for a different social media platform. Rewrites content to match the target platform's norms including character limits, hashtag style, tone, and CTA conventions.",
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.4.2";
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. This is an async operation -- it returns a job_id immediately. Use check_status to poll for completion. Supports Veo 3, Runway, Sora 2, Kling 2.6, and Kling 3.0 models via the Kie.ai API.",
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
- "Text prompt describing the video to generate. Be specific about visual style, camera angles, movement, lighting, and mood."
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 generation model. veo3-fast is fastest (~60s), veo3-quality is highest quality (~120s). sora2-pro is OpenAI Sora premium tier. kling-3 is Kling 3.0 standard (4K, 15s, audio). kling-3-pro is Kling 3.0 pro (higher quality)."
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
- "Aspect ratio. 16:9 for landscape/YouTube, 9:16 for vertical/Reels/TikTok, 1:1 for square. Defaults to 16:9."
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. This is an async operation -- it returns a job_id immediately. Use check_status to poll for completion. Supports Midjourney, Imagen 4, Flux, GPT-4o Image, and Seedream models.",
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
- "Check the status of an async generation job (video or image). Returns the current status, progress percentage, and result URL when complete. Call this tool periodically after generate_video or generate_image.",
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
- "Generate a structured scene-by-scene storyboard for video production. Returns an array of StoryboardFrames with prompts, durations, captions, and voiceover text. Use the output to drive image/video generation.",
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 professional voiceover audio file using ElevenLabs TTS. Returns an audio URL stored in R2. Use this for narration in video production.",
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 an Instagram carousel with AI-powered slide content. Supports multiple templates including Hormozi-style authority carousels. Returns slide data (headlines, body text, emphasis words). Use schedule_post with media_urls to publish the carousel to Instagram.",
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
- 'The carousel topic/subject. Be specific about the angle or hook. Example: "5 reasons your startup will fail in 2026"'
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
- "Schedule or immediately publish a post to one or more social media platforms. Requires the target platforms to have active OAuth connections configured in Social Neuron Settings. Supports YouTube, TikTok, Instagram, Facebook, LinkedIn, Twitter, Threads, and Bluesky. For Instagram carousels, provide media_urls (2-10 image URLs) and set media_type to CAROUSEL_ALBUM.",
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
- 'Hashtags to append to the caption. Include or omit the "#" prefix.'
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
- "List all social media accounts connected to Social Neuron via OAuth. Shows which platforms are available for posting.",
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 posts from Social Neuron. Shows status, platform, title, and timestamps. Useful for checking what has been published or scheduled recently.",
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 optimal posting time slots based on best posting times and existing schedule. Returns non-conflicting slots sorted by engagement score.",
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("Number of days to look back. Defaults to 30. Max 365."),
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
- "Trigger an analytics refresh for all recently posted content across all connected platforms. Queues analytics fetch jobs for posts from the last 7 days.",
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
- response_format: z5.enum(["text", "json"]).optional()
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
- style: z5.array(z5.string()).optional(),
4620
- avoid_patterns: z5.array(z5.string()).optional(),
4621
- hashtag_strategy: z5.string().max(300).optional(),
4622
- cta_style: z5.string().max(300).optional(),
4623
- response_format: z5.enum(["text", "json"]).optional()
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
- "List YouTube comments. Without a video_id, returns recent comments across all channel videos. With a video_id, returns comments for that specific video.",
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 token from a previous list_comments call."),
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. Requires the parent comment ID and reply text.",
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
- "Get synthesized ideation context from performance insights. Returns the same prompt-injection context used by ideation generation.",
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
- "Get current subscription credit balance and plan.",
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
- "Get current MCP run budget consumption for credits/assets.",
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 one-call dashboard summary of the feedback loop state (brand profile, recent content, and current insights).",
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 all autopilot configurations for your account. Shows active schedules, associated recipes, credit budgets, and last run times.",
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., ["mon", "wed", "fri"]).'),
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 the current status of your autopilot system, including active configs, recent runs, and next scheduled execution.",
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 a URL (YouTube video transcript, article text, product page). Routes to scrape-youtube for YouTube URLs or fetch-url-content for other URLs.",
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 a single post's content quality across 7 categories (Hook Strength, Message Clarity, Platform Fit, Brand Alignment, Novelty, CTA Strength, Safety/Claims). Returns pass/fail with per-category scores.",
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("Post caption/body text"),
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
- "Run quality checks on all posts in a content plan. Returns per-post scores and aggregate summary.",
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 week's content plan with platform-specific drafts. Takes a topic or source URL, loads brand context and performance insights, and returns structured posts with hooks, angles, captions, and suggested schedule times.",
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
- "Persist a content plan payload for later review, approvals, and scheduling.",
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 persisted content plan by ID.",
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
- "Update individual posts in a persisted content plan.",
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
- "Create pending approval items for each post in a plan and mark plan status as in_review.",
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 pending approval rows for each post in a content plan.",
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
- reason: z19.string().max(1e3).optional(),
8408
- response_format: z19.enum(["text", "json"]).optional()
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 and discover available MCP tools. Use detail level to control token usage: "name" (~50 tokens), "summary" (~500 tokens), "full" (complete schemas).',
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 = 6e4;
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);