@socialneuron/mcp-server 1.7.18 → 1.8.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/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.7.18";
22
+ MCP_VERSION = "1.8.0";
23
23
  }
24
24
  });
25
25
 
@@ -34,6 +34,20 @@ function getRequestScopes() {
34
34
  function getRequestToken() {
35
35
  return requestContext.getStore()?.token ?? null;
36
36
  }
37
+ function getRequestSurface() {
38
+ return requestContext.getStore()?.surface ?? null;
39
+ }
40
+ function getRequestProjectId() {
41
+ return requestContext.getStore()?.projectId ?? null;
42
+ }
43
+ function resolveSurface() {
44
+ const ctx = getRequestSurface();
45
+ if (ctx) return ctx;
46
+ const transport = process.env.MCP_TRANSPORT;
47
+ if (transport === "stdio") return "mcp-stdio";
48
+ if (transport === "http") return "mcp-http";
49
+ return "cli";
50
+ }
37
51
  var requestContext;
38
52
  var init_request_context = __esm({
39
53
  "src/lib/request-context.ts"() {
@@ -348,7 +362,10 @@ async function captureToolEvent(args) {
348
362
  properties: {
349
363
  tool_name: args.toolName,
350
364
  duration_ms: args.durationMs,
351
- ...args.details
365
+ ...args.details,
366
+ // Which surface invoked the tool: mcp-stdio | mcp-http | rest | cli.
367
+ // Authoritative over any details.surface (there are no such callers today).
368
+ surface: resolveSurface()
352
369
  }
353
370
  });
354
371
  }
@@ -363,6 +380,7 @@ var init_posthog = __esm({
363
380
  "src/lib/posthog.ts"() {
364
381
  "use strict";
365
382
  init_supabase();
383
+ init_request_context();
366
384
  POSTHOG_SALT = "socialneuron-mcp-ph-v1";
367
385
  client = null;
368
386
  }
@@ -377,6 +395,7 @@ __export(supabase_exports, {
377
395
  getAuthenticatedApiKey: () => getAuthenticatedApiKey,
378
396
  getAuthenticatedEmail: () => getAuthenticatedEmail,
379
397
  getAuthenticatedExpiresAt: () => getAuthenticatedExpiresAt,
398
+ getAuthenticatedProjectId: () => getAuthenticatedProjectId,
380
399
  getAuthenticatedScopes: () => getAuthenticatedScopes,
381
400
  getDefaultProjectId: () => getDefaultProjectId,
382
401
  getDefaultUserId: () => getDefaultUserId,
@@ -428,6 +447,9 @@ async function getDefaultUserId() {
428
447
  throw new Error("No user ID available. Set SOCIALNEURON_USER_ID or authenticate via API key.");
429
448
  }
430
449
  async function getDefaultProjectId() {
450
+ const requestProjectId = getRequestProjectId();
451
+ if (requestProjectId) return requestProjectId;
452
+ if (authenticatedProjectId) return authenticatedProjectId;
431
453
  const userId = await getDefaultUserId().catch(() => null);
432
454
  if (userId) {
433
455
  const cached = projectIdCache.get(userId);
@@ -469,6 +491,7 @@ async function initializeAuth() {
469
491
  authenticatedScopes = result.scopes && result.scopes.length > 0 ? result.scopes : ["mcp:read"];
470
492
  authenticatedEmail = result.email || null;
471
493
  authenticatedExpiresAt = result.expiresAt || null;
494
+ authenticatedProjectId = result.projectId || null;
472
495
  if (!_quietAuth) {
473
496
  console.error(
474
497
  "[MCP] Authenticated via API key (prefix: " + apiKey.substring(0, 6) + "..." + apiKey.slice(-4) + ")"
@@ -525,6 +548,9 @@ function getAuthMode() {
525
548
  function getAuthenticatedApiKey() {
526
549
  return authenticatedApiKey;
527
550
  }
551
+ function getAuthenticatedProjectId() {
552
+ return authenticatedProjectId;
553
+ }
528
554
  function isTelemetryDisabled() {
529
555
  return process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true" || process.env.SOCIALNEURON_NO_TELEMETRY === "1";
530
556
  }
@@ -554,7 +580,7 @@ async function logMcpToolInvocation(args) {
554
580
  Promise.resolve().then(() => (init_posthog(), posthog_exports)).then(({ captureToolEvent: captureToolEvent2 }) => captureToolEvent2(args)).catch(() => {
555
581
  });
556
582
  }
557
- var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY, projectIdCache;
583
+ var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, authenticatedProjectId, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY, projectIdCache;
558
584
  var init_supabase = __esm({
559
585
  "src/lib/supabase.ts"() {
560
586
  "use strict";
@@ -568,6 +594,7 @@ var init_supabase = __esm({
568
594
  authenticatedEmail = null;
569
595
  authenticatedExpiresAt = null;
570
596
  authenticatedApiKey = null;
597
+ authenticatedProjectId = null;
571
598
  MCP_RUN_ID = randomUUID();
572
599
  CLOUD_SUPABASE_URL = "https://rhukkjscgzauutioyeei.supabase.co";
573
600
  CLOUD_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJodWtranNjZ3phdXV0aW95ZWVpIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ4NjM4ODYsImV4cCI6MjA4MDQzOTg4Nn0.JVtrviGvN0HaSh0JFS5KNl5FAB5ffG5Y1IMZsQFUrNQ";
@@ -2618,13 +2645,15 @@ var init_tool_catalog = __esm({
2618
2645
  name: "get_loop_pulse",
2619
2646
  description: "Read dynamic loop-health KPIs for the growth loop over the last 7 days (reflection/decision coverage, visual gate pass rate, learning-update application rate, per-platform uptake, autopilot lag) \u2014 each with an ok/warn/bad status. Use to decide whether the loop is closing or where it is stuck.",
2620
2647
  module: "loop",
2621
- scope: "mcp:read"
2648
+ scope: "mcp:read",
2649
+ internal: true
2622
2650
  },
2623
2651
  {
2624
2652
  name: "get_bandit_state",
2625
2653
  description: "Read the current content learning state for a project \u2014 top-K arms per (arm_type, platform) with expected performance and uncertainty. Use to reason about which hook family / format / timing slot currently performs best per platform.",
2626
2654
  module: "loop",
2627
- scope: "mcp:read"
2655
+ scope: "mcp:read",
2656
+ internal: true
2628
2657
  }
2629
2658
  ];
2630
2659
  }
@@ -2980,7 +3009,9 @@ var init_tool_annotations = __esm({
2980
3009
  },
2981
3010
  "mcp:write": {
2982
3011
  readOnlyHint: false,
2983
- destructiveHint: true,
3012
+ // Most write tools create additive drafts/assets. Per MCP semantics,
3013
+ // destructiveHint is reserved for overwrite/delete/irreversible behavior.
3014
+ destructiveHint: false,
2984
3015
  idempotentHint: false,
2985
3016
  openWorldHint: false
2986
3017
  },
@@ -2998,21 +3029,30 @@ var init_tool_annotations = __esm({
2998
3029
  },
2999
3030
  "mcp:comments": {
3000
3031
  readOnlyHint: false,
3001
- destructiveHint: true,
3032
+ destructiveHint: false,
3002
3033
  idempotentHint: false,
3003
3034
  openWorldHint: true
3004
3035
  },
3005
3036
  "mcp:autopilot": {
3006
3037
  readOnlyHint: false,
3007
- destructiveHint: true,
3038
+ destructiveHint: false,
3008
3039
  idempotentHint: false,
3009
3040
  openWorldHint: false
3010
3041
  }
3011
3042
  };
3012
3043
  OVERRIDES = {
3013
- // Destructive tools
3044
+ // Destructive or overwrite tools
3014
3045
  delete_comment: { destructiveHint: true },
3015
3046
  moderate_comment: { destructiveHint: true },
3047
+ save_brand_profile: { destructiveHint: true, idempotentHint: true },
3048
+ update_platform_voice: { destructiveHint: true, idempotentHint: true },
3049
+ update_content_plan: { destructiveHint: true, idempotentHint: true },
3050
+ respond_plan_approval: { destructiveHint: true, idempotentHint: true },
3051
+ update_autopilot_config: { destructiveHint: true, idempotentHint: true },
3052
+ run_content_pipeline: { destructiveHint: true, openWorldHint: true },
3053
+ auto_approve_plan: { destructiveHint: true, idempotentHint: true },
3054
+ execute_recipe: { destructiveHint: true, openWorldHint: true },
3055
+ run_skill: { destructiveHint: true },
3016
3056
  // Read-only tools in non-read scopes (must also clear destructiveHint from scope default)
3017
3057
  list_comments: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
3018
3058
  list_autopilot_configs: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
@@ -3023,14 +3063,12 @@ var init_tool_annotations = __esm({
3023
3063
  // Analytics tool that triggers side effects (data refresh)
3024
3064
  refresh_platform_analytics: { readOnlyHint: false, idempotentHint: true },
3025
3065
  // Write tools that are idempotent
3026
- save_brand_profile: { idempotentHint: true },
3027
- update_platform_voice: { idempotentHint: true },
3028
- update_autopilot_config: { idempotentHint: true },
3029
- update_content_plan: { idempotentHint: true },
3030
- respond_plan_approval: { idempotentHint: true },
3031
3066
  // Distribution is open-world (publishes to external platforms)
3032
3067
  schedule_post: { openWorldHint: true },
3033
3068
  schedule_content_plan: { openWorldHint: true },
3069
+ // This only creates a short-lived OAuth deep link; it does not connect the
3070
+ // external account until the user completes the browser flow.
3071
+ start_platform_connection: { destructiveHint: false },
3034
3072
  // Extraction reads external URLs
3035
3073
  extract_url_content: { openWorldHint: true },
3036
3074
  extract_brand: { openWorldHint: true },
@@ -3038,8 +3076,6 @@ var init_tool_annotations = __esm({
3038
3076
  check_pipeline_readiness: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
3039
3077
  get_pipeline_status: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
3040
3078
  // Pipeline: orchestration tools (non-idempotent, may schedule externally)
3041
- run_content_pipeline: { openWorldHint: true },
3042
- auto_approve_plan: { idempotentHint: true },
3043
3079
  // Suggest: read-only
3044
3080
  suggest_next_content: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
3045
3081
  // Digest/Anomalies: read-only analytics
@@ -3090,9 +3126,89 @@ function toolError(code, message, opts = {}) {
3090
3126
  ...opts.meta ? { _meta: opts.meta } : {}
3091
3127
  };
3092
3128
  }
3129
+ function classifyToolError(result) {
3130
+ const structured = result.structuredContent?.error?.error_type;
3131
+ if (structured && KNOWN_ERROR_TYPES.has(structured)) return structured;
3132
+ const text = result.content?.find((c) => c.type === "text")?.text ?? "";
3133
+ try {
3134
+ const parsed = JSON.parse(text);
3135
+ if (parsed.error_type && KNOWN_ERROR_TYPES.has(parsed.error_type)) {
3136
+ return parsed.error_type;
3137
+ }
3138
+ } catch {
3139
+ }
3140
+ if (/-32602|Input validation error|Invalid arguments/i.test(text)) {
3141
+ return "validation_error";
3142
+ }
3143
+ return "server_error";
3144
+ }
3145
+ var KNOWN_ERROR_TYPES;
3093
3146
  var init_tool_error = __esm({
3094
3147
  "src/lib/tool-error.ts"() {
3095
3148
  "use strict";
3149
+ KNOWN_ERROR_TYPES = /* @__PURE__ */ new Set([
3150
+ "policy_block",
3151
+ "validation_error",
3152
+ "permission_denied",
3153
+ "billing_error",
3154
+ "rate_limited",
3155
+ "not_found",
3156
+ "upstream_error",
3157
+ "server_error"
3158
+ ]);
3159
+ }
3160
+ });
3161
+
3162
+ // src/lib/tool-profile.ts
3163
+ function isToolAllowedByProfile(toolName, profile) {
3164
+ return profile === "full" || !ANTHROPIC_DIRECTORY_EXCLUDED_TOOLS.has(toolName);
3165
+ }
3166
+ function applyToolProfile(server, profile) {
3167
+ if (profile === "full") return;
3168
+ const currentTool = server.tool.bind(server);
3169
+ const currentRegisterTool = server.registerTool?.bind(server);
3170
+ server.tool = function profiledTool(...args) {
3171
+ const name = String(args[0] ?? "");
3172
+ if (!isToolAllowedByProfile(name, profile)) return void 0;
3173
+ return currentTool(...args);
3174
+ };
3175
+ if (currentRegisterTool) {
3176
+ server.registerTool = function profiledRegisterTool(...args) {
3177
+ const name = String(args[0] ?? "");
3178
+ if (!isToolAllowedByProfile(name, profile)) return void 0;
3179
+ return currentRegisterTool(...args);
3180
+ };
3181
+ }
3182
+ }
3183
+ var ANTHROPIC_DIRECTORY_EXCLUDED_TOOLS;
3184
+ var init_tool_profile = __esm({
3185
+ "src/lib/tool-profile.ts"() {
3186
+ "use strict";
3187
+ init_tool_catalog();
3188
+ ANTHROPIC_DIRECTORY_EXCLUDED_TOOLS = /* @__PURE__ */ new Set([
3189
+ "generate_video",
3190
+ "generate_image",
3191
+ "check_status",
3192
+ "create_storyboard",
3193
+ "generate_voiceover",
3194
+ "generate_carousel",
3195
+ "create_carousel",
3196
+ "render_demo_video",
3197
+ "list_compositions",
3198
+ "render_template_video",
3199
+ "check_pipeline_readiness",
3200
+ "run_content_pipeline",
3201
+ "get_pipeline_status",
3202
+ "auto_approve_plan",
3203
+ "list_recipes",
3204
+ "get_recipe_details",
3205
+ "execute_recipe",
3206
+ "get_recipe_run_status",
3207
+ "list_hyperframes_blocks",
3208
+ "render_hyperframes",
3209
+ "list_skills",
3210
+ "run_skill"
3211
+ ]);
3096
3212
  }
3097
3213
  });
3098
3214
 
@@ -3759,6 +3875,52 @@ var init_ideation = __esm({
3759
3875
  }
3760
3876
  });
3761
3877
 
3878
+ // src/lib/checkStatusShape.ts
3879
+ function buildCheckStatusPayload(job, liveStatus) {
3880
+ const status = liveStatus?.status ?? job.status;
3881
+ const progress = liveStatus?.progress ?? null;
3882
+ const resultUrl = liveStatus?.resultUrl ?? job.result_url ?? null;
3883
+ const r2Key = resultUrl && !resultUrl.startsWith("http") ? resultUrl : null;
3884
+ const errorMessage = liveStatus?.error ?? job.error_message ?? null;
3885
+ const allUrls = job.result_metadata?.all_urls ?? null;
3886
+ const modelRequested = job.result_metadata?.model_requested ?? null;
3887
+ const modelDelivered = job.result_metadata?.model_delivered ?? null;
3888
+ const fallbackReason = job.result_metadata?.fallback_reason ?? null;
3889
+ return {
3890
+ job_id: job.id,
3891
+ job_type: job.job_type,
3892
+ model: job.model,
3893
+ status,
3894
+ progress,
3895
+ result_url: resultUrl,
3896
+ r2_key: r2Key,
3897
+ all_urls: allUrls,
3898
+ error: errorMessage,
3899
+ credits_cost: job.credits_cost,
3900
+ created_at: job.created_at,
3901
+ completed_at: job.completed_at,
3902
+ model_requested: modelRequested,
3903
+ model_delivered: modelDelivered,
3904
+ fallback_reason: fallbackReason,
3905
+ id: job.id,
3906
+ jobId: job.id,
3907
+ jobType: job.job_type,
3908
+ resultUrl,
3909
+ credits: job.credits_cost,
3910
+ error_message: errorMessage,
3911
+ createdAt: job.created_at,
3912
+ completedAt: job.completed_at,
3913
+ modelRequested,
3914
+ modelDelivered,
3915
+ fallbackReason
3916
+ };
3917
+ }
3918
+ var init_checkStatusShape = __esm({
3919
+ "src/lib/checkStatusShape.ts"() {
3920
+ "use strict";
3921
+ }
3922
+ });
3923
+
3762
3924
  // src/lib/budget.ts
3763
3925
  function getCreditsUsed() {
3764
3926
  const ctx = requestContext.getStore();
@@ -3848,34 +4010,32 @@ function asEnvelope2(data) {
3848
4010
  function registerContentTools(server) {
3849
4011
  server.tool(
3850
4012
  "generate_video",
3851
- "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.",
4013
+ "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.",
3852
4014
  {
3853
4015
  prompt: z2.string().max(2500).describe(
3854
4016
  '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.'
3855
4017
  ),
3856
- model: z2.enum([
3857
- "veo3-fast",
3858
- "veo3-quality",
3859
- "runway-aleph",
3860
- "sora2",
3861
- "sora2-pro",
3862
- "kling",
3863
- "kling-3",
3864
- "kling-3-pro"
3865
- ]).describe(
3866
- "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)."
4018
+ model: z2.enum(VIDEO_MODEL_ENUM).describe(
4019
+ "Video model, quality ladder best->worst: seedance-2-fast (264cr/8s, S-tier, native audio, top quality/credit) > kling-3 (100cr/5s no-audio, compound prompts, up to 10s) > grok-imagine (30cr/6s, cheapest real model, great image-to-video) > veo3-fast (65cr/8s, Veo 3.1 Fast, native audio) > kling-3-pro (135cr/5s no-audio, up to 15s) > seedance-2 (328cr/8s 720p, premium cinematic, native audio) > veo3-quality (1000cr/8s, Veo 3.1 Quality, hero shots) > wan-2.6 (105cr/5s, fallback) > gemini-omni-video (126cr \u2014 pick this ONLY for multi-reference composites/edits, not raw quality). hailuo-02-standard (180cr), seedance-1.5-pro (150cr), kling (170cr/10s no-audio) are legacy/budget fallbacks."
3867
4020
  ),
3868
4021
  duration: z2.number().min(3).max(30).optional().describe(
3869
- "Video duration in seconds. kling: 5-30s, kling-3/kling-3-pro: 3-15s, sora2: 10-15s. Defaults to 5 seconds."
4022
+ "Video duration in seconds. kling: 5 or 10s \xB7 kling-3: 5 or 10s \xB7 kling-3-pro: 5/10/15s \xB7 grok-imagine/hailuo: 6 or 10s \xB7 wan-2.6: 5/10/15s \xB7 seedance family: 4-15s \xB7 veo3-fast/veo3-quality: fixed 8s (duration ignored). Out-of-range values are clamped server-side, never rejected. Defaults to 5 seconds."
3870
4023
  ),
3871
4024
  aspect_ratio: z2.enum(["16:9", "9:16", "1:1"]).optional().describe(
3872
4025
  "Video aspect ratio. 16:9 for YouTube/landscape, 9:16 for TikTok/Reels/Shorts, 1:1 for Instagram feed/square. Defaults to 16:9."
3873
4026
  ),
3874
4027
  enable_audio: z2.boolean().optional().describe(
3875
- "Enable native audio generation. Kling 2.6: doubles cost. Kling 3.0: 50% more (std 30/sec, pro 40/sec). 5+ languages."
4028
+ "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."
4029
+ ),
4030
+ image_url: z2.string().optional().describe(
4031
+ "Start frame image URL for image-to-video (Kling 3.0 frame control)."
4032
+ ),
4033
+ end_frame_url: z2.string().optional().describe(
4034
+ "End frame image URL (Kling 3.0 only). Enables seamless loop transitions."
4035
+ ),
4036
+ project_id: z2.string().optional().describe(
4037
+ "Project ID to associate the video with (brand context is auto-injected from the project brand profile). Omit to generate without a project association."
3876
4038
  ),
3877
- image_url: z2.string().optional().describe("Start frame image URL for image-to-video (Kling 3.0 frame control)."),
3878
- end_frame_url: z2.string().optional().describe("End frame image URL (Kling 3.0 only). Enables seamless loop transitions."),
3879
4039
  response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
3880
4040
  },
3881
4041
  async ({
@@ -3886,6 +4046,7 @@ function registerContentTools(server) {
3886
4046
  enable_audio,
3887
4047
  image_url,
3888
4048
  end_frame_url,
4049
+ project_id,
3889
4050
  response_format
3890
4051
  }) => {
3891
4052
  const format = response_format ?? "text";
@@ -3924,9 +4085,14 @@ function registerContentTools(server) {
3924
4085
  model,
3925
4086
  duration: duration ?? 5,
3926
4087
  aspectRatio: aspect_ratio ?? "16:9",
3927
- enableAudio: enable_audio ?? true,
4088
+ // Default FALSE (2026-07-13): the old `?? true` default silently multiplied
4089
+ // kling-family costs (2.65x on kling 2.6) for callers that never asked for audio.
4090
+ enableAudio: enable_audio ?? false,
3928
4091
  ...image_url && { imageUrl: image_url },
3929
- ...end_frame_url && { endFrameUrl: end_frame_url }
4092
+ ...end_frame_url && { endFrameUrl: end_frame_url },
4093
+ // The server reads projectId and enforces
4094
+ // ownership via resolveProjectAndContent (index.ts:416-423).
4095
+ ...project_id && { projectId: project_id }
3930
4096
  },
3931
4097
  { timeoutMs: 3e4 }
3932
4098
  );
@@ -4124,7 +4290,7 @@ function registerContentTools(server) {
4124
4290
  );
4125
4291
  server.tool(
4126
4292
  "check_status",
4127
- '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.',
4293
+ '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. JSON shape (response_format=json) is stable across the whole poll lifecycle \u2014 canonical fields job_id, status, progress, result_url, r2_key, all_urls, error, credits_cost, created_at, completed_at are always present (never only camelCase or only snake_case); legacy aliases jobId/resultUrl/credits/createdAt/completedAt/error_message are also always populated for backward compatibility.',
4128
4294
  {
4129
4295
  job_id: z2.string().describe(
4130
4296
  "The job ID returned by generate_video or generate_image. This is the asyncJobId or taskId value."
@@ -4170,10 +4336,13 @@ function registerContentTools(server) {
4170
4336
  };
4171
4337
  }
4172
4338
  if (job.external_id && (job.status === "pending" || job.status === "processing")) {
4173
- const { data: liveStatus } = await callEdgeFunction("kie-task-status", {
4174
- taskId: job.external_id,
4175
- model: job.model
4176
- });
4339
+ const { data: liveStatus } = await callEdgeFunction(
4340
+ "kie-task-status",
4341
+ {
4342
+ taskId: job.external_id,
4343
+ model: job.model
4344
+ }
4345
+ );
4177
4346
  if (liveStatus) {
4178
4347
  const lines2 = [
4179
4348
  `Job: ${job.id}`,
@@ -4196,13 +4365,17 @@ function registerContentTools(server) {
4196
4365
  {
4197
4366
  type: "text",
4198
4367
  text: JSON.stringify(
4368
+ // v1.8.0: spread legacy live-branch fields first for
4369
+ // backward compatibility, then overlay
4370
+ // buildCheckStatusPayload's canonical snake_case fields +
4371
+ // both-alias set so a consumer never sees a different
4372
+ // field name depending on which branch served the poll.
4373
+ // lib/checkStatusShape.ts is the single stable shape —
4374
+ // it derives jobId/jobType/model/credits/createdAt from
4375
+ // `job` + `liveStatus`; do not duplicate them here.
4199
4376
  asEnvelope2({
4200
- jobId: job.id,
4201
- jobType: job.job_type,
4202
- model: job.model,
4203
4377
  ...liveStatus,
4204
- credits: job.credits_cost,
4205
- createdAt: job.created_at
4378
+ ...buildCheckStatusPayload(job, liveStatus)
4206
4379
  }),
4207
4380
  null,
4208
4381
  2
@@ -4229,7 +4402,7 @@ function registerContentTools(server) {
4229
4402
  const filename = segments[segments.length - 1] || "media";
4230
4403
  lines.push(`Media ready: ${filename}`);
4231
4404
  lines.push(
4232
- "(Pass job_id directly to schedule_post, or use get_media_url with job_id for a download link)"
4405
+ "(Pass job_id directly to schedule_post, or pass response_format=json's r2_key to get_media_url for a download link)"
4233
4406
  );
4234
4407
  } else {
4235
4408
  lines.push(`Result URL: ${job.result_url}`);
@@ -4253,11 +4426,15 @@ function registerContentTools(server) {
4253
4426
  if (format === "json") {
4254
4427
  const enriched = {
4255
4428
  ...job,
4256
- r2_key: job.result_url && !job.result_url.startsWith("http") ? job.result_url : null,
4257
- all_urls: allUrls ?? null
4429
+ ...buildCheckStatusPayload(job)
4258
4430
  };
4259
4431
  return {
4260
- content: [{ type: "text", text: JSON.stringify(asEnvelope2(enriched), null, 2) }]
4432
+ content: [
4433
+ {
4434
+ type: "text",
4435
+ text: JSON.stringify(asEnvelope2(enriched), null, 2)
4436
+ }
4437
+ ]
4261
4438
  };
4262
4439
  }
4263
4440
  return {
@@ -4275,7 +4452,15 @@ function registerContentTools(server) {
4275
4452
  brand_context: z2.string().max(3e3).optional().describe(
4276
4453
  "Brand context JSON from extract_brand. Include colors, voice tone, visual style keywords for consistent branding across frames."
4277
4454
  ),
4278
- platform: z2.enum(["tiktok", "instagram-reels", "youtube-shorts", "youtube", "general"]).describe("Target platform. Determines aspect ratio, duration, and pacing."),
4455
+ platform: z2.enum([
4456
+ "tiktok",
4457
+ "instagram-reels",
4458
+ "youtube-shorts",
4459
+ "youtube",
4460
+ "general"
4461
+ ]).describe(
4462
+ "Target platform. Determines aspect ratio, duration, and pacing."
4463
+ ),
4279
4464
  target_duration: z2.number().min(5).max(120).optional().describe(
4280
4465
  "Target total duration in seconds. Defaults to 30s for short-form, 60s for YouTube."
4281
4466
  ),
@@ -4283,7 +4468,9 @@ function registerContentTools(server) {
4283
4468
  style: z2.string().optional().describe(
4284
4469
  'Visual style direction (e.g., "cinematic", "anime", "documentary", "motion graphics").'
4285
4470
  ),
4286
- response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to json for structured storyboard data.")
4471
+ response_format: z2.enum(["text", "json"]).optional().describe(
4472
+ "Response format. Defaults to json for structured storyboard data."
4473
+ )
4287
4474
  },
4288
4475
  async ({
4289
4476
  concept,
@@ -4295,7 +4482,11 @@ function registerContentTools(server) {
4295
4482
  response_format
4296
4483
  }) => {
4297
4484
  const format = response_format ?? "json";
4298
- const isShortForm = ["tiktok", "instagram-reels", "youtube-shorts"].includes(platform3);
4485
+ const isShortForm = [
4486
+ "tiktok",
4487
+ "instagram-reels",
4488
+ "youtube-shorts"
4489
+ ].includes(platform3);
4299
4490
  const duration = target_duration ?? (isShortForm ? 30 : 60);
4300
4491
  const scenes = num_scenes ?? (isShortForm ? 7 : 10);
4301
4492
  const aspectRatio = isShortForm ? "9:16" : "16:9";
@@ -4376,7 +4567,12 @@ Return ONLY valid JSON in this exact format:
4376
4567
  );
4377
4568
  if (error) {
4378
4569
  return {
4379
- content: [{ type: "text", text: `Storyboard generation failed: ${error}` }],
4570
+ content: [
4571
+ {
4572
+ type: "text",
4573
+ text: `Storyboard generation failed: ${error}`
4574
+ }
4575
+ ],
4380
4576
  isError: true
4381
4577
  };
4382
4578
  }
@@ -4386,7 +4582,12 @@ Return ONLY valid JSON in this exact format:
4386
4582
  try {
4387
4583
  const parsed = JSON.parse(rawContent);
4388
4584
  return {
4389
- content: [{ type: "text", text: JSON.stringify(asEnvelope2(parsed), null, 2) }]
4585
+ content: [
4586
+ {
4587
+ type: "text",
4588
+ text: JSON.stringify(asEnvelope2(parsed), null, 2)
4589
+ }
4590
+ ]
4390
4591
  };
4391
4592
  } catch {
4392
4593
  return {
@@ -4431,7 +4632,10 @@ Return ONLY valid JSON in this exact format:
4431
4632
  isError: true
4432
4633
  };
4433
4634
  }
4434
- const rateLimit = checkRateLimit("posting", `generate_voiceover:${userId}`);
4635
+ const rateLimit = checkRateLimit(
4636
+ "posting",
4637
+ `generate_voiceover:${userId}`
4638
+ );
4435
4639
  if (!rateLimit.allowed) {
4436
4640
  return {
4437
4641
  content: [
@@ -4458,14 +4662,22 @@ Return ONLY valid JSON in this exact format:
4458
4662
  );
4459
4663
  if (error) {
4460
4664
  return {
4461
- content: [{ type: "text", text: `Voiceover generation failed: ${error}` }],
4665
+ content: [
4666
+ {
4667
+ type: "text",
4668
+ text: `Voiceover generation failed: ${error}`
4669
+ }
4670
+ ],
4462
4671
  isError: true
4463
4672
  };
4464
4673
  }
4465
4674
  if (!data?.audioUrl) {
4466
4675
  return {
4467
4676
  content: [
4468
- { type: "text", text: "Voiceover generation failed: no audio URL returned." }
4677
+ {
4678
+ type: "text",
4679
+ text: "Voiceover generation failed: no audio URL returned."
4680
+ }
4469
4681
  ],
4470
4682
  isError: true
4471
4683
  };
@@ -4527,20 +4739,30 @@ Return ONLY valid JSON in this exact format:
4527
4739
  "Carousel template. hormozi-authority: bold typography, one idea per slide, dark backgrounds. educational-series: numbered tips. Default: hormozi-authority."
4528
4740
  ),
4529
4741
  slide_count: z2.number().min(3).max(10).optional().describe("Number of slides (3-10). Default: 7."),
4530
- aspect_ratio: z2.enum(["1:1", "4:5", "9:16"]).optional().describe("Aspect ratio. 1:1 square (default), 4:5 portrait, 9:16 story."),
4742
+ aspect_ratio: z2.enum(["1:1", "4:5", "9:16"]).optional().describe(
4743
+ "Aspect ratio. 1:1 square (default), 4:5 portrait, 9:16 story."
4744
+ ),
4531
4745
  style: z2.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
4532
4746
  "Visual style. hormozi: black bg, bold white text, gold accents. Default: hormozi (when using hormozi-authority template)."
4533
4747
  ),
4534
4748
  hook: z2.string().max(300).optional().describe(
4535
4749
  "Explicit hook/opener for slide 1. Overrides any hook derived from topic. Keep under 15 words."
4536
4750
  ),
4537
- hook_family: z2.enum(["curiosity", "authority", "pain_point", "contrarian", "data_driven"]).optional().describe(
4538
- "Hook family tag. Persisted with the carousel so bandit learners can attribute engagement to hook pattern."
4751
+ hook_family: z2.enum([
4752
+ "curiosity",
4753
+ "authority",
4754
+ "pain_point",
4755
+ "contrarian",
4756
+ "data_driven"
4757
+ ]).optional().describe(
4758
+ "Hook family tag. Persisted with the carousel so downstream analytics can attribute engagement to hook pattern."
4539
4759
  ),
4540
4760
  cta_text: z2.string().max(200).optional().describe("Explicit CTA copy for the final slide."),
4541
4761
  cta_url: z2.string().url().optional().describe("URL promoted on the CTA slide."),
4542
4762
  tone: z2.string().max(200).optional().describe("Voice/tone override. Composes with brand profile voice."),
4543
- constraints: z2.string().max(500).optional().describe('Content constraints. Example: "No fabricated statistics. Sentence case only."'),
4763
+ constraints: z2.string().max(500).optional().describe(
4764
+ 'Content constraints. Example: "No fabricated statistics. Sentence case only."'
4765
+ ),
4544
4766
  platform: z2.enum(["linkedin", "instagram", "tiktok", "x"]).optional().describe("Target platform. Affects tone and format guardrails."),
4545
4767
  project_id: z2.string().optional().describe("Project ID to associate the carousel with."),
4546
4768
  response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to json.")
@@ -4575,7 +4797,10 @@ Return ONLY valid JSON in this exact format:
4575
4797
  };
4576
4798
  }
4577
4799
  const userId = await getDefaultUserId();
4578
- const rateLimit = checkRateLimit("posting", `generate_carousel:${userId}`);
4800
+ const rateLimit = checkRateLimit(
4801
+ "posting",
4802
+ `generate_carousel:${userId}`
4803
+ );
4579
4804
  if (!rateLimit.allowed) {
4580
4805
  return {
4581
4806
  content: [
@@ -4608,13 +4833,23 @@ Return ONLY valid JSON in this exact format:
4608
4833
  );
4609
4834
  if (error) {
4610
4835
  return {
4611
- content: [{ type: "text", text: `Carousel generation failed: ${error}` }],
4836
+ content: [
4837
+ {
4838
+ type: "text",
4839
+ text: `Carousel generation failed: ${error}`
4840
+ }
4841
+ ],
4612
4842
  isError: true
4613
4843
  };
4614
4844
  }
4615
4845
  if (!data?.carousel) {
4616
4846
  return {
4617
- content: [{ type: "text", text: "Carousel generation returned no data." }],
4847
+ content: [
4848
+ {
4849
+ type: "text",
4850
+ text: "Carousel generation returned no data."
4851
+ }
4852
+ ],
4618
4853
  isError: true
4619
4854
  };
4620
4855
  }
@@ -4662,7 +4897,7 @@ Return ONLY valid JSON in this exact format:
4662
4897
  }
4663
4898
  );
4664
4899
  }
4665
- var VIDEO_CREDIT_ESTIMATES, IMAGE_CREDIT_ESTIMATES;
4900
+ var VIDEO_CREDIT_ESTIMATES, VIDEO_MODEL_ENUM, IMAGE_CREDIT_ESTIMATES;
4666
4901
  var init_content2 = __esm({
4667
4902
  "src/tools/content.ts"() {
4668
4903
  "use strict";
@@ -4670,17 +4905,36 @@ var init_content2 = __esm({
4670
4905
  init_rate_limit();
4671
4906
  init_supabase();
4672
4907
  init_version();
4908
+ init_checkStatusShape();
4673
4909
  init_budget();
4674
4910
  VIDEO_CREDIT_ESTIMATES = {
4675
- "veo3-fast": 200,
4676
- "veo3-quality": 1e3,
4677
- "runway-aleph": 340,
4678
- sora2: 500,
4679
- "sora2-pro": 1500,
4680
- kling: 170,
4911
+ "seedance-2-fast": 264,
4681
4912
  "kling-3": 100,
4682
- "kling-3-pro": 135
4913
+ "grok-imagine": 30,
4914
+ "veo3-fast": 65,
4915
+ "kling-3-pro": 135,
4916
+ "seedance-2": 328,
4917
+ "veo3-quality": 1e3,
4918
+ "wan-2.6": 105,
4919
+ "gemini-omni-video": 126,
4920
+ "hailuo-02-standard": 180,
4921
+ "seedance-1.5-pro": 150,
4922
+ kling: 170
4683
4923
  };
4924
+ VIDEO_MODEL_ENUM = [
4925
+ "seedance-2-fast",
4926
+ "kling-3",
4927
+ "grok-imagine",
4928
+ "veo3-fast",
4929
+ "kling-3-pro",
4930
+ "seedance-2",
4931
+ "veo3-quality",
4932
+ "wan-2.6",
4933
+ "gemini-omni-video",
4934
+ "hailuo-02-standard",
4935
+ "seedance-1.5-pro",
4936
+ "kling"
4937
+ ];
4684
4938
  IMAGE_CREDIT_ESTIMATES = {
4685
4939
  midjourney: 20,
4686
4940
  "nano-banana": 15,
@@ -4750,6 +5004,7 @@ var init_sanitize_error = __esm({
4750
5004
  });
4751
5005
 
4752
5006
  // src/lib/ssrf.ts
5007
+ import { promises as dnsPromises } from "node:dns";
4753
5008
  function isBlockedIP(ip) {
4754
5009
  const normalized = ip.replace(/^\[/, "").replace(/\]$/, "");
4755
5010
  if (normalized.includes(":")) {
@@ -4803,8 +5058,7 @@ async function validateUrlForSSRF(urlString) {
4803
5058
  let resolvedIP;
4804
5059
  if (!isIPAddress(hostname)) {
4805
5060
  try {
4806
- const dns = await import("node:dns");
4807
- const resolver = new dns.promises.Resolver();
5061
+ const resolver = new dnsPromises.Resolver();
4808
5062
  const resolvedIPs = [];
4809
5063
  try {
4810
5064
  const aRecords = await resolver.resolve4(hostname);
@@ -4930,6 +5184,24 @@ function asEnvelope3(data) {
4930
5184
  data
4931
5185
  };
4932
5186
  }
5187
+ function accountEffectiveStatus(account) {
5188
+ return account.effective_status || account.status;
5189
+ }
5190
+ function isUsableAccount(account) {
5191
+ const status = accountEffectiveStatus(account);
5192
+ return status === "active" || status === "expires_soon";
5193
+ }
5194
+ function formatAccountChoice(account) {
5195
+ const name = account.username ? `@${account.username}` : "unnamed";
5196
+ const project = account.project_id ? `project_id=${account.project_id}` : "project_id=unassigned";
5197
+ const refresh = account.has_refresh_token ? "OAuth 2.0 refresh" : "no refresh token";
5198
+ return `${account.id} (${name}, ${project}, ${accountEffectiveStatus(account)}, ${refresh})`;
5199
+ }
5200
+ function requestedAccountIdForPlatform(accountId, accountIds, platform3) {
5201
+ if (accountId) return accountId;
5202
+ if (!accountIds) return void 0;
5203
+ return accountIds[platform3] || accountIds[platform3.toLowerCase()];
5204
+ }
4933
5205
  function isAlreadyR2Signed(url) {
4934
5206
  try {
4935
5207
  const u = new URL(url);
@@ -5073,16 +5345,18 @@ function registerDistributionTools(server) {
5073
5345
  schedule_at: z3.string().optional().describe(
5074
5346
  'ISO 8601 UTC datetime for scheduled posting (e.g. "2026-03-20T14:00:00Z"). Omit to post immediately. Must be in the future.'
5075
5347
  ),
5076
- project_id: z3.string().optional().describe("Social Neuron project ID to associate this post with."),
5348
+ project_id: z3.string().optional().describe(
5349
+ "Social Neuron brand/project ID to associate this post with. Provide this when the account has multiple brands so brand voice and connected account routing stay scoped to the right brand."
5350
+ ),
5077
5351
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
5078
5352
  attribution: z3.boolean().optional().describe(
5079
5353
  'If true, appends "Created with Social Neuron" to the caption. Default: false.'
5080
5354
  ),
5081
5355
  account_id: z3.string().optional().describe(
5082
- "Connected account ID to post from. Use list_connected_accounts to find the right ID. Required when multiple accounts exist for the same platform."
5356
+ "Connected account ID to post from. Use list_connected_accounts to find the right ID. Required when multiple accounts exist for the same platform. If project_id is provided, the account must belong to that project or be unassigned."
5083
5357
  ),
5084
5358
  account_ids: z3.record(z3.string(), z3.string()).optional().describe(
5085
- 'Per-platform account IDs when posting to multiple platforms. Example: {"twitter": "abc123", "instagram": "def456"}. Use list_connected_accounts to find IDs.'
5359
+ 'Per-platform account IDs when posting to multiple platforms. Example: {"twitter": "abc123", "instagram": "def456"}. Use list_connected_accounts with the same project_id to find IDs.'
5086
5360
  ),
5087
5361
  auto_rehost: z3.boolean().optional().describe(
5088
5362
  "Whether to persist non-R2 media_url/media_urls into R2 before posting. Default: true. Set to false only if you know the source URL will outlive the scheduling window and every target platform supports URL ingest."
@@ -5292,22 +5566,53 @@ function registerDistributionTools(server) {
5292
5566
  isError: true
5293
5567
  };
5294
5568
  }
5295
- const { data: accountsData } = await callEdgeFunction("mcp-data", { action: "connected-accounts" }, { timeoutMs: 1e4 });
5569
+ const { data: accountsData } = await callEdgeFunction(
5570
+ "mcp-data",
5571
+ {
5572
+ action: "connected-accounts",
5573
+ ...project_id ? { projectId: project_id, project_id } : {}
5574
+ },
5575
+ { timeoutMs: 1e4 }
5576
+ );
5296
5577
  if (accountsData?.accounts) {
5297
5578
  const accounts = accountsData.accounts;
5298
5579
  const issues = [];
5299
5580
  for (const platform3 of normalizedPlatforms) {
5300
5581
  const platformAccounts = accounts.filter(
5301
- (a) => a.platform.toLowerCase() === platform3.toLowerCase() && a.status === "active"
5582
+ (a) => a.platform.toLowerCase() === platform3.toLowerCase() && isUsableAccount(a)
5583
+ );
5584
+ const requestedAccountId = requestedAccountIdForPlatform(
5585
+ account_id,
5586
+ account_ids,
5587
+ platform3
5302
5588
  );
5589
+ if (requestedAccountId) {
5590
+ const selected = accounts.find((a) => a.id === requestedAccountId);
5591
+ if (!selected) {
5592
+ issues.push(
5593
+ `${platform3}: Account "${requestedAccountId}" is not available${project_id ? ` for project_id ${project_id}` : ""}. Call \`list_connected_accounts\`${project_id ? " with the same project_id" : ""} and choose one of the returned IDs.`
5594
+ );
5595
+ } else if (selected.platform.toLowerCase() !== platform3.toLowerCase()) {
5596
+ issues.push(
5597
+ `${platform3}: Account "${requestedAccountId}" belongs to ${selected.platform}, not ${platform3}.`
5598
+ );
5599
+ } else if (!isUsableAccount(selected)) {
5600
+ issues.push(
5601
+ `${platform3}: Account "${selected.username || selected.id}" is ${accountEffectiveStatus(selected)}. Reconnect at socialneuron.com/settings/connections.`
5602
+ );
5603
+ } else if (project_id && selected.project_id && selected.project_id !== project_id) {
5604
+ issues.push(
5605
+ `${platform3}: Account "${selected.username || selected.id}" is bound to project_id ${selected.project_id}, not ${project_id}. Use the selected brand's account or reconnect/assign it in Settings > Integrations.`
5606
+ );
5607
+ }
5608
+ continue;
5609
+ }
5303
5610
  if (platformAccounts.length === 0) {
5304
5611
  issues.push(
5305
- `${platform3}: not connected yet. This is a one-time browser setup on socialneuron.com \u2014 NOT another OAuth in Claude. Call \`start_platform_connection\` with platform="${platform3.toLowerCase()}" to get a deep link, ask the user to open it in their browser and approve on the platform, then call \`wait_for_connection\` before retrying schedule_post.`
5612
+ `${platform3}: not connected yet. This is a one-time browser setup on socialneuron.com \u2014 NOT another OAuth in Claude. Call \`start_platform_connection\` with platform="${platform3.toLowerCase()}"${project_id ? ` and project_id="${project_id}"` : ""} to get a deep link, ask the user to open it in their browser and approve on the platform, then call \`wait_for_connection\` before retrying schedule_post.`
5306
5613
  );
5307
- } else if (platformAccounts.length > 1 && !account_id && !account_ids?.[platform3.toLowerCase()]) {
5308
- const accountList = platformAccounts.map(
5309
- (a) => ` - ${a.id} (${a.username || "unnamed"}${a.has_refresh_token ? ", OAuth 2.0" : ", no refresh token"})`
5310
- ).join("\n");
5614
+ } else if (platformAccounts.length > 1) {
5615
+ const accountList = platformAccounts.map((a) => ` - ${formatAccountChoice(a)}`).join("\n");
5311
5616
  issues.push(
5312
5617
  `${platform3}: Multiple accounts found. Specify account_id or account_ids to choose:
5313
5618
  ${accountList}`
@@ -5462,13 +5767,21 @@ Created with Social Neuron`;
5462
5767
  );
5463
5768
  server.tool(
5464
5769
  "list_connected_accounts",
5465
- "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.",
5770
+ "Check which social platforms have active OAuth connections for posting. Call this before schedule_post to verify credentials. Pass project_id to list the accounts for a specific brand/project, then pass the returned account id as account_id/account_ids when posting. If a platform is missing or expired, the user needs to reconnect at socialneuron.com/settings/connections.",
5466
5771
  {
5772
+ project_id: z3.string().optional().describe(
5773
+ "Brand/project ID to scope connected accounts. Use the same project_id when calling schedule_post."
5774
+ ),
5775
+ include_all: z3.boolean().optional().describe("If true, include expired or inactive accounts as well as usable accounts."),
5467
5776
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
5468
5777
  },
5469
- async ({ response_format }) => {
5778
+ async ({ project_id, include_all, response_format }) => {
5470
5779
  const format = response_format ?? "text";
5471
- const { data: result, error: efError } = await callEdgeFunction("mcp-data", { action: "connected-accounts" });
5780
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
5781
+ action: "connected-accounts",
5782
+ ...project_id ? { projectId: project_id, project_id } : {},
5783
+ ...include_all ? { includeAll: true } : {}
5784
+ });
5472
5785
  if (efError || !result?.success) {
5473
5786
  return {
5474
5787
  content: [
@@ -5501,12 +5814,17 @@ Created with Social Neuron`;
5501
5814
  ]
5502
5815
  };
5503
5816
  }
5504
- const lines = [`${accounts.length} connected account(s):`, ""];
5817
+ const lines = [
5818
+ `${accounts.length} connected account(s)${project_id ? ` for project ${project_id}` : ""}:`,
5819
+ ""
5820
+ ];
5505
5821
  for (const account of accounts) {
5506
5822
  const name = account.username || "(unnamed)";
5507
5823
  const platformLower = account.platform.toLowerCase();
5824
+ const project = account.project_id ? `project_id=${account.project_id}` : "project_id=unassigned";
5825
+ const status = accountEffectiveStatus(account);
5508
5826
  lines.push(
5509
- ` ${platformLower}: ${name} (connected ${account.created_at.split("T")[0]})`
5827
+ ` ${platformLower}: ${name} | id=${account.id} | ${project} | status=${status} (connected ${account.created_at.split("T")[0]})`
5510
5828
  );
5511
5829
  }
5512
5830
  if (format === "json") {
@@ -6714,7 +7032,8 @@ function registerMediaTools(server) {
6714
7032
  },
6715
7033
  async ({ r2_key, response_format }) => {
6716
7034
  const format = response_format ?? "text";
6717
- const { data, error } = await callEdgeFunction("get-signed-url", { r2Key: r2_key, operation: "get" }, { timeoutMs: 1e4 });
7035
+ const normalizedR2Key = r2_key.startsWith("r2://") ? r2_key.slice("r2://".length) : r2_key;
7036
+ const { data, error } = await callEdgeFunction("get-signed-url", { r2Key: normalizedR2Key, operation: "get" }, { timeoutMs: 1e4 });
6718
7037
  const signedDownloadUrl = data?.url ?? data?.signedUrl;
6719
7038
  if (error) {
6720
7039
  return {
@@ -14122,7 +14441,7 @@ function registerCarouselTools(server) {
14122
14441
  "Explicit hook/opener for slide 1. Overrides any hook derived from topic. Keep under 15 words for strongest scroll-stop."
14123
14442
  ),
14124
14443
  hook_family: z28.enum(["curiosity", "authority", "pain_point", "contrarian", "data_driven"]).optional().describe(
14125
- "Hook family tag. Recorded on the carousel so downstream analytics + bandit learners can attribute engagement to hook pattern."
14444
+ "Hook family tag. Recorded on the carousel so downstream analytics can attribute engagement to hook pattern."
14126
14445
  ),
14127
14446
  cta_text: z28.string().max(200).optional().describe("Explicit CTA copy for the final slide. If omitted, derived from topic."),
14128
14447
  cta_url: z28.string().url().optional().describe("URL promoted on the CTA slide. Defaults to the project landing page."),
@@ -14869,7 +15188,10 @@ var init_content_calendar = __esm({
14869
15188
  import { z as z32 } from "zod";
14870
15189
  function findActiveAccount(accounts, platform3) {
14871
15190
  const target = platform3.toLowerCase();
14872
- return accounts.find((a) => a.platform.toLowerCase() === target && a.status === "active") ?? null;
15191
+ return accounts.find((a) => {
15192
+ const effectiveStatus = a.effective_status || a.status;
15193
+ return a.platform.toLowerCase() === target && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
15194
+ }) ?? null;
14873
15195
  }
14874
15196
  function registerConnectionTools(server) {
14875
15197
  server.tool(
@@ -14877,11 +15199,15 @@ function registerConnectionTools(server) {
14877
15199
  "Begin connecting a social platform (Instagram, TikTok, YouTube, etc.). Returns a single-use deep link the user opens in a browser to complete the one-time OAuth handshake on socialneuron.com. This is NOT another OAuth in Claude \u2014 platform connections require a browser session because the social platforms (Meta, Google, TikTok) only accept callbacks on socialneuron.com. After the user clicks the link and approves on the platform, call `wait_for_connection` to detect completion. Link expires in 2 minutes; mint a new one if needed. Use `list_connected_accounts` first to check whether the platform is already connected before calling this.",
14878
15200
  {
14879
15201
  platform: z32.enum(PLATFORM_ENUM4).describe("Platform to connect. Lower-case: instagram, tiktok, youtube, etc."),
15202
+ project_id: z32.string().optional().describe(
15203
+ "Brand/project ID to bind the new social account to. Use this when connecting a brand-specific account."
15204
+ ),
14880
15205
  response_format: z32.enum(["text", "json"]).optional().describe("Response format. Default: text.")
14881
15206
  },
14882
- async ({ platform: platform3, response_format }) => {
15207
+ async ({ platform: platform3, project_id, response_format }) => {
14883
15208
  const format = response_format ?? "text";
14884
- const rl = checkRateLimit("read", `start_platform_connection:${platform3}`);
15209
+ const userId = await getDefaultUserId();
15210
+ const rl = checkRateLimit("read", `start_platform_connection:${userId}`);
14885
15211
  if (!rl.allowed) {
14886
15212
  return {
14887
15213
  content: [
@@ -14893,7 +15219,15 @@ function registerConnectionTools(server) {
14893
15219
  isError: true
14894
15220
  };
14895
15221
  }
14896
- const { data, error } = await callEdgeFunction("mcp-data", { action: "mint-connection-nonce", platform: platform3 }, { timeoutMs: 1e4 });
15222
+ const { data, error } = await callEdgeFunction(
15223
+ "mcp-data",
15224
+ {
15225
+ action: "mint-connection-nonce",
15226
+ platform: platform3,
15227
+ ...project_id ? { projectId: project_id, project_id } : {}
15228
+ },
15229
+ { timeoutMs: 1e4 }
15230
+ );
14897
15231
  if (error || !data?.success || !data.deep_link) {
14898
15232
  const errMsg = error ?? data?.error ?? "Unknown error";
14899
15233
  return {
@@ -14914,6 +15248,7 @@ function registerConnectionTools(server) {
14914
15248
  text: JSON.stringify(
14915
15249
  {
14916
15250
  platform: data.platform,
15251
+ project_id: project_id ?? null,
14917
15252
  deep_link: data.deep_link,
14918
15253
  expires_at: data.expires_at,
14919
15254
  next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection."
@@ -14932,6 +15267,7 @@ function registerConnectionTools(server) {
14932
15267
  type: "text",
14933
15268
  text: [
14934
15269
  `${data.platform} connection ready.`,
15270
+ ...project_id ? [`Brand/project: ${project_id}`] : [],
14935
15271
  "",
14936
15272
  'Ask the user to open this link in a browser and click "Connect" on the platform:',
14937
15273
  ` ${data.deep_link}`,
@@ -14952,17 +15288,21 @@ function registerConnectionTools(server) {
14952
15288
  "Poll until a platform connection becomes active. Use after `start_platform_connection` while the user completes the browser OAuth flow. Returns when the account row appears with status=active, or when the timeout elapses. Default timeout 120s, max 600s.",
14953
15289
  {
14954
15290
  platform: z32.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
15291
+ project_id: z32.string().optional().describe(
15292
+ "Brand/project ID to scope the connection poll. Use the same project_id passed to start_platform_connection."
15293
+ ),
14955
15294
  timeout_s: z32.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
14956
15295
  poll_interval_s: z32.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
14957
15296
  response_format: z32.enum(["text", "json"]).optional().describe("Response format. Default: text.")
14958
15297
  },
14959
- async ({ platform: platform3, timeout_s, poll_interval_s, response_format }) => {
15298
+ async ({ platform: platform3, project_id, timeout_s, poll_interval_s, response_format }) => {
14960
15299
  const format = response_format ?? "text";
14961
15300
  const startedAt = Date.now();
14962
15301
  const timeoutMs = (timeout_s ?? 120) * 1e3;
14963
15302
  const intervalMs = (poll_interval_s ?? 5) * 1e3;
14964
15303
  const deadline = startedAt + timeoutMs;
14965
- const rl = checkRateLimit("read", `wait_for_connection:${platform3}`);
15304
+ const userId = await getDefaultUserId();
15305
+ const rl = checkRateLimit("read", `wait_for_connection:${userId}`);
14966
15306
  if (!rl.allowed) {
14967
15307
  return {
14968
15308
  content: [
@@ -14974,90 +15314,123 @@ function registerConnectionTools(server) {
14974
15314
  isError: true
14975
15315
  };
14976
15316
  }
14977
- let attempts = 0;
14978
- while (Date.now() < deadline) {
14979
- attempts++;
14980
- const { data, error } = await callEdgeFunction("mcp-data", { action: "connected-accounts" }, { timeoutMs: 1e4 });
14981
- if (!error && data?.success) {
14982
- const found = findActiveAccount(data.accounts ?? [], platform3);
14983
- if (found) {
14984
- if (format === "json") {
15317
+ const activeWaits = inFlightWaitsByUser.get(userId) ?? 0;
15318
+ if (activeWaits >= MAX_CONCURRENT_WAITS_PER_USER) {
15319
+ return {
15320
+ content: [
15321
+ {
15322
+ type: "text",
15323
+ text: `Too many concurrent \`wait_for_connection\` calls (max ${MAX_CONCURRENT_WAITS_PER_USER}). Let an existing wait finish, or poll \`list_connected_accounts\` instead.`
15324
+ }
15325
+ ],
15326
+ isError: true
15327
+ };
15328
+ }
15329
+ inFlightWaitsByUser.set(userId, activeWaits + 1);
15330
+ try {
15331
+ let attempts = 0;
15332
+ while (Date.now() < deadline) {
15333
+ attempts++;
15334
+ const { data, error } = await callEdgeFunction(
15335
+ "mcp-data",
15336
+ {
15337
+ action: "connected-accounts",
15338
+ ...project_id ? { projectId: project_id, project_id } : {}
15339
+ },
15340
+ { timeoutMs: 1e4 }
15341
+ );
15342
+ if (!error && data?.success) {
15343
+ const found = findActiveAccount(data.accounts ?? [], platform3);
15344
+ if (found) {
15345
+ if (format === "json") {
15346
+ return {
15347
+ content: [
15348
+ {
15349
+ type: "text",
15350
+ text: JSON.stringify(
15351
+ {
15352
+ connected: true,
15353
+ platform: found.platform,
15354
+ project_id: found.project_id ?? project_id ?? null,
15355
+ account_id: found.id,
15356
+ username: found.username,
15357
+ connected_at: found.created_at,
15358
+ attempts
15359
+ },
15360
+ null,
15361
+ 2
15362
+ )
15363
+ }
15364
+ ],
15365
+ isError: false
15366
+ };
15367
+ }
14985
15368
  return {
14986
15369
  content: [
14987
15370
  {
14988
15371
  type: "text",
14989
- text: JSON.stringify(
14990
- {
14991
- connected: true,
14992
- platform: found.platform,
14993
- account_id: found.id,
14994
- username: found.username,
14995
- connected_at: found.created_at,
14996
- attempts
14997
- },
14998
- null,
14999
- 2
15000
- )
15372
+ text: [
15373
+ `${found.platform} is connected.`,
15374
+ ...found.project_id || project_id ? [`Brand/project: ${found.project_id ?? project_id}`] : [],
15375
+ `Account: ${found.username || "(unnamed)"} (id=${found.id})`,
15376
+ `Detected after ${attempts} poll(s) in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s.`,
15377
+ "Ready to call `schedule_post`."
15378
+ ].join("\n")
15001
15379
  }
15002
15380
  ],
15003
15381
  isError: false
15004
15382
  };
15005
15383
  }
15006
- return {
15007
- content: [
15008
- {
15009
- type: "text",
15010
- text: [
15011
- `${found.platform} is connected.`,
15012
- `Account: ${found.username || "(unnamed)"} (id=${found.id})`,
15013
- `Detected after ${attempts} poll(s) in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s.`,
15014
- "Ready to call `schedule_post`."
15015
- ].join("\n")
15016
- }
15017
- ],
15018
- isError: false
15019
- };
15020
15384
  }
15385
+ const remaining = deadline - Date.now();
15386
+ if (remaining <= 0) break;
15387
+ await new Promise((resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining)));
15388
+ }
15389
+ const message = `${platform3} did not connect within ${timeout_s ?? 120}s (${attempts} polls). The user may not have completed the browser OAuth yet, or the link expired. Mint a new link with \`start_platform_connection\` and try again, or have the user go directly to socialneuron.com/settings/connections.`;
15390
+ if (format === "json") {
15391
+ return {
15392
+ content: [
15393
+ {
15394
+ type: "text",
15395
+ text: JSON.stringify(
15396
+ {
15397
+ connected: false,
15398
+ platform: platform3,
15399
+ project_id: project_id ?? null,
15400
+ attempts,
15401
+ timed_out: true,
15402
+ message
15403
+ },
15404
+ null,
15405
+ 2
15406
+ )
15407
+ }
15408
+ ],
15409
+ isError: true
15410
+ };
15021
15411
  }
15022
- const remaining = deadline - Date.now();
15023
- if (remaining <= 0) break;
15024
- await new Promise((resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining)));
15025
- }
15026
- const message = `${platform3} did not connect within ${timeout_s ?? 120}s (${attempts} polls). The user may not have completed the browser OAuth yet, or the link expired. Mint a new link with \`start_platform_connection\` and try again, or have the user go directly to socialneuron.com/settings/connections.`;
15027
- if (format === "json") {
15028
15412
  return {
15029
- content: [
15030
- {
15031
- type: "text",
15032
- text: JSON.stringify(
15033
- {
15034
- connected: false,
15035
- platform: platform3,
15036
- attempts,
15037
- timed_out: true,
15038
- message
15039
- },
15040
- null,
15041
- 2
15042
- )
15043
- }
15044
- ],
15413
+ content: [{ type: "text", text: message }],
15045
15414
  isError: true
15046
15415
  };
15416
+ } finally {
15417
+ const remainingWaits = (inFlightWaitsByUser.get(userId) ?? 1) - 1;
15418
+ if (remainingWaits <= 0) {
15419
+ inFlightWaitsByUser.delete(userId);
15420
+ } else {
15421
+ inFlightWaitsByUser.set(userId, remainingWaits);
15422
+ }
15047
15423
  }
15048
- return {
15049
- content: [{ type: "text", text: message }],
15050
- isError: true
15051
- };
15052
15424
  }
15053
15425
  );
15054
15426
  }
15055
- var PLATFORM_ENUM4;
15427
+ var PLATFORM_ENUM4, MAX_CONCURRENT_WAITS_PER_USER, inFlightWaitsByUser;
15056
15428
  var init_connections = __esm({
15057
15429
  "src/tools/connections.ts"() {
15058
15430
  "use strict";
15059
15431
  init_edge_function();
15060
15432
  init_rate_limit();
15433
+ init_supabase();
15061
15434
  PLATFORM_ENUM4 = [
15062
15435
  "youtube",
15063
15436
  "tiktok",
@@ -15070,6 +15443,8 @@ var init_connections = __esm({
15070
15443
  "shopify",
15071
15444
  "etsy"
15072
15445
  ];
15446
+ MAX_CONCURRENT_WAITS_PER_USER = 3;
15447
+ inFlightWaitsByUser = /* @__PURE__ */ new Map();
15073
15448
  }
15074
15449
  });
15075
15450
 
@@ -15934,7 +16309,12 @@ function applyScopeEnforcement(server, scopeResolver) {
15934
16309
  toolName: name,
15935
16310
  status,
15936
16311
  durationMs: Date.now() - startedAt,
15937
- details: { source: "wrapper" }
16312
+ details: {
16313
+ source: "wrapper",
16314
+ // Classify failures so the tool-error rate is diagnosable
16315
+ // (validation vs permission vs billing vs upstream vs server).
16316
+ ...status === "error" ? { error_type: classifyToolError(result) } : {}
16317
+ }
15938
16318
  });
15939
16319
  return truncateResponse(result);
15940
16320
  } catch (err) {
@@ -15944,6 +16324,8 @@ function applyScopeEnforcement(server, scopeResolver) {
15944
16324
  durationMs: Date.now() - startedAt,
15945
16325
  details: {
15946
16326
  source: "wrapper",
16327
+ // A thrown exception escaped the handler — an unclassified fault.
16328
+ error_type: "server_error",
15947
16329
  exception: err instanceof Error ? err.message.slice(0, 200) : "unknown"
15948
16330
  }
15949
16331
  });
@@ -16037,6 +16419,7 @@ function truncateResponse(result) {
16037
16419
  return { ...result, content: truncated };
16038
16420
  }
16039
16421
  function registerAllTools(server, options) {
16422
+ applyToolProfile(server, options?.toolProfile ?? "full");
16040
16423
  registerIdeationTools(server);
16041
16424
  registerContentTools(server);
16042
16425
  registerDistributionTools(server);
@@ -16089,6 +16472,7 @@ var init_register_tools = __esm({
16089
16472
  init_supabase();
16090
16473
  init_www_authenticate();
16091
16474
  init_tool_error();
16475
+ init_tool_profile();
16092
16476
  init_scanner();
16093
16477
  init_ideation();
16094
16478
  init_content2();
@@ -16173,13 +16557,13 @@ async function invokeToolRest(name, args) {
16173
16557
  }
16174
16558
  function extractRestError(result) {
16175
16559
  const sc = result.structuredContent?.error;
16176
- if (sc?.error_type && KNOWN_ERROR_TYPES.has(sc.error_type)) {
16560
+ if (sc?.error_type && KNOWN_ERROR_TYPES2.has(sc.error_type)) {
16177
16561
  return { error_type: sc.error_type, message: sc.message ?? "Tool error." };
16178
16562
  }
16179
16563
  const text = result.content?.find((c) => c.type === "text")?.text ?? "";
16180
16564
  try {
16181
16565
  const parsed = JSON.parse(text);
16182
- if (parsed.error_type && KNOWN_ERROR_TYPES.has(parsed.error_type)) {
16566
+ if (parsed.error_type && KNOWN_ERROR_TYPES2.has(parsed.error_type)) {
16183
16567
  return { error_type: parsed.error_type, message: parsed.message ?? text };
16184
16568
  }
16185
16569
  } catch {
@@ -16189,7 +16573,7 @@ function extractRestError(result) {
16189
16573
  }
16190
16574
  return { error_type: "server_error", message: text || "Tool error." };
16191
16575
  }
16192
- var cachedCallHandler, KNOWN_ERROR_TYPES;
16576
+ var cachedCallHandler, KNOWN_ERROR_TYPES2;
16193
16577
  var init_rest_invoke = __esm({
16194
16578
  "src/lib/rest-invoke.ts"() {
16195
16579
  "use strict";
@@ -16197,7 +16581,7 @@ var init_rest_invoke = __esm({
16197
16581
  init_request_context();
16198
16582
  init_version();
16199
16583
  init_tool_catalog();
16200
- KNOWN_ERROR_TYPES = /* @__PURE__ */ new Set([
16584
+ KNOWN_ERROR_TYPES2 = /* @__PURE__ */ new Set([
16201
16585
  "policy_block",
16202
16586
  "validation_error",
16203
16587
  "permission_denied",
@@ -16274,7 +16658,9 @@ async function handleCall(args, asJson) {
16274
16658
  scopes: auth.scopes ?? [],
16275
16659
  token: apiKey,
16276
16660
  creditsUsed: 0,
16277
- assetsGenerated: 0
16661
+ assetsGenerated: 0,
16662
+ surface: "cli",
16663
+ projectId: auth.projectId ?? null
16278
16664
  },
16279
16665
  () => invokeToolRest(toolName, toolArgs)
16280
16666
  );