@socialneuron/mcp-server 1.9.0 → 1.9.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/sn.js CHANGED
@@ -19,7 +19,7 @@ var MCP_VERSION;
19
19
  var init_version = __esm({
20
20
  "src/lib/version.ts"() {
21
21
  "use strict";
22
- MCP_VERSION = "1.9.0";
22
+ MCP_VERSION = "1.9.1";
23
23
  }
24
24
  });
25
25
 
@@ -4347,6 +4347,92 @@ var init_ideation = __esm({
4347
4347
  }
4348
4348
  });
4349
4349
 
4350
+ // src/lib/sanitize-error.ts
4351
+ function redactSensitiveIdentifiers(message) {
4352
+ return message.replace(EMAIL_PATTERN, "[redacted-email]").replace(UUID_PATTERN, "[redacted-id]");
4353
+ }
4354
+ function sanitizeError(error) {
4355
+ const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
4356
+ for (const [pattern, userMessage] of ERROR_PATTERNS) {
4357
+ if (pattern.test(msg)) {
4358
+ return userMessage;
4359
+ }
4360
+ }
4361
+ return "An unexpected error occurred. Please try again.";
4362
+ }
4363
+ var ERROR_PATTERNS, UUID_PATTERN, EMAIL_PATTERN;
4364
+ var init_sanitize_error = __esm({
4365
+ "src/lib/sanitize-error.ts"() {
4366
+ "use strict";
4367
+ ERROR_PATTERNS = [
4368
+ // Postgres / PostgREST
4369
+ [
4370
+ /PGRST301|permission denied/i,
4371
+ "Access denied. Check your account permissions."
4372
+ ],
4373
+ [
4374
+ /42P01|does not exist/i,
4375
+ "Service temporarily unavailable. Please try again."
4376
+ ],
4377
+ [
4378
+ /23505|unique.*constraint|duplicate key/i,
4379
+ "A duplicate record already exists."
4380
+ ],
4381
+ [/23503|foreign key/i, "Referenced record not found."],
4382
+ // Gemini / Google AI
4383
+ [
4384
+ /google.*api.*key|googleapis\.com.*40[13]/i,
4385
+ "Content generation failed. Please try again."
4386
+ ],
4387
+ [
4388
+ /RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
4389
+ "AI service rate limit reached. Please wait and retry."
4390
+ ],
4391
+ [
4392
+ /SAFETY|prompt.*blocked|content.*filter/i,
4393
+ "Content was blocked by the AI safety filter. Try rephrasing."
4394
+ ],
4395
+ [
4396
+ /gemini.*error|generativelanguage/i,
4397
+ "Content generation failed. Please try again."
4398
+ ],
4399
+ // Kie.ai
4400
+ [/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
4401
+ // Stripe
4402
+ [
4403
+ /stripe.*api|sk_live_|sk_test_/i,
4404
+ "Payment processing error. Please try again."
4405
+ ],
4406
+ // Network / fetch
4407
+ [
4408
+ /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
4409
+ "External service unavailable. Please try again."
4410
+ ],
4411
+ [
4412
+ /fetch failed|network error|abort.*timeout/i,
4413
+ "Network request failed. Please try again."
4414
+ ],
4415
+ [/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
4416
+ // Supabase Edge Function internals
4417
+ [
4418
+ /FunctionsHttpError|non-2xx status/i,
4419
+ "Backend service error. Please try again."
4420
+ ],
4421
+ [
4422
+ /JWT|token.*expired|token.*invalid/i,
4423
+ "Authentication expired. Please re-authenticate."
4424
+ ],
4425
+ // Generic sensitive patterns (API keys, URLs with secrets)
4426
+ [
4427
+ /[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i,
4428
+ "An internal error occurred. Please try again."
4429
+ ]
4430
+ ];
4431
+ UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
4432
+ EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
4433
+ }
4434
+ });
4435
+
4350
4436
  // src/lib/checkStatusShape.ts
4351
4437
  function buildCheckStatusPayload(job, liveStatus) {
4352
4438
  const status = liveStatus?.status ?? job.status;
@@ -4442,9 +4528,22 @@ function checkCreditBudget(estimatedCost) {
4442
4528
  }
4443
4529
  const used = getCreditsUsed();
4444
4530
  if (used + estimatedCost > MAX_CREDITS_PER_RUN) {
4531
+ const message = `Per-run credit cap reached \u2014 this is a local spend guard, not a server rate limit, so retrying will not help. The SOCIALNEURON_MAX_CREDITS_PER_RUN environment variable is set to ${MAX_CREDITS_PER_RUN} credits. This run has already spent ${used} credits and this call is estimated at ~${estimatedCost} more (${used} + ${estimatedCost} = ${used + estimatedCost} > ${MAX_CREDITS_PER_RUN}). To proceed, raise or unset SOCIALNEURON_MAX_CREDITS_PER_RUN.`;
4445
4532
  return {
4446
4533
  ok: false,
4447
- message: `Credit budget exceeded for this MCP run. Used=${used}, next~=${estimatedCost}, limit=${MAX_CREDITS_PER_RUN}.`
4534
+ message,
4535
+ error: toolError("billing_error", message, {
4536
+ recover_with: [
4537
+ "Raise SOCIALNEURON_MAX_CREDITS_PER_RUN to a higher credit ceiling.",
4538
+ "Unset SOCIALNEURON_MAX_CREDITS_PER_RUN to remove the per-run cap."
4539
+ ],
4540
+ details: {
4541
+ env_var: "SOCIALNEURON_MAX_CREDITS_PER_RUN",
4542
+ credits_used_this_run: used,
4543
+ estimated_call_cost: estimatedCost,
4544
+ max_credits_per_run: MAX_CREDITS_PER_RUN
4545
+ }
4546
+ })
4448
4547
  };
4449
4548
  }
4450
4549
  return { ok: true };
@@ -4467,8 +4566,15 @@ var init_budget = __esm({
4467
4566
  "src/lib/budget.ts"() {
4468
4567
  "use strict";
4469
4568
  init_request_context();
4470
- MAX_CREDITS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
4471
- MAX_ASSETS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0));
4569
+ init_tool_error();
4570
+ MAX_CREDITS_PER_RUN = Math.max(
4571
+ 0,
4572
+ Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0)
4573
+ );
4574
+ MAX_ASSETS_PER_RUN = Math.max(
4575
+ 0,
4576
+ Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0)
4577
+ );
4472
4578
  _globalCreditsUsed = 0;
4473
4579
  _globalAssetsGenerated = 0;
4474
4580
  }
@@ -4495,7 +4601,7 @@ function asEnvelope2(data) {
4495
4601
  function registerContentTools(server) {
4496
4602
  server.tool(
4497
4603
  "generate_video",
4498
- "Start an async AI video generation job \u2014 returns a job_id immediately. Poll with check_status every 10-30s until complete. Base credit costs (reference config; dynamic models scale with duration/audio/resolution): seedance-2-fast 264 (8s, best quality/credit) \xB7 kling-3 100 (5s no-audio) \xB7 grok-imagine 30 (6s, cheapest) \xB7 veo3-fast 65 (8s) \xB7 kling-3-pro 135 (5s no-audio) \xB7 seedance-2 328 (8s 720p) \xB7 veo3-quality 1000 (8s) \xB7 wan-2.6 105 (5s) \xB7 gemini-omni-video 126 (multi-reference composites) \xB7 hailuo-02-standard 180 / seedance-1.5-pro 150 / kling 170 (legacy/budget fallbacks). Costs are estimated pre-check and reconciled post-response from the actual charge. audio adds ~1.5x on kling-3/kling-3-pro and ~2.65x on kling (enable_audio defaults to FALSE). Check get_credit_balance first for expensive generations.",
4604
+ "Start an async AI video generation job \u2014 returns a job_id immediately. Poll with check_status every 10-30s until complete. Base credit costs (reference config; dynamic models scale with duration/audio/resolution): seedance-2-fast 264 (8s, best quality/credit) \xB7 kling-3 100 (5s no-audio) \xB7 grok-imagine 30 (6s, cheapest) \xB7 veo3-fast 65 (8s) \xB7 kling-3-pro 135 (5s no-audio) \xB7 seedance-2 328 (8s 720p) \xB7 veo3-quality 1000 (8s) \xB7 wan-2.6 105 (5s) \xB7 gemini-omni-video 126 (multi-reference composites) \xB7 hailuo-02-standard 180 / seedance-1.5-pro 150 / kling 170 (legacy/budget fallbacks). Costs are estimated pre-check and reconciled post-response from the actual charge. audio adds ~1.5x on kling-3/kling-3-pro and ~2.65x on kling; enable_audio defaults to FALSE EXCEPT the seedance-2 family (seedance-2-fast, seedance-2) where audio is ON by default and its cost is already included. Check get_credit_balance first for expensive generations.",
4499
4605
  {
4500
4606
  prompt: z2.string().max(2500).describe(
4501
4607
  '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.'
@@ -4510,7 +4616,7 @@ function registerContentTools(server) {
4510
4616
  "Video aspect ratio. 16:9 for YouTube/landscape, 9:16 for TikTok/Reels/Shorts, 1:1 for Instagram feed/square. Defaults to 16:9."
4511
4617
  ),
4512
4618
  enable_audio: z2.boolean().optional().describe(
4513
- "Enable native audio generation. DEFAULTS TO FALSE (cost control). Cost multiplier when true: kling 2.6 ~2.65x (17->45 cr/sec), kling-3 1.5x (20->30 cr/sec), kling-3-pro ~1.5x (27->40 cr/sec). seedance-2/-fast generate audio natively regardless. 5+ languages."
4619
+ "Enable native audio generation. For most models this DEFAULTS TO FALSE (cost control). EXCEPTION: the seedance-2 family (seedance-2-fast, seedance-2) DEFAULTS TO TRUE \u2014 these generate audio natively and their credit cost already includes it, so audio is on unless you explicitly pass false. Cost multiplier when true on other models: kling 2.6 ~2.65x (17->45 cr/sec), kling-3 1.5x (20->30 cr/sec), kling-3-pro ~1.5x (27->40 cr/sec). 5+ languages."
4514
4620
  ),
4515
4621
  image_url: z2.string().optional().describe(
4516
4622
  "Start frame image URL for image-to-video (Kling 3.0 frame control)."
@@ -4546,10 +4652,7 @@ function registerContentTools(server) {
4546
4652
  const estimatedCost = VIDEO_CREDIT_ESTIMATES[model] ?? 120;
4547
4653
  const budgetCheck = checkCreditBudget(estimatedCost);
4548
4654
  if (!budgetCheck.ok) {
4549
- return {
4550
- content: [{ type: "text", text: budgetCheck.message }],
4551
- isError: true
4552
- };
4655
+ return budgetCheck.error;
4553
4656
  }
4554
4657
  const rateLimit = checkRateLimit(
4555
4658
  "generation",
@@ -4573,9 +4676,12 @@ function registerContentTools(server) {
4573
4676
  model,
4574
4677
  duration: duration ?? 5,
4575
4678
  aspectRatio: aspect_ratio ?? "16:9",
4576
- // Default FALSE (2026-07-13): the old `?? true` default silently multiplied
4577
- // kling-family costs (2.65x on kling 2.6) for callers that never asked for audio.
4578
- enableAudio: enable_audio ?? false,
4679
+ // Default FALSE for most models (2026-07-13): the old `?? true` default silently
4680
+ // multiplied kling-family costs (2.65x on kling 2.6) for callers that never asked
4681
+ // for audio. Exception (2026-07-17): the seedance-2 family bills audio into its base
4682
+ // cost and generates it natively, so a false default there shipped silent no-audio
4683
+ // mp4s — those models default TRUE when the caller omits enable_audio.
4684
+ enableAudio: enable_audio ?? AUDIO_NATIVE_DEFAULT_MODELS.has(model),
4579
4685
  ...image_url && { imageUrl: image_url },
4580
4686
  ...end_frame_url && { endFrameUrl: end_frame_url },
4581
4687
  // The server reads projectId and enforces
@@ -4697,10 +4803,7 @@ function registerContentTools(server) {
4697
4803
  const estimatedCost = IMAGE_CREDIT_ESTIMATES[model] ?? 30;
4698
4804
  const budgetCheck = checkCreditBudget(estimatedCost);
4699
4805
  if (!budgetCheck.ok) {
4700
- return {
4701
- content: [{ type: "text", text: budgetCheck.message }],
4702
- isError: true
4703
- };
4806
+ return budgetCheck.error;
4704
4807
  }
4705
4808
  const rateLimit = checkRateLimit(
4706
4809
  "generation",
@@ -4945,7 +5048,7 @@ function registerContentTools(server) {
4945
5048
  );
4946
5049
  }
4947
5050
  if (job.error_message) {
4948
- lines.push(`Error: ${job.error_message}`);
5051
+ lines.push(`Error: ${redactSensitiveIdentifiers(job.error_message)}`);
4949
5052
  }
4950
5053
  const fallbackDisclosure = buildFallbackDisclosureLine(
4951
5054
  job.result_metadata
@@ -5099,10 +5202,7 @@ Return ONLY valid JSON in this exact format:
5099
5202
  const estimatedCost = 10;
5100
5203
  const budgetCheck = checkCreditBudget(estimatedCost);
5101
5204
  if (!budgetCheck.ok) {
5102
- return {
5103
- content: [{ type: "text", text: budgetCheck.message }],
5104
- isError: true
5105
- };
5205
+ return budgetCheck.error;
5106
5206
  }
5107
5207
  const { data, error } = await callEdgeFunction(
5108
5208
  "social-neuron-ai",
@@ -5189,10 +5289,7 @@ Return ONLY valid JSON in this exact format:
5189
5289
  const estimatedCost = 15;
5190
5290
  const budgetCheck = checkCreditBudget(estimatedCost);
5191
5291
  if (!budgetCheck.ok) {
5192
- return {
5193
- content: [{ type: "text", text: budgetCheck.message }],
5194
- isError: true
5195
- };
5292
+ return budgetCheck.error;
5196
5293
  }
5197
5294
  const rateLimit = checkRateLimit(
5198
5295
  "generation",
@@ -5355,10 +5452,7 @@ Return ONLY valid JSON in this exact format:
5355
5452
  const estimatedCost = 10 + slideCount * 2;
5356
5453
  const budgetCheck = checkCreditBudget(estimatedCost);
5357
5454
  if (!budgetCheck.ok) {
5358
- return {
5359
- content: [{ type: "text", text: budgetCheck.message }],
5360
- isError: true
5361
- };
5455
+ return budgetCheck.error;
5362
5456
  }
5363
5457
  const userId = await getDefaultUserId();
5364
5458
  const rateLimit = checkRateLimit(
@@ -5461,13 +5555,14 @@ Return ONLY valid JSON in this exact format:
5461
5555
  }
5462
5556
  );
5463
5557
  }
5464
- var VIDEO_CREDIT_ESTIMATES, VIDEO_MODEL_ENUM, IMAGE_CREDIT_ESTIMATES;
5558
+ var VIDEO_CREDIT_ESTIMATES, AUDIO_NATIVE_DEFAULT_MODELS, VIDEO_MODEL_ENUM, IMAGE_CREDIT_ESTIMATES;
5465
5559
  var init_content2 = __esm({
5466
5560
  "src/tools/content.ts"() {
5467
5561
  "use strict";
5468
5562
  init_edge_function();
5469
5563
  init_rate_limit();
5470
5564
  init_supabase();
5565
+ init_sanitize_error();
5471
5566
  init_version();
5472
5567
  init_checkStatusShape();
5473
5568
  init_budget();
@@ -5485,6 +5580,10 @@ var init_content2 = __esm({
5485
5580
  "seedance-1.5-pro": 150,
5486
5581
  kling: 170
5487
5582
  };
5583
+ AUDIO_NATIVE_DEFAULT_MODELS = /* @__PURE__ */ new Set([
5584
+ "seedance-2-fast",
5585
+ "seedance-2"
5586
+ ]);
5488
5587
  VIDEO_MODEL_ENUM = [
5489
5588
  "seedance-2-fast",
5490
5589
  "kling-3",
@@ -5513,57 +5612,6 @@ var init_content2 = __esm({
5513
5612
  }
5514
5613
  });
5515
5614
 
5516
- // src/lib/sanitize-error.ts
5517
- function sanitizeError(error) {
5518
- const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
5519
- for (const [pattern, userMessage] of ERROR_PATTERNS) {
5520
- if (pattern.test(msg)) {
5521
- return userMessage;
5522
- }
5523
- }
5524
- return "An unexpected error occurred. Please try again.";
5525
- }
5526
- var ERROR_PATTERNS;
5527
- var init_sanitize_error = __esm({
5528
- "src/lib/sanitize-error.ts"() {
5529
- "use strict";
5530
- ERROR_PATTERNS = [
5531
- // Postgres / PostgREST
5532
- [/PGRST301|permission denied/i, "Access denied. Check your account permissions."],
5533
- [/42P01|does not exist/i, "Service temporarily unavailable. Please try again."],
5534
- [/23505|unique.*constraint|duplicate key/i, "A duplicate record already exists."],
5535
- [/23503|foreign key/i, "Referenced record not found."],
5536
- // Gemini / Google AI
5537
- [/google.*api.*key|googleapis\.com.*40[13]/i, "Content generation failed. Please try again."],
5538
- [
5539
- /RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
5540
- "AI service rate limit reached. Please wait and retry."
5541
- ],
5542
- [
5543
- /SAFETY|prompt.*blocked|content.*filter/i,
5544
- "Content was blocked by the AI safety filter. Try rephrasing."
5545
- ],
5546
- [/gemini.*error|generativelanguage/i, "Content generation failed. Please try again."],
5547
- // Kie.ai
5548
- [/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
5549
- // Stripe
5550
- [/stripe.*api|sk_live_|sk_test_/i, "Payment processing error. Please try again."],
5551
- // Network / fetch
5552
- [
5553
- /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
5554
- "External service unavailable. Please try again."
5555
- ],
5556
- [/fetch failed|network error|abort.*timeout/i, "Network request failed. Please try again."],
5557
- [/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
5558
- // Supabase Edge Function internals
5559
- [/FunctionsHttpError|non-2xx status/i, "Backend service error. Please try again."],
5560
- [/JWT|token.*expired|token.*invalid/i, "Authentication expired. Please re-authenticate."],
5561
- // Generic sensitive patterns (API keys, URLs with secrets)
5562
- [/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i, "An internal error occurred. Please try again."]
5563
- ];
5564
- }
5565
- });
5566
-
5567
5615
  // src/lib/ssrf.ts
5568
5616
  import { promises as dnsPromises } from "node:dns";
5569
5617
  function isBlockedIP(ip) {
@@ -13397,6 +13445,14 @@ var init_plan_approvals = __esm({
13397
13445
 
13398
13446
  // src/tools/discovery.ts
13399
13447
  import { z as z23 } from "zod";
13448
+ function isHostedTransport() {
13449
+ return process.env.MCP_TRANSPORT === "http";
13450
+ }
13451
+ function isPubliclyDiscoverable(tool) {
13452
+ if (tool.internal || tool.hiddenFromPublicCount) return false;
13453
+ if (tool.localOnly && isHostedTransport()) return false;
13454
+ return true;
13455
+ }
13400
13456
  function toolKnowledgeDocument(tool) {
13401
13457
  const lines = [
13402
13458
  `Tool: ${tool.name}`,
@@ -13407,7 +13463,8 @@ function toolKnowledgeDocument(tool) {
13407
13463
  if (tool.task_intent) lines.push(`Task intent: ${tool.task_intent}`);
13408
13464
  if (tool.use_when) lines.push(`Use when: ${tool.use_when}`);
13409
13465
  if (tool.avoid_when) lines.push(`Avoid when: ${tool.avoid_when}`);
13410
- if (tool.next_tools?.length) lines.push(`Common next tools: ${tool.next_tools.join(", ")}`);
13466
+ if (tool.next_tools?.length)
13467
+ lines.push(`Common next tools: ${tool.next_tools.join(", ")}`);
13411
13468
  return {
13412
13469
  id: `tool:${tool.name}`,
13413
13470
  title: `MCP tool: ${tool.name}`,
@@ -13424,9 +13481,7 @@ function toolKnowledgeDocument(tool) {
13424
13481
  function getKnowledgeDocuments() {
13425
13482
  return [
13426
13483
  ...STATIC_KNOWLEDGE_DOCUMENTS,
13427
- ...TOOL_CATALOG.filter((t) => !t.internal && !t.hiddenFromPublicCount).map(
13428
- toolKnowledgeDocument
13429
- )
13484
+ ...TOOL_CATALOG.filter(isPubliclyDiscoverable).map(toolKnowledgeDocument)
13430
13485
  ];
13431
13486
  }
13432
13487
  function tokenize(input) {
@@ -13480,7 +13535,9 @@ function registerDiscoveryTools(server) {
13480
13535
  };
13481
13536
  return {
13482
13537
  structuredContent,
13483
- content: [{ type: "text", text: JSON.stringify(structuredContent) }]
13538
+ content: [
13539
+ { type: "text", text: JSON.stringify(structuredContent) }
13540
+ ]
13484
13541
  };
13485
13542
  }
13486
13543
  );
@@ -13501,10 +13558,14 @@ function registerDiscoveryTools(server) {
13501
13558
  }
13502
13559
  },
13503
13560
  async ({ id }) => {
13504
- const doc = getKnowledgeDocuments().find((candidate) => candidate.id === id);
13561
+ const doc = getKnowledgeDocuments().find(
13562
+ (candidate) => candidate.id === id
13563
+ );
13505
13564
  if (!doc) {
13506
13565
  return {
13507
- content: [{ type: "text", text: `Document not found: ${id}` }],
13566
+ content: [
13567
+ { type: "text", text: `Document not found: ${id}` }
13568
+ ],
13508
13569
  isError: true
13509
13570
  };
13510
13571
  }
@@ -13517,7 +13578,9 @@ function registerDiscoveryTools(server) {
13517
13578
  };
13518
13579
  return {
13519
13580
  structuredContent,
13520
- content: [{ type: "text", text: JSON.stringify(structuredContent) }]
13581
+ content: [
13582
+ { type: "text", text: JSON.stringify(structuredContent) }
13583
+ ]
13521
13584
  };
13522
13585
  }
13523
13586
  );
@@ -13526,7 +13589,9 @@ function registerDiscoveryTools(server) {
13526
13589
  '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.',
13527
13590
  {
13528
13591
  query: z23.string().optional().describe("Search query to filter tools by name or description"),
13529
- module: z23.string().optional().describe('Filter by module name (e.g. "planning", "content", "analytics")'),
13592
+ module: z23.string().optional().describe(
13593
+ 'Filter by module name (e.g. "planning", "content", "analytics")'
13594
+ ),
13530
13595
  scope: z23.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
13531
13596
  detail: z23.enum(["name", "summary", "full"]).default("summary").describe(
13532
13597
  'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
@@ -13543,14 +13608,18 @@ function registerDiscoveryTools(server) {
13543
13608
  if (query) {
13544
13609
  results = searchTools(query);
13545
13610
  }
13546
- results = results.filter((t) => !t.internal && !t.hiddenFromPublicCount);
13611
+ results = results.filter(isPubliclyDiscoverable);
13547
13612
  if (module) {
13548
13613
  const moduleTools = getToolsByModule(module);
13549
- results = results.filter((t) => moduleTools.some((mt) => mt.name === t.name));
13614
+ results = results.filter(
13615
+ (t) => moduleTools.some((mt) => mt.name === t.name)
13616
+ );
13550
13617
  }
13551
13618
  if (scope) {
13552
13619
  const scopeTools = getToolsByScope(scope);
13553
- results = results.filter((t) => scopeTools.some((st) => st.name === t.name));
13620
+ results = results.filter(
13621
+ (t) => scopeTools.some((st) => st.name === t.name)
13622
+ );
13554
13623
  }
13555
13624
  if (available_only) {
13556
13625
  results = results.filter(isAvailable);
@@ -13588,7 +13657,12 @@ function registerDiscoveryTools(server) {
13588
13657
  text: JSON.stringify(
13589
13658
  {
13590
13659
  toolCount: results.length,
13591
- ...hasKnownScopes ? { scopes: { available: currentScopes, unavailable_matches: unavailableCount } } : {},
13660
+ ...hasKnownScopes ? {
13661
+ scopes: {
13662
+ available: currentScopes,
13663
+ unavailable_matches: unavailableCount
13664
+ }
13665
+ } : {},
13592
13666
  tools: output
13593
13667
  },
13594
13668
  null,
@@ -16179,25 +16253,39 @@ function registerCarouselTools(server) {
16179
16253
  ]).optional().describe("Carousel template. Default: hormozi-authority."),
16180
16254
  slide_count: z28.number().min(3).max(10).optional().describe("Number of slides (3-10). Default: 7."),
16181
16255
  aspect_ratio: z28.enum(["1:1", "4:5", "9:16"]).optional().describe("Aspect ratio for both carousel and images. Default: 1:1."),
16182
- style: z28.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe("Visual style. Default: hormozi for hormozi-authority template."),
16256
+ style: z28.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
16257
+ "Visual style. Default: hormozi for hormozi-authority template."
16258
+ ),
16183
16259
  image_style_suffix: z28.string().max(500).optional().describe(
16184
16260
  'Style suffix appended to every image prompt for visual consistency across slides. Example: "dark moody lighting, cinematic, 35mm film grain".'
16185
16261
  ),
16186
16262
  hook: z28.string().max(300).optional().describe(
16187
16263
  "Explicit hook/opener for slide 1. Overrides any hook derived from topic. Keep under 15 words for strongest scroll-stop."
16188
16264
  ),
16189
- hook_family: z28.enum(["curiosity", "authority", "pain_point", "contrarian", "data_driven"]).optional().describe(
16265
+ hook_family: z28.enum([
16266
+ "curiosity",
16267
+ "authority",
16268
+ "pain_point",
16269
+ "contrarian",
16270
+ "data_driven"
16271
+ ]).optional().describe(
16190
16272
  "Hook family tag. Recorded on the carousel so downstream analytics can attribute engagement to hook pattern."
16191
16273
  ),
16192
- cta_text: z28.string().max(200).optional().describe("Explicit CTA copy for the final slide. If omitted, derived from topic."),
16193
- cta_url: z28.string().url().optional().describe("URL promoted on the CTA slide. Defaults to the project landing page."),
16274
+ cta_text: z28.string().max(200).optional().describe(
16275
+ "Explicit CTA copy for the final slide. If omitted, derived from topic."
16276
+ ),
16277
+ cta_url: z28.string().url().optional().describe(
16278
+ "URL promoted on the CTA slide. Defaults to the project landing page."
16279
+ ),
16194
16280
  tone: z28.string().max(200).optional().describe(
16195
16281
  'Voice/tone override. Composes with the brand profile voice when present. Example: "educational, confident, not arrogant".'
16196
16282
  ),
16197
16283
  constraints: z28.string().max(500).optional().describe(
16198
16284
  'Content constraints applied at generation time. Example: "No fabricated statistics. Sentence case only. No ALL CAPS."'
16199
16285
  ),
16200
- platform: z28.enum(["linkedin", "instagram", "tiktok", "x"]).optional().describe("Target platform. Affects tone conventions and slide-count guardrails."),
16286
+ platform: z28.enum(["linkedin", "instagram", "tiktok", "x"]).optional().describe(
16287
+ "Target platform. Affects tone conventions and slide-count guardrails."
16288
+ ),
16201
16289
  brand_id: z28.string().optional().describe(
16202
16290
  "Brand/project ID to pull visual context from (colors, logo, mood). Falls back to project_id, then default project."
16203
16291
  ),
@@ -16229,7 +16317,9 @@ function registerCarouselTools(server) {
16229
16317
  const slideCount = slide_count ?? 7;
16230
16318
  const ratio = aspect_ratio ?? "1:1";
16231
16319
  let brandContext = null;
16232
- const brandProjectId = brand_id || project_id || await getDefaultProjectId();
16320
+ const defaultProjectId = await getDefaultProjectId();
16321
+ const brandProjectId = brand_id || project_id || defaultProjectId;
16322
+ const resolvedProjectId = project_id || defaultProjectId;
16233
16323
  if (brandProjectId) {
16234
16324
  brandContext = await fetchBrandVisualContext(brandProjectId);
16235
16325
  }
@@ -16238,10 +16328,7 @@ function registerCarouselTools(server) {
16238
16328
  const totalEstimatedCost = carouselTextCost + slideCount * perImageCost;
16239
16329
  const budgetCheck = checkCreditBudget(totalEstimatedCost);
16240
16330
  if (!budgetCheck.ok) {
16241
- return {
16242
- content: [{ type: "text", text: budgetCheck.message }],
16243
- isError: true
16244
- };
16331
+ return budgetCheck.error;
16245
16332
  }
16246
16333
  const assetBudget = checkAssetBudget(slideCount);
16247
16334
  if (!assetBudget.ok) {
@@ -16251,7 +16338,10 @@ function registerCarouselTools(server) {
16251
16338
  };
16252
16339
  }
16253
16340
  const userId = await getDefaultUserId();
16254
- const rateLimit = checkRateLimit("generation", `create_carousel:${userId}`);
16341
+ const rateLimit = checkRateLimit(
16342
+ "generation",
16343
+ `create_carousel:${userId}`
16344
+ );
16255
16345
  if (!rateLimit.allowed) {
16256
16346
  return {
16257
16347
  content: [
@@ -16271,7 +16361,7 @@ function registerCarouselTools(server) {
16271
16361
  slideCount,
16272
16362
  aspectRatio: ratio,
16273
16363
  style: resolvedStyle,
16274
- projectId: project_id,
16364
+ projectId: resolvedProjectId,
16275
16365
  hook,
16276
16366
  hookFamily: hook_family,
16277
16367
  ctaText: cta_text,
@@ -16285,7 +16375,12 @@ function registerCarouselTools(server) {
16285
16375
  if (carouselError || !carouselData?.carousel) {
16286
16376
  const errMsg = carouselError ?? "No carousel data returned";
16287
16377
  return {
16288
- content: [{ type: "text", text: `Carousel text generation failed: ${errMsg}` }],
16378
+ content: [
16379
+ {
16380
+ type: "text",
16381
+ text: `Carousel text generation failed: ${errMsg}`
16382
+ }
16383
+ ],
16289
16384
  isError: true
16290
16385
  };
16291
16386
  }
@@ -16314,7 +16409,8 @@ function registerCarouselTools(server) {
16314
16409
  {
16315
16410
  prompt: imagePrompt,
16316
16411
  model: image_model,
16317
- aspectRatio: ratio
16412
+ aspectRatio: ratio,
16413
+ ...resolvedProjectId && { projectId: resolvedProjectId }
16318
16414
  },
16319
16415
  { timeoutMs: 3e4 }
16320
16416
  );
@@ -16371,14 +16467,19 @@ function registerCarouselTools(server) {
16371
16467
  type: "text",
16372
16468
  text: JSON.stringify(
16373
16469
  {
16374
- _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
16470
+ _meta: {
16471
+ version: MCP_VERSION,
16472
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
16473
+ },
16375
16474
  data: {
16376
16475
  carouselId: carousel.id,
16377
16476
  templateId,
16378
16477
  style: resolvedStyle,
16379
16478
  slideCount: carousel.slides.length,
16380
16479
  slides: carousel.slides.map((s) => {
16381
- const job = imageJobs.find((j) => j.slideNumber === s.slideNumber);
16480
+ const job = imageJobs.find(
16481
+ (j) => j.slideNumber === s.slideNumber
16482
+ );
16382
16483
  return {
16383
16484
  ...s,
16384
16485
  imageJobId: job?.jobId ?? null,
@@ -16405,9 +16506,17 @@ function registerCarouselTools(server) {
16405
16506
  credits: {
16406
16507
  textGeneration: textCredits,
16407
16508
  imagesEstimated: successfulJobs.length * perImageCost,
16408
- imagesCharged: imageJobs.reduce((sum, job) => sum + (job.creditsCharged ?? 0), 0),
16409
- imagesRefunded: imageJobs.reduce((sum, job) => sum + (job.creditsRefunded ?? 0), 0),
16410
- billingUnknownSlides: imageJobs.filter((job) => job.billingStatus === "unknown").length,
16509
+ imagesCharged: imageJobs.reduce(
16510
+ (sum, job) => sum + (job.creditsCharged ?? 0),
16511
+ 0
16512
+ ),
16513
+ imagesRefunded: imageJobs.reduce(
16514
+ (sum, job) => sum + (job.creditsRefunded ?? 0),
16515
+ 0
16516
+ ),
16517
+ billingUnknownSlides: imageJobs.filter(
16518
+ (job) => job.billingStatus === "unknown"
16519
+ ).length,
16411
16520
  totalEstimated: textCredits + successfulJobs.length * perImageCost
16412
16521
  }
16413
16522
  }
@@ -16435,7 +16544,9 @@ function registerCarouselTools(server) {
16435
16544
  for (const slide of carousel.slides) {
16436
16545
  const job = imageJobs.find((j) => j.slideNumber === slide.slideNumber);
16437
16546
  const status = job?.jobId ? `image: ${job.jobId}` : `image FAILED: ${job?.error}`;
16438
- lines.push(` ${slide.slideNumber}. ${slide.headline || "(no headline)"} [${status}]`);
16547
+ lines.push(
16548
+ ` ${slide.slideNumber}. ${slide.headline || "(no headline)"} [${status}]`
16549
+ );
16439
16550
  }
16440
16551
  if (failedJobs.length > 0) {
16441
16552
  lines.push("");
@@ -16446,7 +16557,9 @@ function registerCarouselTools(server) {
16446
16557
  const jobIdList = successfulJobs.map((j) => j.jobId).join(", ");
16447
16558
  lines.push("");
16448
16559
  lines.push("Next steps:");
16449
- lines.push(` 1. Poll each job: check_status with job_id for each of: ${jobIdList}`);
16560
+ lines.push(
16561
+ ` 1. Poll each job: check_status with job_id for each of: ${jobIdList}`
16562
+ );
16450
16563
  lines.push(
16451
16564
  " 2. When all complete: schedule_post with job_ids=[...] and media_type=CAROUSEL_ALBUM"
16452
16565
  );
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@socialneuron/mcp-server",
3
3
  "mcpName": "com.socialneuron/mcp-server",
4
- "version": "1.9.0",
4
+ "version": "1.9.1",
5
5
  "description": "Official MCP server, REST API & CLI for Social Neuron — create, schedule, and optimize social content with 35+ AI models across YouTube, TikTok, and more.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
package/tools.lock.json CHANGED
@@ -60,7 +60,7 @@
60
60
  "generate_content": "20f9bdf78f1b5ab13ab698b59e6eb73246b79d0f5ffd88cd1ce4461ce3a052ed",
61
61
  "generate_image": "415eb2a66595d4312f1082c037061cb684085ba2dc40512dd36ef430bdb0de30",
62
62
  "generate_performance_digest": "6601d42259a5449069f7a8c802c9fbae841961a99d0735f2e80ac739f32360f5",
63
- "generate_video": "b1284528861276a04a2b7596333b4aff85f634c0ee965a51fafc9f46ee7a2138",
63
+ "generate_video": "d6488e325dee2e24365b6a050517deadfde0358968362769363f9a70b0b729f0",
64
64
  "generate_voiceover": "6e4e864bdcb0e47e66ccef1e3fc9aaea4ebf49ab17043bdee1708c81ae1f80aa",
65
65
  "get_active_campaigns": "62c89b5e574d3fbef9fc71c7807874e7d2c96c2608b3e4e1d27e429833dc1ef1",
66
66
  "get_autopilot_status": "ca00b246fcd55755a88b1ae655a6f715066060e1ebbb0b40fd6fc9f160fdecc5",