@socialneuron/mcp-server 1.4.1 → 1.5.0

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 CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to `@socialneuron/mcp-server` will be documented in this file.
4
4
 
5
+ ## [1.5.0] - 2026-03-19
6
+
7
+ ### Changed
8
+ - **LLM-optimized tool descriptions**: Rewrote 27 tool descriptions and enriched 15 parameters for agent comprehension. Every tool now answers "when to call", "what to pass", and "what comes next" — following Arcade ToolBench patterns (Tool Description, Constrained Input, Dependency Hint, Performance Hint).
9
+ - **API key cache TTL**: Reduced from 60s to 10s to limit revocation exposure window.
10
+ - **OAuth issuer URL**: Production metadata now derives from `MCP_SERVER_URL` instead of defaulting to localhost.
11
+ - **SECURITY.md**: Updated supported versions, added scanner false-positive documentation.
12
+ - **CLI setup URL**: Fixed `app.socialneuron.com` → `www.socialneuron.com`.
13
+
14
+ ### Dependencies
15
+ - `@supabase/supabase-js` 2.98.0 → 2.99.2
16
+ - `open` 10.0.0 → 11.0.0 (requires Node.js 20+)
17
+ - `posthog-node` 5.28.1 → 5.28.3
18
+ - `vitest` 3.2.4 → 4.1.0
19
+ - `esbuild` 0.27.3 → 0.27.4
20
+ - `@types/node` 25.4.0 → 25.5.0
21
+
5
22
  ## [1.4.0] - 2026-03-13
6
23
 
7
24
  ### Changed
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."
@@ -1025,7 +1033,7 @@ Content Type: ${content_type}`;
1025
1033
  );
1026
1034
  server.tool(
1027
1035
  "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.",
1036
+ '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
1037
  {
1030
1038
  source: z.enum(["youtube", "google_trends", "rss", "url"]).describe(
1031
1039
  '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.'
@@ -1111,7 +1119,7 @@ Content Type: ${content_type}`;
1111
1119
  );
1112
1120
  server.tool(
1113
1121
  "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.",
1122
+ "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
1123
  {
1116
1124
  content: z.string().max(5e3).describe(
1117
1125
  "The content to adapt. Can be a caption, script, blog excerpt, or any text."
@@ -1304,7 +1312,7 @@ function sanitizeDbError(error) {
1304
1312
  init_request_context();
1305
1313
 
1306
1314
  // src/lib/version.ts
1307
- var MCP_VERSION = "1.4.1";
1315
+ var MCP_VERSION = "1.5.0";
1308
1316
 
1309
1317
  // src/tools/content.ts
1310
1318
  var MAX_CREDITS_PER_RUN = Math.max(
@@ -1412,10 +1420,10 @@ function checkAssetBudget() {
1412
1420
  function registerContentTools(server) {
1413
1421
  server.tool(
1414
1422
  "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.",
1423
+ "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
1424
  {
1417
1425
  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."
1426
+ '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
1427
  ),
1420
1428
  model: z2.enum([
1421
1429
  "veo3-fast",
@@ -1427,13 +1435,13 @@ function registerContentTools(server) {
1427
1435
  "kling-3",
1428
1436
  "kling-3-pro"
1429
1437
  ]).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)."
1438
+ "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
1439
  ),
1432
1440
  duration: z2.number().min(3).max(30).optional().describe(
1433
1441
  "Video duration in seconds. kling: 5-30s, kling-3/kling-3-pro: 3-15s, sora2: 10-15s. Defaults to 5 seconds."
1434
1442
  ),
1435
1443
  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."
1444
+ "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
1445
  ),
1438
1446
  enable_audio: z2.boolean().optional().describe(
1439
1447
  "Enable native audio generation. Kling 2.6: doubles cost. Kling 3.0: 50% more (std 30/sec, pro 40/sec). 5+ languages."
@@ -1620,7 +1628,7 @@ function registerContentTools(server) {
1620
1628
  );
1621
1629
  server.tool(
1622
1630
  "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.",
1631
+ "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
1632
  {
1625
1633
  prompt: z2.string().max(2e3).describe(
1626
1634
  "Text prompt describing the image to generate. Be specific about style, composition, colors, lighting, and subject matter."
@@ -1800,7 +1808,7 @@ function registerContentTools(server) {
1800
1808
  );
1801
1809
  server.tool(
1802
1810
  "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.",
1811
+ '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
1812
  {
1805
1813
  job_id: z2.string().describe(
1806
1814
  "The job ID returned by generate_video or generate_image. This is the asyncJobId or taskId value."
@@ -1976,7 +1984,7 @@ function registerContentTools(server) {
1976
1984
  );
1977
1985
  server.tool(
1978
1986
  "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.",
1987
+ "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
1988
  {
1981
1989
  concept: z2.string().max(2e3).describe(
1982
1990
  '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").'
@@ -2163,7 +2171,7 @@ Return ONLY valid JSON in this exact format:
2163
2171
  );
2164
2172
  server.tool(
2165
2173
  "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.",
2174
+ "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
2175
  {
2168
2176
  text: z2.string().max(5e3).describe("The script/text to convert to speech."),
2169
2177
  voice: z2.enum([
@@ -2314,10 +2322,10 @@ Return ONLY valid JSON in this exact format:
2314
2322
  );
2315
2323
  server.tool(
2316
2324
  "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.",
2325
+ "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
2326
  {
2319
2327
  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"'
2328
+ '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
2329
  ),
2322
2330
  template_id: z2.enum([
2323
2331
  "educational-series",
@@ -2645,14 +2653,12 @@ function asEnvelope2(data) {
2645
2653
  function registerDistributionTools(server) {
2646
2654
  server.tool(
2647
2655
  "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.",
2656
+ '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
2657
  {
2650
2658
  media_url: z3.string().optional().describe(
2651
2659
  "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
2660
  ),
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
- ),
2661
+ 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
2662
  media_type: z3.enum(["IMAGE", "VIDEO", "CAROUSEL_ALBUM"]).optional().describe(
2657
2663
  "Media type. Set to CAROUSEL_ALBUM with media_urls for Instagram carousels. Default: auto-detected from media_url."
2658
2664
  ),
@@ -2668,16 +2674,10 @@ function registerDistributionTools(server) {
2668
2674
  "threads",
2669
2675
  "bluesky"
2670
2676
  ])
2671
- ).min(1).describe(
2672
- "Target platforms to post to. Each must have an active OAuth connection."
2673
- ),
2677
+ ).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
2678
  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
- ),
2679
+ 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"].'),
2680
+ 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
2681
  project_id: z3.string().optional().describe("Social Neuron project ID to associate this post with."),
2682
2682
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
2683
2683
  attribution: z3.boolean().optional().describe(
@@ -2831,7 +2831,7 @@ Created with Social Neuron`;
2831
2831
  );
2832
2832
  server.tool(
2833
2833
  "list_connected_accounts",
2834
- "List all social media accounts connected to Social Neuron via OAuth. Shows which platforms are available for posting.",
2834
+ "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
2835
  {
2836
2836
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
2837
2837
  },
@@ -2896,7 +2896,7 @@ Created with Social Neuron`;
2896
2896
  );
2897
2897
  server.tool(
2898
2898
  "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.",
2899
+ "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
2900
  {
2901
2901
  platform: z3.enum([
2902
2902
  "youtube",
@@ -3735,7 +3735,7 @@ function registerAnalyticsTools(server) {
3735
3735
  "threads",
3736
3736
  "bluesky"
3737
3737
  ]).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."),
3738
+ 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
3739
  content_id: z4.string().uuid().optional().describe(
3740
3740
  "Filter to a specific content_history ID to see performance of one piece of content."
3741
3741
  ),
@@ -3915,7 +3915,7 @@ function registerAnalyticsTools(server) {
3915
3915
  );
3916
3916
  server.tool(
3917
3917
  "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.",
3918
+ "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
3919
  {
3920
3920
  response_format: z4.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
3921
3921
  },
@@ -5845,13 +5845,11 @@ function asEnvelope6(data) {
5845
5845
  function registerCommentsTools(server) {
5846
5846
  server.tool(
5847
5847
  "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.",
5848
+ '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
5849
  {
5850
- video_id: z10.string().optional().describe(
5851
- "YouTube video ID. If omitted, returns comments across all channel videos."
5852
- ),
5850
+ 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
5851
  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."),
5852
+ page_token: z10.string().optional().describe("Pagination cursor from previous list_comments response nextPageToken field. Omit for first page of results."),
5855
5853
  response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
5856
5854
  },
5857
5855
  async ({ video_id, max_results, page_token, response_format }) => {
@@ -5916,7 +5914,7 @@ function registerCommentsTools(server) {
5916
5914
  );
5917
5915
  server.tool(
5918
5916
  "reply_to_comment",
5919
- "Reply to a YouTube comment. Requires the parent comment ID and reply text.",
5917
+ "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
5918
  {
5921
5919
  parent_id: z10.string().describe(
5922
5920
  "The ID of the parent comment to reply to (from list_comments)."
@@ -6447,7 +6445,7 @@ function asEnvelope8(data) {
6447
6445
  function registerCreditsTools(server) {
6448
6446
  server.tool(
6449
6447
  "get_credit_balance",
6450
- "Get current subscription credit balance and plan.",
6448
+ "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
6449
  {
6452
6450
  response_format: z12.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6453
6451
  },
@@ -6500,7 +6498,7 @@ Monthly used: ${payload.monthlyUsed}` + (payload.monthlyLimit ? ` / ${payload.mo
6500
6498
  );
6501
6499
  server.tool(
6502
6500
  "get_budget_status",
6503
- "Get current MCP run budget consumption for credits/assets.",
6501
+ "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
6502
  {
6505
6503
  response_format: z12.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6506
6504
  },
@@ -6743,7 +6741,7 @@ function asEnvelope11(data) {
6743
6741
  function registerAutopilotTools(server) {
6744
6742
  server.tool(
6745
6743
  "list_autopilot_configs",
6746
- "List all autopilot configurations for your account. Shows active schedules, associated recipes, credit budgets, and last run times.",
6744
+ "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
6745
  {
6748
6746
  active_only: z15.boolean().optional().describe("If true, only return active configs. Defaults to false (show all)."),
6749
6747
  response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
@@ -6889,7 +6887,7 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
6889
6887
  );
6890
6888
  server.tool(
6891
6889
  "get_autopilot_status",
6892
- "Get the current status of your autopilot system, including active configs, recent runs, and next scheduled execution.",
6890
+ "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
6891
  {
6894
6892
  response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6895
6893
  },
@@ -7006,7 +7004,7 @@ ${content.suggested_hooks.map((h) => ` - ${h}`).join("\n")}`
7006
7004
  function registerExtractionTools(server) {
7007
7005
  server.tool(
7008
7006
  "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.",
7007
+ "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
7008
  {
7011
7009
  url: z16.string().url().describe("URL to extract content from"),
7012
7010
  extract_type: z16.enum(["auto", "transcript", "article", "product"]).default("auto").describe("Type of extraction"),
@@ -7204,9 +7202,9 @@ function asEnvelope13(data) {
7204
7202
  function registerQualityTools(server) {
7205
7203
  server.tool(
7206
7204
  "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.",
7205
+ "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
7206
  {
7209
- caption: z17.string().describe("Post caption/body text"),
7207
+ 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
7208
  title: z17.string().optional().describe("Post title (important for YouTube)"),
7211
7209
  platforms: z17.array(
7212
7210
  z17.enum([
@@ -7220,7 +7218,7 @@ function registerQualityTools(server) {
7220
7218
  "bluesky"
7221
7219
  ])
7222
7220
  ).min(1).describe("Target platforms"),
7223
- threshold: z17.number().min(0).max(35).default(26).describe("Minimum total score to pass"),
7221
+ 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
7222
  brand_keyword: z17.string().optional().describe("Brand keyword for alignment check"),
7225
7223
  brand_avoid_patterns: z17.array(z17.string()).optional(),
7226
7224
  custom_banned_terms: z17.array(z17.string()).optional(),
@@ -7291,7 +7289,7 @@ function registerQualityTools(server) {
7291
7289
  );
7292
7290
  server.tool(
7293
7291
  "quality_check_plan",
7294
- "Run quality checks on all posts in a content plan. Returns per-post scores and aggregate summary.",
7292
+ "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
7293
  {
7296
7294
  plan: z17.object({
7297
7295
  posts: z17.array(
@@ -7303,7 +7301,7 @@ function registerQualityTools(server) {
7303
7301
  })
7304
7302
  )
7305
7303
  }).passthrough().describe("Content plan with posts array"),
7306
- threshold: z17.number().min(0).max(35).default(26).describe("Minimum total score to pass"),
7304
+ 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."),
7307
7305
  response_format: z17.enum(["text", "json"]).default("text")
7308
7306
  },
7309
7307
  async ({ plan, threshold, response_format }) => {
@@ -7484,7 +7482,7 @@ function formatPlanAsText(plan) {
7484
7482
  function registerPlanningTools(server) {
7485
7483
  server.tool(
7486
7484
  "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.",
7485
+ "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
7486
  {
7489
7487
  topic: z18.string().describe("Main topic or content theme"),
7490
7488
  source_url: z18.string().optional().describe("URL to extract content from (YouTube, article)"),
@@ -7820,7 +7818,7 @@ ${rawText.slice(0, 1e3)}`
7820
7818
  );
7821
7819
  server.tool(
7822
7820
  "save_content_plan",
7823
- "Persist a content plan payload for later review, approvals, and scheduling.",
7821
+ "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
7822
  {
7825
7823
  plan: z18.object({
7826
7824
  topic: z18.string(),
@@ -8833,7 +8831,7 @@ function searchTools(query) {
8833
8831
  function registerDiscoveryTools(server) {
8834
8832
  server.tool(
8835
8833
  "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).',
8834
+ '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
8835
  {
8838
8836
  query: z20.string().optional().describe("Search query to filter tools by name or description"),
8839
8837
  module: z20.string().optional().describe('Filter by module name (e.g. "planning", "content", "analytics")'),
@@ -8960,7 +8958,7 @@ function getJWKS(supabaseUrl) {
8960
8958
  return jwks;
8961
8959
  }
8962
8960
  var apiKeyCache = /* @__PURE__ */ new Map();
8963
- var API_KEY_CACHE_TTL_MS = 6e4;
8961
+ var API_KEY_CACHE_TTL_MS = 1e4;
8964
8962
  function createTokenVerifier(options) {
8965
8963
  const { supabaseUrl, supabaseAnonKey } = options;
8966
8964
  return {
@@ -9061,7 +9059,30 @@ var PORT = parseInt(process.env.PORT ?? "8080", 10);
9061
9059
  var SUPABASE_URL2 = process.env.SUPABASE_URL ?? "";
9062
9060
  var SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY ?? "";
9063
9061
  var MCP_SERVER_URL = process.env.MCP_SERVER_URL ?? `http://localhost:${PORT}/mcp`;
9062
+ var APP_BASE_URL = process.env.APP_BASE_URL ?? "https://www.socialneuron.com";
9064
9063
  var NODE_ENV = process.env.NODE_ENV ?? "development";
9064
+ function deriveOAuthIssuerUrl() {
9065
+ if (process.env.OAUTH_ISSUER_URL) {
9066
+ return process.env.OAUTH_ISSUER_URL;
9067
+ }
9068
+ try {
9069
+ const mcpUrl = new URL(MCP_SERVER_URL);
9070
+ const isLocalhost = mcpUrl.hostname === "localhost" || mcpUrl.hostname === "127.0.0.1";
9071
+ if (isLocalhost) {
9072
+ if (NODE_ENV === "development") {
9073
+ return `${mcpUrl.protocol}//${mcpUrl.host}`;
9074
+ }
9075
+ } else {
9076
+ return `${mcpUrl.protocol}//${mcpUrl.host}`;
9077
+ }
9078
+ } catch {
9079
+ }
9080
+ if (APP_BASE_URL && !APP_BASE_URL.includes("localhost")) {
9081
+ return APP_BASE_URL;
9082
+ }
9083
+ return "https://mcp.socialneuron.com";
9084
+ }
9085
+ var OAUTH_ISSUER_URL = deriveOAuthIssuerUrl();
9065
9086
  if (!SUPABASE_URL2 || !SUPABASE_ANON_KEY) {
9066
9087
  console.error("[MCP HTTP] Missing SUPABASE_URL or SUPABASE_ANON_KEY");
9067
9088
  process.exit(1);
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var MCP_VERSION;
14
14
  var init_version = __esm({
15
15
  "src/lib/version.ts"() {
16
16
  "use strict";
17
- MCP_VERSION = "1.4.1";
17
+ MCP_VERSION = "1.5.0";
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("No user ID available. Set SOCIALNEURON_USER_ID or authenticate via API key.");
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((expiresMs - Date.now()) / (1e3 * 60 * 60 * 24));
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("[MCP] Migrate to API key auth: npx @socialneuron/mcp-server setup");
471
- console.error("[MCP] Then remove SOCIALNEURON_SERVICE_KEY from your environment.");
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 { createServer } from "node:http";
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://app.socialneuron.com";
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(`${supabaseUrl}/functions/v1/mcp-auth?action=exchange-key`, {
3037
- method: "POST",
3038
- headers: { "Content-Type": "application/json" },
3039
- body: JSON.stringify({ code_verifier: codeVerifier, state })
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(` PKCE exchange error: ${err instanceof Error ? err.message : String(err)}`);
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(" - Tool invocations are logged for usage metering (no content stored)");
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(" - Data export/delete: https://app.socialneuron.com/settings");
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((resolve3) => {
3096
- const timeout = setTimeout(() => {
3097
- server2.close();
3098
- resolve3({ error: "Authorization timed out after 120 seconds." });
3099
- }, 12e4);
3100
- server2.on("request", async (req, res) => {
3101
- res.setHeader("Access-Control-Allow-Origin", "*");
3102
- res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
3103
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
3104
- if (req.method === "OPTIONS") {
3105
- res.writeHead(204);
3106
- res.end();
3107
- return;
3108
- }
3109
- if (req.method === "POST" && req.url === "/callback") {
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 (!data.api_key) {
3119
- res.writeHead(400, { "Content-Type": "application/json" });
3120
- res.end(JSON.stringify({ error: "Missing api_key" }));
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(200, { "Content-Type": "application/json" });
3124
- res.end(JSON.stringify({ success: true }));
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
- return;
3133
- }
3134
- res.writeHead(404);
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(" The key will still work if the server was in legacy mode.");
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(" No MCP client config found. Add this to your MCP config manually:");
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",');
@@ -3305,17 +3333,13 @@ async function runLoginDevice() {
3305
3333
  if (pollResponse.status === 200) {
3306
3334
  const pollData = await pollResponse.json();
3307
3335
  if (pollData.api_key) {
3308
- const validation = await validateApiKey(pollData.api_key);
3309
- if (validation.valid) {
3310
- await saveApiKey(pollData.api_key);
3311
- await saveSupabaseUrl(supabaseUrl);
3312
- console.error("");
3313
- console.error(" Authorized!");
3314
- console.error(` User: ${validation.email || "unknown"}`);
3315
- console.error(` Key prefix: ${pollData.api_key.substring(0, 12)}...`);
3316
- console.error("");
3317
- return;
3318
- }
3336
+ await saveApiKey(pollData.api_key);
3337
+ await saveSupabaseUrl(supabaseUrl);
3338
+ console.error("");
3339
+ console.error(" Authorized!");
3340
+ console.error(` Key prefix: ${pollData.api_key.substring(0, 12)}...`);
3341
+ console.error("");
3342
+ return;
3319
3343
  }
3320
3344
  }
3321
3345
  if (pollResponse.status === 410) {
@@ -3874,10 +3898,10 @@ init_supabase();
3874
3898
  function registerIdeationTools(server2) {
3875
3899
  server2.tool(
3876
3900
  "generate_content",
3877
- "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.",
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.",
3878
3902
  {
3879
3903
  prompt: z.string().max(1e4).describe(
3880
- "Detailed prompt describing the content to generate. Include context like topic, angle, audience, and any specific requirements."
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.'
3881
3905
  ),
3882
3906
  content_type: z.enum(["script", "caption", "blog", "hook"]).describe(
3883
3907
  'Type of content to generate. "script" for video scripts, "caption" for social media captions, "blog" for blog posts, "hook" for attention-grabbing hooks.'
@@ -3895,7 +3919,7 @@ function registerIdeationTools(server2) {
3895
3919
  "Target social media platform. Helps tailor tone, length, and format."
3896
3920
  ),
3897
3921
  brand_voice: z.string().max(500).optional().describe(
3898
- 'Brand voice guidelines to follow (e.g. "professional and empathetic", "playful and Gen-Z"). Leave blank to use a neutral tone.'
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.'
3899
3923
  ),
3900
3924
  model: z.enum(["gemini-2.0-flash", "gemini-2.5-flash", "gemini-2.5-pro"]).optional().describe(
3901
3925
  "AI model to use. Defaults to gemini-2.5-flash. Use gemini-2.5-pro for highest quality."
@@ -4051,7 +4075,7 @@ Content Type: ${content_type}`;
4051
4075
  );
4052
4076
  server2.tool(
4053
4077
  "fetch_trends",
4054
- "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.",
4078
+ '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.',
4055
4079
  {
4056
4080
  source: z.enum(["youtube", "google_trends", "rss", "url"]).describe(
4057
4081
  '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.'
@@ -4137,7 +4161,7 @@ Content Type: ${content_type}`;
4137
4161
  );
4138
4162
  server2.tool(
4139
4163
  "adapt_content",
4140
- "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.",
4164
+ "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.",
4141
4165
  {
4142
4166
  content: z.string().max(5e3).describe(
4143
4167
  "The content to adapt. Can be a caption, script, blog excerpt, or any text."
@@ -4435,10 +4459,10 @@ function checkAssetBudget() {
4435
4459
  function registerContentTools(server2) {
4436
4460
  server2.tool(
4437
4461
  "generate_video",
4438
- "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.",
4462
+ "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.",
4439
4463
  {
4440
4464
  prompt: z2.string().max(2500).describe(
4441
- "Text prompt describing the video to generate. Be specific about visual style, camera angles, movement, lighting, and mood."
4465
+ '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.'
4442
4466
  ),
4443
4467
  model: z2.enum([
4444
4468
  "veo3-fast",
@@ -4450,13 +4474,13 @@ function registerContentTools(server2) {
4450
4474
  "kling-3",
4451
4475
  "kling-3-pro"
4452
4476
  ]).describe(
4453
- "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)."
4477
+ "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)."
4454
4478
  ),
4455
4479
  duration: z2.number().min(3).max(30).optional().describe(
4456
4480
  "Video duration in seconds. kling: 5-30s, kling-3/kling-3-pro: 3-15s, sora2: 10-15s. Defaults to 5 seconds."
4457
4481
  ),
4458
4482
  aspect_ratio: z2.enum(["16:9", "9:16", "1:1"]).optional().describe(
4459
- "Aspect ratio. 16:9 for landscape/YouTube, 9:16 for vertical/Reels/TikTok, 1:1 for square. Defaults to 16:9."
4483
+ "Video aspect ratio. 16:9 for YouTube/landscape, 9:16 for TikTok/Reels/Shorts, 1:1 for Instagram feed/square. Defaults to 16:9."
4460
4484
  ),
4461
4485
  enable_audio: z2.boolean().optional().describe(
4462
4486
  "Enable native audio generation. Kling 2.6: doubles cost. Kling 3.0: 50% more (std 30/sec, pro 40/sec). 5+ languages."
@@ -4643,7 +4667,7 @@ function registerContentTools(server2) {
4643
4667
  );
4644
4668
  server2.tool(
4645
4669
  "generate_image",
4646
- "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.",
4670
+ "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).",
4647
4671
  {
4648
4672
  prompt: z2.string().max(2e3).describe(
4649
4673
  "Text prompt describing the image to generate. Be specific about style, composition, colors, lighting, and subject matter."
@@ -4823,7 +4847,7 @@ function registerContentTools(server2) {
4823
4847
  );
4824
4848
  server2.tool(
4825
4849
  "check_status",
4826
- "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.",
4850
+ '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.',
4827
4851
  {
4828
4852
  job_id: z2.string().describe(
4829
4853
  "The job ID returned by generate_video or generate_image. This is the asyncJobId or taskId value."
@@ -4999,7 +5023,7 @@ function registerContentTools(server2) {
4999
5023
  );
5000
5024
  server2.tool(
5001
5025
  "create_storyboard",
5002
- "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.",
5026
+ "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.",
5003
5027
  {
5004
5028
  concept: z2.string().max(2e3).describe(
5005
5029
  '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").'
@@ -5186,7 +5210,7 @@ Return ONLY valid JSON in this exact format:
5186
5210
  );
5187
5211
  server2.tool(
5188
5212
  "generate_voiceover",
5189
- "Generate a professional voiceover audio file using ElevenLabs TTS. Returns an audio URL stored in R2. Use this for narration in video production.",
5213
+ "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.",
5190
5214
  {
5191
5215
  text: z2.string().max(5e3).describe("The script/text to convert to speech."),
5192
5216
  voice: z2.enum([
@@ -5337,10 +5361,10 @@ Return ONLY valid JSON in this exact format:
5337
5361
  );
5338
5362
  server2.tool(
5339
5363
  "generate_carousel",
5340
- "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.",
5364
+ "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.",
5341
5365
  {
5342
5366
  topic: z2.string().max(200).describe(
5343
- 'The carousel topic/subject. Be specific about the angle or hook. Example: "5 reasons your startup will fail in 2026"'
5367
+ '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.'
5344
5368
  ),
5345
5369
  template_id: z2.enum([
5346
5370
  "educational-series",
@@ -5547,14 +5571,12 @@ function asEnvelope2(data) {
5547
5571
  function registerDistributionTools(server2) {
5548
5572
  server2.tool(
5549
5573
  "schedule_post",
5550
- "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.",
5574
+ '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.',
5551
5575
  {
5552
5576
  media_url: z3.string().optional().describe(
5553
5577
  "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."
5554
5578
  ),
5555
- media_urls: z3.array(z3.string()).optional().describe(
5556
- "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."
5557
- ),
5579
+ 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."),
5558
5580
  media_type: z3.enum(["IMAGE", "VIDEO", "CAROUSEL_ALBUM"]).optional().describe(
5559
5581
  "Media type. Set to CAROUSEL_ALBUM with media_urls for Instagram carousels. Default: auto-detected from media_url."
5560
5582
  ),
@@ -5570,16 +5592,10 @@ function registerDistributionTools(server2) {
5570
5592
  "threads",
5571
5593
  "bluesky"
5572
5594
  ])
5573
- ).min(1).describe(
5574
- "Target platforms to post to. Each must have an active OAuth connection."
5575
- ),
5595
+ ).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."),
5576
5596
  title: z3.string().optional().describe("Post title (used by YouTube and some other platforms)."),
5577
- hashtags: z3.array(z3.string()).optional().describe(
5578
- 'Hashtags to append to the caption. Include or omit the "#" prefix.'
5579
- ),
5580
- schedule_at: z3.string().optional().describe(
5581
- 'ISO 8601 datetime for scheduled posting (e.g. "2026-03-15T14:00:00Z"). Omit for immediate posting.'
5582
- ),
5597
+ 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"].'),
5598
+ 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.'),
5583
5599
  project_id: z3.string().optional().describe("Social Neuron project ID to associate this post with."),
5584
5600
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
5585
5601
  attribution: z3.boolean().optional().describe(
@@ -5733,7 +5749,7 @@ Created with Social Neuron`;
5733
5749
  );
5734
5750
  server2.tool(
5735
5751
  "list_connected_accounts",
5736
- "List all social media accounts connected to Social Neuron via OAuth. Shows which platforms are available for posting.",
5752
+ "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.",
5737
5753
  {
5738
5754
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
5739
5755
  },
@@ -5798,7 +5814,7 @@ Created with Social Neuron`;
5798
5814
  );
5799
5815
  server2.tool(
5800
5816
  "list_recent_posts",
5801
- "List recent posts from Social Neuron. Shows status, platform, title, and timestamps. Useful for checking what has been published or scheduled recently.",
5817
+ "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.",
5802
5818
  {
5803
5819
  platform: z3.enum([
5804
5820
  "youtube",
@@ -6639,7 +6655,7 @@ function registerAnalyticsTools(server2) {
6639
6655
  "threads",
6640
6656
  "bluesky"
6641
6657
  ]).optional().describe("Filter analytics to a specific platform."),
6642
- days: z4.number().min(1).max(365).optional().describe("Number of days to look back. Defaults to 30. Max 365."),
6658
+ 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."),
6643
6659
  content_id: z4.string().uuid().optional().describe(
6644
6660
  "Filter to a specific content_history ID to see performance of one piece of content."
6645
6661
  ),
@@ -6819,7 +6835,7 @@ function registerAnalyticsTools(server2) {
6819
6835
  );
6820
6836
  server2.tool(
6821
6837
  "refresh_platform_analytics",
6822
- "Trigger an analytics refresh for all recently posted content across all connected platforms. Queues analytics fetch jobs for posts from the last 7 days.",
6838
+ "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.",
6823
6839
  {
6824
6840
  response_format: z4.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6825
6841
  },
@@ -8755,13 +8771,11 @@ function asEnvelope6(data) {
8755
8771
  function registerCommentsTools(server2) {
8756
8772
  server2.tool(
8757
8773
  "list_comments",
8758
- "List YouTube comments. Without a video_id, returns recent comments across all channel videos. With a video_id, returns comments for that specific video.",
8774
+ '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.',
8759
8775
  {
8760
- video_id: z10.string().optional().describe(
8761
- "YouTube video ID. If omitted, returns comments across all channel videos."
8762
- ),
8776
+ 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.'),
8763
8777
  max_results: z10.number().min(1).max(100).optional().describe("Maximum number of comments to return. Defaults to 50."),
8764
- page_token: z10.string().optional().describe("Pagination token from a previous list_comments call."),
8778
+ page_token: z10.string().optional().describe("Pagination cursor from previous list_comments response nextPageToken field. Omit for first page of results."),
8765
8779
  response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8766
8780
  },
8767
8781
  async ({ video_id, max_results, page_token, response_format }) => {
@@ -8826,7 +8840,7 @@ function registerCommentsTools(server2) {
8826
8840
  );
8827
8841
  server2.tool(
8828
8842
  "reply_to_comment",
8829
- "Reply to a YouTube comment. Requires the parent comment ID and reply text.",
8843
+ "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.",
8830
8844
  {
8831
8845
  parent_id: z10.string().describe(
8832
8846
  "The ID of the parent comment to reply to (from list_comments)."
@@ -9359,7 +9373,7 @@ function asEnvelope8(data) {
9359
9373
  function registerCreditsTools(server2) {
9360
9374
  server2.tool(
9361
9375
  "get_credit_balance",
9362
- "Get current subscription credit balance and plan.",
9376
+ "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).",
9363
9377
  {
9364
9378
  response_format: z12.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9365
9379
  },
@@ -9412,7 +9426,7 @@ Monthly used: ${payload.monthlyUsed}` + (payload.monthlyLimit ? ` / ${payload.mo
9412
9426
  );
9413
9427
  server2.tool(
9414
9428
  "get_budget_status",
9415
- "Get current MCP run budget consumption for credits/assets.",
9429
+ "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.",
9416
9430
  {
9417
9431
  response_format: z12.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9418
9432
  },
@@ -9658,7 +9672,7 @@ function asEnvelope11(data) {
9658
9672
  function registerAutopilotTools(server2) {
9659
9673
  server2.tool(
9660
9674
  "list_autopilot_configs",
9661
- "List all autopilot configurations for your account. Shows active schedules, associated recipes, credit budgets, and last run times.",
9675
+ "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.",
9662
9676
  {
9663
9677
  active_only: z15.boolean().optional().describe("If true, only return active configs. Defaults to false (show all)."),
9664
9678
  response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
@@ -9804,7 +9818,7 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
9804
9818
  );
9805
9819
  server2.tool(
9806
9820
  "get_autopilot_status",
9807
- "Get the current status of your autopilot system, including active configs, recent runs, and next scheduled execution.",
9821
+ "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.",
9808
9822
  {
9809
9823
  response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9810
9824
  },
@@ -9923,7 +9937,7 @@ ${content.suggested_hooks.map((h) => ` - ${h}`).join("\n")}`
9923
9937
  function registerExtractionTools(server2) {
9924
9938
  server2.tool(
9925
9939
  "extract_url_content",
9926
- "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.",
9940
+ "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.",
9927
9941
  {
9928
9942
  url: z16.string().url().describe("URL to extract content from"),
9929
9943
  extract_type: z16.enum(["auto", "transcript", "article", "product"]).default("auto").describe("Type of extraction"),
@@ -10123,9 +10137,9 @@ function asEnvelope13(data) {
10123
10137
  function registerQualityTools(server2) {
10124
10138
  server2.tool(
10125
10139
  "quality_check",
10126
- "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.",
10140
+ "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.",
10127
10141
  {
10128
- caption: z17.string().describe("Post caption/body text"),
10142
+ 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."),
10129
10143
  title: z17.string().optional().describe("Post title (important for YouTube)"),
10130
10144
  platforms: z17.array(
10131
10145
  z17.enum([
@@ -10139,7 +10153,7 @@ function registerQualityTools(server2) {
10139
10153
  "bluesky"
10140
10154
  ])
10141
10155
  ).min(1).describe("Target platforms"),
10142
- threshold: z17.number().min(0).max(35).default(26).describe("Minimum total score to pass"),
10156
+ 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."),
10143
10157
  brand_keyword: z17.string().optional().describe("Brand keyword for alignment check"),
10144
10158
  brand_avoid_patterns: z17.array(z17.string()).optional(),
10145
10159
  custom_banned_terms: z17.array(z17.string()).optional(),
@@ -10210,7 +10224,7 @@ function registerQualityTools(server2) {
10210
10224
  );
10211
10225
  server2.tool(
10212
10226
  "quality_check_plan",
10213
- "Run quality checks on all posts in a content plan. Returns per-post scores and aggregate summary.",
10227
+ "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.",
10214
10228
  {
10215
10229
  plan: z17.object({
10216
10230
  posts: z17.array(
@@ -10222,7 +10236,7 @@ function registerQualityTools(server2) {
10222
10236
  })
10223
10237
  )
10224
10238
  }).passthrough().describe("Content plan with posts array"),
10225
- threshold: z17.number().min(0).max(35).default(26).describe("Minimum total score to pass"),
10239
+ 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."),
10226
10240
  response_format: z17.enum(["text", "json"]).default("text")
10227
10241
  },
10228
10242
  async ({ plan, threshold, response_format }) => {
@@ -10405,7 +10419,7 @@ function formatPlanAsText(plan) {
10405
10419
  function registerPlanningTools(server2) {
10406
10420
  server2.tool(
10407
10421
  "plan_content_week",
10408
- "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.",
10422
+ "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.",
10409
10423
  {
10410
10424
  topic: z18.string().describe("Main topic or content theme"),
10411
10425
  source_url: z18.string().optional().describe("URL to extract content from (YouTube, article)"),
@@ -10741,7 +10755,7 @@ ${rawText.slice(0, 1e3)}`
10741
10755
  );
10742
10756
  server2.tool(
10743
10757
  "save_content_plan",
10744
- "Persist a content plan payload for later review, approvals, and scheduling.",
10758
+ "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.",
10745
10759
  {
10746
10760
  plan: z18.object({
10747
10761
  topic: z18.string(),
@@ -11406,7 +11420,7 @@ import { z as z20 } from "zod";
11406
11420
  function registerDiscoveryTools(server2) {
11407
11421
  server2.tool(
11408
11422
  "search_tools",
11409
- 'Search and discover available MCP tools. Use detail level to control token usage: "name" (~50 tokens), "summary" (~500 tokens), "full" (complete schemas).',
11423
+ '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.',
11410
11424
  {
11411
11425
  query: z20.string().optional().describe("Search query to filter tools by name or description"),
11412
11426
  module: z20.string().optional().describe('Filter by module name (e.g. "planning", "content", "analytics")'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@socialneuron/mcp-server",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "MCP server for Social Neuron - AI content creation platform",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -61,10 +61,10 @@
61
61
  },
62
62
  "dependencies": {
63
63
  "@modelcontextprotocol/sdk": "^1.27.1",
64
- "@supabase/supabase-js": "2.98.0",
64
+ "@supabase/supabase-js": "2.99.2",
65
65
  "express": "^5.1.0",
66
66
  "jose": "^6.2.1",
67
- "open": "10.0.0",
67
+ "open": "11.0.0",
68
68
  "zod": "^4.0.0"
69
69
  },
70
70
  "optionalDependencies": {
@@ -75,7 +75,7 @@
75
75
  "@types/node": "^25.3.5",
76
76
  "esbuild": "^0.27.3",
77
77
  "typescript": "^5.9.3",
78
- "vitest": "^3.0.0"
78
+ "vitest": "^4.1.0"
79
79
  },
80
80
  "engines": {
81
81
  "node": ">=20.0.0 <21.0.0 || >=22.0.0"