@socialneuron/mcp-server 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  All notable changes to `@socialneuron/mcp-server` will be documented in this file.
4
4
 
5
+ ## [1.8.1] - 2026-07-14
6
+
7
+ ### Fixed
8
+
9
+ - **`create_storyboard` empty responses.** The tool now accepts the AI service's canonical `text` response field as well as the legacy `content` field, and reports a real tool error when neither contains a storyboard.
10
+ - **Project isolation across the creative workflow.** `generate_image`, `create_storyboard`, `generate_voiceover`, and `render_hyperframes` now accept `project_id`. Analytics refresh, analytics reads, performance insights, and best-time recommendations are also project-scoped and default to the active project context.
11
+ - **Autopilot run visibility.** `get_autopilot_status` now returns recent runs supplied by the backend instead of replacing them with an empty array.
12
+ - **HyperFrames capability copy.** Removed the obsolete Phase-2/runtime-not-installed warning now that live HyperFrames renders complete successfully, and corrected image/storyboard/voiceover credit descriptions to match the client budget checks.
13
+
5
14
  ## [1.8.0] - 2026-07-14
6
15
 
7
16
  ### Changed
package/dist/http.js CHANGED
@@ -1997,7 +1997,7 @@ function checkRateLimit(category, key) {
1997
1997
  init_supabase();
1998
1998
 
1999
1999
  // src/lib/version.ts
2000
- var MCP_VERSION = "1.8.0";
2000
+ var MCP_VERSION = "1.8.1";
2001
2001
 
2002
2002
  // src/tools/ideation.ts
2003
2003
  function asEnvelope(data) {
@@ -2717,7 +2717,7 @@ function registerContentTools(server) {
2717
2717
  );
2718
2718
  server.tool(
2719
2719
  "generate_image",
2720
- "Start an async AI image generation job \u2014 returns a job_id immediately. Poll with check_status every 5-15s until complete. Costs 2-10 credits depending on model. Use for social media posts, carousel slides, or as input to generate_video (image-to-video).",
2720
+ "Start an async AI image generation job \u2014 returns a job_id immediately. Poll with check_status every 5-15s until complete. Costs 15-50 credits depending on model. Use for social media posts, carousel slides, or as input to generate_video (image-to-video). Pass project_id so the asset is stored with the correct brand/project.",
2721
2721
  {
2722
2722
  prompt: z2.string().max(2e3).describe(
2723
2723
  "Text prompt describing the image to generate. Be specific about style, composition, colors, lighting, and subject matter."
@@ -2739,9 +2739,10 @@ function registerContentTools(server) {
2739
2739
  image_url: z2.string().optional().describe(
2740
2740
  "Reference image URL for image-to-image generation. Required for ideogram model. Optional for others."
2741
2741
  ),
2742
+ project_id: z2.string().optional().describe("Project ID to associate the generated image with."),
2742
2743
  response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
2743
2744
  },
2744
- async ({ prompt, model, aspect_ratio, image_url, response_format }) => {
2745
+ async ({ prompt, model, aspect_ratio, image_url, project_id, response_format }) => {
2745
2746
  const format = response_format ?? "text";
2746
2747
  const userId = await getDefaultUserId();
2747
2748
  const assetBudget = checkAssetBudget();
@@ -2777,7 +2778,8 @@ function registerContentTools(server) {
2777
2778
  prompt,
2778
2779
  model,
2779
2780
  aspectRatio: aspect_ratio ?? "1:1",
2780
- imageUrl: image_url
2781
+ imageUrl: image_url,
2782
+ ...project_id && { projectId: project_id }
2781
2783
  },
2782
2784
  { timeoutMs: 3e4 }
2783
2785
  );
@@ -2816,7 +2818,8 @@ function registerContentTools(server) {
2816
2818
  jobId,
2817
2819
  taskId: data.taskId,
2818
2820
  asyncJobId: data.asyncJobId,
2819
- model: data.model
2821
+ model: data.model,
2822
+ projectId: project_id ?? null
2820
2823
  }),
2821
2824
  null,
2822
2825
  2
@@ -2997,7 +3000,7 @@ function registerContentTools(server) {
2997
3000
  );
2998
3001
  server.tool(
2999
3002
  "create_storyboard",
3000
- "Plan a multi-scene video storyboard with AI-generated prompts, durations, captions, and voiceover text per frame. Use before generate_video or generate_image to create cohesive multi-shot content. Include brand_context from get_brand_profile for consistent visual branding across frames.",
3003
+ "Plan a multi-scene video storyboard with AI-generated prompts, durations, captions, and voiceover text per frame. Use before generate_video or generate_image to create cohesive multi-shot content. Include brand_context from get_brand_profile and project_id for consistent, project-scoped production. Costs 10 credits.",
3001
3004
  {
3002
3005
  concept: z2.string().max(2e3).describe(
3003
3006
  'The video concept/idea. Include: hook, key messages, target audience, and desired outcome (e.g., "TikTok ad for VPN app targeting privacy-conscious millennials, hook with shocking stat about data leaks").'
@@ -3021,6 +3024,7 @@ function registerContentTools(server) {
3021
3024
  style: z2.string().optional().describe(
3022
3025
  'Visual style direction (e.g., "cinematic", "anime", "documentary", "motion graphics").'
3023
3026
  ),
3027
+ project_id: z2.string().optional().describe("Project ID for brand-scoped generation and attribution."),
3024
3028
  response_format: z2.enum(["text", "json"]).optional().describe(
3025
3029
  "Response format. Defaults to json for structured storyboard data."
3026
3030
  )
@@ -3032,6 +3036,7 @@ function registerContentTools(server) {
3032
3036
  target_duration,
3033
3037
  num_scenes,
3034
3038
  style,
3039
+ project_id,
3035
3040
  response_format
3036
3041
  }) => {
3037
3042
  const format = response_format ?? "json";
@@ -3114,7 +3119,8 @@ Return ONLY valid JSON in this exact format:
3114
3119
  prompt: storyboardPrompt,
3115
3120
  type: "storyboard",
3116
3121
  model: "gemini-2.5-flash",
3117
- responseFormat: "json"
3122
+ responseFormat: "json",
3123
+ ...project_id && { projectId: project_id }
3118
3124
  },
3119
3125
  { timeoutMs: 6e4 }
3120
3126
  );
@@ -3129,7 +3135,18 @@ Return ONLY valid JSON in this exact format:
3129
3135
  isError: true
3130
3136
  };
3131
3137
  }
3132
- const rawContent = data?.content ?? "";
3138
+ const rawContent = data?.content?.trim() || data?.text?.trim() || "";
3139
+ if (!rawContent) {
3140
+ return {
3141
+ content: [
3142
+ {
3143
+ type: "text",
3144
+ text: "Storyboard generation failed: the AI service returned an empty response."
3145
+ }
3146
+ ],
3147
+ isError: true
3148
+ };
3149
+ }
3133
3150
  addCreditsUsed(estimatedCost);
3134
3151
  if (format === "json") {
3135
3152
  try {
@@ -3165,16 +3182,17 @@ Return ONLY valid JSON in this exact format:
3165
3182
  );
3166
3183
  server.tool(
3167
3184
  "generate_voiceover",
3168
- "Generate a voiceover audio file for video narration. Returns an R2-hosted audio URL. Use after create_storyboard to add narration to each scene, or standalone for podcast intros and ad reads. Costs ~2 credits per generation.",
3185
+ "Generate a voiceover audio file for video narration. Returns an R2-hosted audio URL. Use after create_storyboard to add narration to each scene, or standalone for podcast intros and ad reads. Pass project_id to keep the asset with the correct brand/project. Costs 15 credits per generation.",
3169
3186
  {
3170
3187
  text: z2.string().max(5e3).describe("The script/text to convert to speech."),
3171
3188
  voice: z2.enum(["rachel", "domi"]).optional().describe(
3172
3189
  "Voice selection. rachel=warm female, domi=confident female. Defaults to rachel."
3173
3190
  ),
3174
3191
  speed: z2.number().min(0.5).max(2).optional().describe("Speech speed multiplier. 1.0 is normal. Defaults to 1.0."),
3192
+ project_id: z2.string().optional().describe("Project ID to associate the generated voiceover with."),
3175
3193
  response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
3176
3194
  },
3177
- async ({ text, voice, speed, response_format }) => {
3195
+ async ({ text, voice, speed, project_id, response_format }) => {
3178
3196
  const format = response_format ?? "text";
3179
3197
  const userId = await getDefaultUserId();
3180
3198
  const estimatedCost = 15;
@@ -3209,7 +3227,8 @@ Return ONLY valid JSON in this exact format:
3209
3227
  {
3210
3228
  text,
3211
3229
  voiceId: ELEVENLABS_VOICE_IDS[voice ?? "rachel"],
3212
- speed: speed ?? 1
3230
+ speed: speed ?? 1,
3231
+ ...project_id && { projectId: project_id }
3213
3232
  },
3214
3233
  { timeoutMs: 6e4 }
3215
3234
  );
@@ -3245,7 +3264,8 @@ Return ONLY valid JSON in this exact format:
3245
3264
  asEnvelope2({
3246
3265
  audioUrl: data.audioUrl,
3247
3266
  durationSeconds: data.durationSeconds,
3248
- voice: voice ?? "rachel"
3267
+ voice: voice ?? "rachel",
3268
+ projectId: project_id ?? null
3249
3269
  }),
3250
3270
  null,
3251
3271
  2
@@ -5713,7 +5733,7 @@ function asEnvelope4(data) {
5713
5733
  function registerAnalyticsTools(server) {
5714
5734
  server.tool(
5715
5735
  "fetch_analytics",
5716
- "Get post performance metrics \u2014 views, likes, comments, shares, and engagement rate. Filter by platform, time range (default 30 days), or specific content_id. Call refresh_platform_analytics first if data seems stale. Results sorted by most recent capture.",
5736
+ "Get project-scoped post performance metrics \u2014 views, likes, comments, shares, and engagement rate. Filter by platform, time range (default 30 days), or specific content_id. Call refresh_platform_analytics first if data seems stale. Results sorted by most recent capture.",
5717
5737
  {
5718
5738
  platform: z5.enum([
5719
5739
  "youtube",
@@ -5731,19 +5751,22 @@ function registerAnalyticsTools(server) {
5731
5751
  content_id: z5.string().uuid().optional().describe(
5732
5752
  "Filter to a specific content_history ID to see performance of one piece of content."
5733
5753
  ),
5754
+ project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
5734
5755
  limit: z5.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
5735
5756
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
5736
5757
  },
5737
- async ({ platform: platform2, days, content_id, limit, response_format }) => {
5758
+ async ({ platform: platform2, days, content_id, project_id, limit, response_format }) => {
5738
5759
  const format = response_format ?? "text";
5739
5760
  const lookbackDays = days ?? 30;
5740
5761
  const maxPosts = limit ?? 20;
5762
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
5741
5763
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
5742
5764
  action: "analytics",
5743
5765
  platform: platform2,
5744
5766
  days: lookbackDays,
5745
5767
  limit: maxPosts,
5746
- contentId: content_id
5768
+ contentId: content_id,
5769
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
5747
5770
  });
5748
5771
  if (efError) {
5749
5772
  return {
@@ -5813,14 +5836,19 @@ function registerAnalyticsTools(server) {
5813
5836
  );
5814
5837
  server.tool(
5815
5838
  "refresh_platform_analytics",
5816
- "Queue analytics refresh jobs for all posts from the last 7 days across connected platforms. Call this before fetch_analytics if you need fresh data. Returns immediately \u2014 data updates asynchronously over the next 1-5 minutes.",
5839
+ "Queue analytics refresh jobs for posts from the last 7 days in one project across its connected platforms. Call this before fetch_analytics if you need fresh data. Returns immediately \u2014 data updates asynchronously over the next 1-5 minutes.",
5817
5840
  {
5841
+ project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
5818
5842
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
5819
5843
  },
5820
- async ({ response_format }) => {
5844
+ async ({ project_id, response_format }) => {
5821
5845
  const format = response_format ?? "text";
5822
5846
  const userId = await getDefaultUserId();
5823
- const rateLimit = checkRateLimit("posting", `refresh_platform_analytics:${userId}`);
5847
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
5848
+ const rateLimit = checkRateLimit(
5849
+ "posting",
5850
+ `refresh_platform_analytics:${userId}:${resolvedProjectId ?? "default"}`
5851
+ );
5824
5852
  if (!rateLimit.allowed) {
5825
5853
  return {
5826
5854
  content: [
@@ -5832,7 +5860,10 @@ function registerAnalyticsTools(server) {
5832
5860
  isError: true
5833
5861
  };
5834
5862
  }
5835
- const { data, error } = await callEdgeFunction("fetch-analytics", { userId });
5863
+ const { data, error } = await callEdgeFunction("fetch-analytics", {
5864
+ userId,
5865
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
5866
+ });
5836
5867
  if (error) {
5837
5868
  return {
5838
5869
  content: [{ type: "text", text: `Error refreshing analytics: ${error}` }],
@@ -5861,7 +5892,8 @@ function registerAnalyticsTools(server) {
5861
5892
  success: true,
5862
5893
  postsProcessed: result.postsProcessed,
5863
5894
  queued,
5864
- errored
5895
+ errored,
5896
+ projectId: resolvedProjectId ?? null
5865
5897
  });
5866
5898
  return {
5867
5899
  structuredContent,
@@ -6913,6 +6945,7 @@ function registerRemotionTools(server) {
6913
6945
 
6914
6946
  // src/tools/insights.ts
6915
6947
  import { z as z9 } from "zod";
6948
+ init_supabase();
6916
6949
  var MAX_INSIGHT_AGE_DAYS = 30;
6917
6950
  var PLATFORM_ENUM = [
6918
6951
  "youtube",
@@ -6937,22 +6970,25 @@ function asEnvelope6(data) {
6937
6970
  function registerInsightsTools(server) {
6938
6971
  server.tool(
6939
6972
  "get_performance_insights",
6940
- "Query performance insights derived from post analytics. Returns metrics like engagement rate, view velocity, and click rate aggregated over time. Use this to understand what content is performing well.",
6973
+ "Query project-scoped performance insights derived from post analytics. Returns metrics like engagement rate, view velocity, and click rate aggregated over time. Use this to understand what content is performing well.",
6941
6974
  {
6942
6975
  insight_type: z9.enum(["top_hooks", "optimal_timing", "best_models", "competitor_patterns"]).optional().describe("Filter to a specific insight type."),
6943
6976
  days: z9.number().min(1).max(90).optional().describe("Number of days to look back. Defaults to 30. Max 90."),
6944
6977
  limit: z9.number().min(1).max(50).optional().describe("Maximum number of insights to return. Defaults to 10."),
6978
+ project_id: z9.string().optional().describe("Project ID. Defaults to the active project context."),
6945
6979
  response_format: z9.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6946
6980
  },
6947
- async ({ insight_type, days, limit, response_format }) => {
6981
+ async ({ insight_type, days, limit, project_id, response_format }) => {
6948
6982
  const format = response_format ?? "text";
6949
6983
  const lookbackDays = days ?? 30;
6950
6984
  const maxRows = limit ?? 10;
6951
6985
  const effectiveDays = Math.min(lookbackDays, MAX_INSIGHT_AGE_DAYS);
6986
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
6952
6987
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
6953
6988
  action: "performance-insights",
6954
6989
  days: effectiveDays,
6955
- limit: maxRows
6990
+ limit: maxRows,
6991
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
6956
6992
  });
6957
6993
  if (efError || !result?.success) {
6958
6994
  return {
@@ -7039,19 +7075,22 @@ function registerInsightsTools(server) {
7039
7075
  );
7040
7076
  server.tool(
7041
7077
  "get_best_posting_times",
7042
- "Analyze post analytics data to find the best times to post for maximum engagement. Returns the top 5 time slots (day of week + hour) ranked by average engagement.",
7078
+ "Analyze project-scoped post analytics data to find the best times to post for maximum engagement. Returns the top 5 time slots (day of week + hour) ranked by average engagement.",
7043
7079
  {
7044
7080
  platform: z9.enum(PLATFORM_ENUM).optional().describe("Filter to a specific platform."),
7045
7081
  days: z9.number().min(1).max(90).optional().describe("Number of days to analyze. Defaults to 30. Max 90."),
7082
+ project_id: z9.string().optional().describe("Project ID. Defaults to the active project context."),
7046
7083
  response_format: z9.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7047
7084
  },
7048
- async ({ platform: platform2, days, response_format }) => {
7085
+ async ({ platform: platform2, days, project_id, response_format }) => {
7049
7086
  const format = response_format ?? "text";
7050
7087
  const lookbackDays = days ?? 30;
7088
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
7051
7089
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
7052
7090
  action: "best-posting-times",
7053
7091
  days: lookbackDays,
7054
- platform: platform2 ?? void 0
7092
+ platform: platform2 ?? void 0,
7093
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
7055
7094
  });
7056
7095
  if (efError || !result?.success) {
7057
7096
  return {
@@ -8144,7 +8183,7 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
8144
8183
  }
8145
8184
  const statusData = {
8146
8185
  activeConfigs: result?.activeConfigs ?? 0,
8147
- recentRuns: [],
8186
+ recentRuns: result?.recentRuns ?? result?.recent_runs ?? [],
8148
8187
  pendingApprovals: result?.pendingApprovals ?? 0
8149
8188
  };
8150
8189
  if (format === "json") {
@@ -8166,8 +8205,20 @@ ${"=".repeat(40)}
8166
8205
  text += `Pending Approvals: ${statusData.pendingApprovals}
8167
8206
 
8168
8207
  `;
8169
- text += `No recent runs.
8208
+ if (statusData.recentRuns.length === 0) {
8209
+ text += `No recent runs.
8170
8210
  `;
8211
+ } else {
8212
+ text += `Recent Runs (${statusData.recentRuns.length}):
8213
+ `;
8214
+ for (const run of statusData.recentRuns.slice(0, 10)) {
8215
+ const id = String(run.id ?? run.run_id ?? "unknown");
8216
+ const status = String(run.status ?? "unknown");
8217
+ const credits = run.credits_used ?? run.creditsUsed;
8218
+ text += ` ${id}: ${status}${credits == null ? "" : ` (${String(credits)} credits)`}
8219
+ `;
8220
+ }
8221
+ }
8171
8222
  return {
8172
8223
  content: [{ type: "text", text }]
8173
8224
  };
@@ -13297,7 +13348,7 @@ function registerHyperframesTools(server) {
13297
13348
  );
13298
13349
  server.tool(
13299
13350
  "render_hyperframes",
13300
- "Render an HTML video composition (Hyperframes) to MP4. Author the composition as HTML with data-* timing attributes and GSAP timelines \u2014 frame-accurate, no React build step. Use list_hyperframes_blocks to see the pre-built block catalog. Returns a job ID \u2014 poll with check_status. Note: F4 v1 ships the EF + scaffold; the worker container needs the hyperframes runtime installed (Phase 2) before this returns finished MP4s.",
13351
+ "Render an HTML video composition (Hyperframes) to MP4. Author the composition as HTML with data-* timing attributes and GSAP timelines \u2014 frame-accurate, no React build step. Use list_hyperframes_blocks to see the pre-built block catalog. Pass project_id to keep the render with the correct brand/project. Returns a job ID \u2014 poll with check_status.",
13301
13352
  {
13302
13353
  composition_html: z30.string().max(5e5).optional().describe(
13303
13354
  "Inline HTML composition (full <html>...</html>). Max 500KB. Use composition_url for larger."
@@ -13309,7 +13360,8 @@ function registerHyperframesTools(server) {
13309
13360
  aspect_ratio: z30.enum(["9:16", "16:9", "1:1"]).optional().describe('Output aspect ratio. Default "9:16".'),
13310
13361
  duration_sec: z30.number().min(0.1).max(600).describe("Output duration in seconds. Required. Max 600 (10 min)."),
13311
13362
  fps: z30.union([z30.literal(24), z30.literal(30), z30.literal(60)]).optional().describe("Frames per second. Default 30."),
13312
- quality: z30.enum(["draft", "standard", "high"]).optional().describe("draft: fast iteration; standard: production; high: final master.")
13363
+ quality: z30.enum(["draft", "standard", "high"]).optional().describe("draft: fast iteration; standard: production; high: final master."),
13364
+ project_id: z30.string().optional().describe("Project ID to associate the Hyperframes render with.")
13313
13365
  },
13314
13366
  async ({
13315
13367
  composition_html,
@@ -13318,7 +13370,8 @@ function registerHyperframesTools(server) {
13318
13370
  aspect_ratio,
13319
13371
  duration_sec,
13320
13372
  fps,
13321
- quality
13373
+ quality,
13374
+ project_id
13322
13375
  }) => {
13323
13376
  const userId = await getDefaultUserId();
13324
13377
  const rateLimit = checkRateLimit("generation", `render_hyperframes:${userId}`);
@@ -13363,7 +13416,8 @@ function registerHyperframesTools(server) {
13363
13416
  aspectRatio: aspect_ratio || "9:16",
13364
13417
  durationSec: duration_sec,
13365
13418
  fps: fps || 30,
13366
- quality: quality || "standard"
13419
+ quality: quality || "standard",
13420
+ ...project_id && { projectId: project_id }
13367
13421
  });
13368
13422
  if (error || !data?.jobId) {
13369
13423
  throw new Error(error || data?.error || "Failed to create Hyperframes render job");
@@ -13379,7 +13433,7 @@ function registerHyperframesTools(server) {
13379
13433
  ` Duration: ${duration_sec}s @ ${fps || 30}fps (${aspect_ratio || "9:16"})`,
13380
13434
  ` Quality: ${quality || "standard"}`,
13381
13435
  ``,
13382
- `Poll with check_status. Note: until F4 Phase 2 wires the runtime, the job will fail with a clear "HyperframesNotInstalled" message.`
13436
+ `Poll with check_status.`
13383
13437
  ].join("\n")
13384
13438
  }
13385
13439
  ]
package/dist/index.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.8.0";
22
+ MCP_VERSION = "1.8.1";
23
23
  }
24
24
  });
25
25
 
@@ -2775,7 +2775,7 @@ function registerContentTools(server2) {
2775
2775
  );
2776
2776
  server2.tool(
2777
2777
  "generate_image",
2778
- "Start an async AI image generation job \u2014 returns a job_id immediately. Poll with check_status every 5-15s until complete. Costs 2-10 credits depending on model. Use for social media posts, carousel slides, or as input to generate_video (image-to-video).",
2778
+ "Start an async AI image generation job \u2014 returns a job_id immediately. Poll with check_status every 5-15s until complete. Costs 15-50 credits depending on model. Use for social media posts, carousel slides, or as input to generate_video (image-to-video). Pass project_id so the asset is stored with the correct brand/project.",
2779
2779
  {
2780
2780
  prompt: z2.string().max(2e3).describe(
2781
2781
  "Text prompt describing the image to generate. Be specific about style, composition, colors, lighting, and subject matter."
@@ -2797,9 +2797,10 @@ function registerContentTools(server2) {
2797
2797
  image_url: z2.string().optional().describe(
2798
2798
  "Reference image URL for image-to-image generation. Required for ideogram model. Optional for others."
2799
2799
  ),
2800
+ project_id: z2.string().optional().describe("Project ID to associate the generated image with."),
2800
2801
  response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
2801
2802
  },
2802
- async ({ prompt: prompt2, model, aspect_ratio, image_url, response_format }) => {
2803
+ async ({ prompt: prompt2, model, aspect_ratio, image_url, project_id, response_format }) => {
2803
2804
  const format = response_format ?? "text";
2804
2805
  const userId = await getDefaultUserId();
2805
2806
  const assetBudget = checkAssetBudget();
@@ -2835,7 +2836,8 @@ function registerContentTools(server2) {
2835
2836
  prompt: prompt2,
2836
2837
  model,
2837
2838
  aspectRatio: aspect_ratio ?? "1:1",
2838
- imageUrl: image_url
2839
+ imageUrl: image_url,
2840
+ ...project_id && { projectId: project_id }
2839
2841
  },
2840
2842
  { timeoutMs: 3e4 }
2841
2843
  );
@@ -2874,7 +2876,8 @@ function registerContentTools(server2) {
2874
2876
  jobId,
2875
2877
  taskId: data.taskId,
2876
2878
  asyncJobId: data.asyncJobId,
2877
- model: data.model
2879
+ model: data.model,
2880
+ projectId: project_id ?? null
2878
2881
  }),
2879
2882
  null,
2880
2883
  2
@@ -3055,7 +3058,7 @@ function registerContentTools(server2) {
3055
3058
  );
3056
3059
  server2.tool(
3057
3060
  "create_storyboard",
3058
- "Plan a multi-scene video storyboard with AI-generated prompts, durations, captions, and voiceover text per frame. Use before generate_video or generate_image to create cohesive multi-shot content. Include brand_context from get_brand_profile for consistent visual branding across frames.",
3061
+ "Plan a multi-scene video storyboard with AI-generated prompts, durations, captions, and voiceover text per frame. Use before generate_video or generate_image to create cohesive multi-shot content. Include brand_context from get_brand_profile and project_id for consistent, project-scoped production. Costs 10 credits.",
3059
3062
  {
3060
3063
  concept: z2.string().max(2e3).describe(
3061
3064
  'The video concept/idea. Include: hook, key messages, target audience, and desired outcome (e.g., "TikTok ad for VPN app targeting privacy-conscious millennials, hook with shocking stat about data leaks").'
@@ -3079,6 +3082,7 @@ function registerContentTools(server2) {
3079
3082
  style: z2.string().optional().describe(
3080
3083
  'Visual style direction (e.g., "cinematic", "anime", "documentary", "motion graphics").'
3081
3084
  ),
3085
+ project_id: z2.string().optional().describe("Project ID for brand-scoped generation and attribution."),
3082
3086
  response_format: z2.enum(["text", "json"]).optional().describe(
3083
3087
  "Response format. Defaults to json for structured storyboard data."
3084
3088
  )
@@ -3090,6 +3094,7 @@ function registerContentTools(server2) {
3090
3094
  target_duration,
3091
3095
  num_scenes,
3092
3096
  style,
3097
+ project_id,
3093
3098
  response_format
3094
3099
  }) => {
3095
3100
  const format = response_format ?? "json";
@@ -3172,7 +3177,8 @@ Return ONLY valid JSON in this exact format:
3172
3177
  prompt: storyboardPrompt,
3173
3178
  type: "storyboard",
3174
3179
  model: "gemini-2.5-flash",
3175
- responseFormat: "json"
3180
+ responseFormat: "json",
3181
+ ...project_id && { projectId: project_id }
3176
3182
  },
3177
3183
  { timeoutMs: 6e4 }
3178
3184
  );
@@ -3187,7 +3193,18 @@ Return ONLY valid JSON in this exact format:
3187
3193
  isError: true
3188
3194
  };
3189
3195
  }
3190
- const rawContent = data?.content ?? "";
3196
+ const rawContent = data?.content?.trim() || data?.text?.trim() || "";
3197
+ if (!rawContent) {
3198
+ return {
3199
+ content: [
3200
+ {
3201
+ type: "text",
3202
+ text: "Storyboard generation failed: the AI service returned an empty response."
3203
+ }
3204
+ ],
3205
+ isError: true
3206
+ };
3207
+ }
3191
3208
  addCreditsUsed(estimatedCost);
3192
3209
  if (format === "json") {
3193
3210
  try {
@@ -3223,16 +3240,17 @@ Return ONLY valid JSON in this exact format:
3223
3240
  );
3224
3241
  server2.tool(
3225
3242
  "generate_voiceover",
3226
- "Generate a voiceover audio file for video narration. Returns an R2-hosted audio URL. Use after create_storyboard to add narration to each scene, or standalone for podcast intros and ad reads. Costs ~2 credits per generation.",
3243
+ "Generate a voiceover audio file for video narration. Returns an R2-hosted audio URL. Use after create_storyboard to add narration to each scene, or standalone for podcast intros and ad reads. Pass project_id to keep the asset with the correct brand/project. Costs 15 credits per generation.",
3227
3244
  {
3228
3245
  text: z2.string().max(5e3).describe("The script/text to convert to speech."),
3229
3246
  voice: z2.enum(["rachel", "domi"]).optional().describe(
3230
3247
  "Voice selection. rachel=warm female, domi=confident female. Defaults to rachel."
3231
3248
  ),
3232
3249
  speed: z2.number().min(0.5).max(2).optional().describe("Speech speed multiplier. 1.0 is normal. Defaults to 1.0."),
3250
+ project_id: z2.string().optional().describe("Project ID to associate the generated voiceover with."),
3233
3251
  response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
3234
3252
  },
3235
- async ({ text, voice, speed, response_format }) => {
3253
+ async ({ text, voice, speed, project_id, response_format }) => {
3236
3254
  const format = response_format ?? "text";
3237
3255
  const userId = await getDefaultUserId();
3238
3256
  const estimatedCost = 15;
@@ -3267,7 +3285,8 @@ Return ONLY valid JSON in this exact format:
3267
3285
  {
3268
3286
  text,
3269
3287
  voiceId: ELEVENLABS_VOICE_IDS[voice ?? "rachel"],
3270
- speed: speed ?? 1
3288
+ speed: speed ?? 1,
3289
+ ...project_id && { projectId: project_id }
3271
3290
  },
3272
3291
  { timeoutMs: 6e4 }
3273
3292
  );
@@ -3303,7 +3322,8 @@ Return ONLY valid JSON in this exact format:
3303
3322
  asEnvelope2({
3304
3323
  audioUrl: data.audioUrl,
3305
3324
  durationSeconds: data.durationSeconds,
3306
- voice: voice ?? "rachel"
3325
+ voice: voice ?? "rachel",
3326
+ projectId: project_id ?? null
3307
3327
  }),
3308
3328
  null,
3309
3329
  2
@@ -5856,7 +5876,7 @@ function asEnvelope4(data) {
5856
5876
  function registerAnalyticsTools(server2) {
5857
5877
  server2.tool(
5858
5878
  "fetch_analytics",
5859
- "Get post performance metrics \u2014 views, likes, comments, shares, and engagement rate. Filter by platform, time range (default 30 days), or specific content_id. Call refresh_platform_analytics first if data seems stale. Results sorted by most recent capture.",
5879
+ "Get project-scoped post performance metrics \u2014 views, likes, comments, shares, and engagement rate. Filter by platform, time range (default 30 days), or specific content_id. Call refresh_platform_analytics first if data seems stale. Results sorted by most recent capture.",
5860
5880
  {
5861
5881
  platform: z5.enum([
5862
5882
  "youtube",
@@ -5874,19 +5894,22 @@ function registerAnalyticsTools(server2) {
5874
5894
  content_id: z5.string().uuid().optional().describe(
5875
5895
  "Filter to a specific content_history ID to see performance of one piece of content."
5876
5896
  ),
5897
+ project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
5877
5898
  limit: z5.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
5878
5899
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
5879
5900
  },
5880
- async ({ platform: platform3, days, content_id, limit, response_format }) => {
5901
+ async ({ platform: platform3, days, content_id, project_id, limit, response_format }) => {
5881
5902
  const format = response_format ?? "text";
5882
5903
  const lookbackDays = days ?? 30;
5883
5904
  const maxPosts = limit ?? 20;
5905
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
5884
5906
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
5885
5907
  action: "analytics",
5886
5908
  platform: platform3,
5887
5909
  days: lookbackDays,
5888
5910
  limit: maxPosts,
5889
- contentId: content_id
5911
+ contentId: content_id,
5912
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
5890
5913
  });
5891
5914
  if (efError) {
5892
5915
  return {
@@ -5956,14 +5979,19 @@ function registerAnalyticsTools(server2) {
5956
5979
  );
5957
5980
  server2.tool(
5958
5981
  "refresh_platform_analytics",
5959
- "Queue analytics refresh jobs for all posts from the last 7 days across connected platforms. Call this before fetch_analytics if you need fresh data. Returns immediately \u2014 data updates asynchronously over the next 1-5 minutes.",
5982
+ "Queue analytics refresh jobs for posts from the last 7 days in one project across its connected platforms. Call this before fetch_analytics if you need fresh data. Returns immediately \u2014 data updates asynchronously over the next 1-5 minutes.",
5960
5983
  {
5984
+ project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
5961
5985
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
5962
5986
  },
5963
- async ({ response_format }) => {
5987
+ async ({ project_id, response_format }) => {
5964
5988
  const format = response_format ?? "text";
5965
5989
  const userId = await getDefaultUserId();
5966
- const rateLimit = checkRateLimit("posting", `refresh_platform_analytics:${userId}`);
5990
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
5991
+ const rateLimit = checkRateLimit(
5992
+ "posting",
5993
+ `refresh_platform_analytics:${userId}:${resolvedProjectId ?? "default"}`
5994
+ );
5967
5995
  if (!rateLimit.allowed) {
5968
5996
  return {
5969
5997
  content: [
@@ -5975,7 +6003,10 @@ function registerAnalyticsTools(server2) {
5975
6003
  isError: true
5976
6004
  };
5977
6005
  }
5978
- const { data, error } = await callEdgeFunction("fetch-analytics", { userId });
6006
+ const { data, error } = await callEdgeFunction("fetch-analytics", {
6007
+ userId,
6008
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
6009
+ });
5979
6010
  if (error) {
5980
6011
  return {
5981
6012
  content: [{ type: "text", text: `Error refreshing analytics: ${error}` }],
@@ -6004,7 +6035,8 @@ function registerAnalyticsTools(server2) {
6004
6035
  success: true,
6005
6036
  postsProcessed: result.postsProcessed,
6006
6037
  queued,
6007
- errored
6038
+ errored,
6039
+ projectId: resolvedProjectId ?? null
6008
6040
  });
6009
6041
  return {
6010
6042
  structuredContent,
@@ -7107,22 +7139,25 @@ function asEnvelope6(data) {
7107
7139
  function registerInsightsTools(server2) {
7108
7140
  server2.tool(
7109
7141
  "get_performance_insights",
7110
- "Query performance insights derived from post analytics. Returns metrics like engagement rate, view velocity, and click rate aggregated over time. Use this to understand what content is performing well.",
7142
+ "Query project-scoped performance insights derived from post analytics. Returns metrics like engagement rate, view velocity, and click rate aggregated over time. Use this to understand what content is performing well.",
7111
7143
  {
7112
7144
  insight_type: z9.enum(["top_hooks", "optimal_timing", "best_models", "competitor_patterns"]).optional().describe("Filter to a specific insight type."),
7113
7145
  days: z9.number().min(1).max(90).optional().describe("Number of days to look back. Defaults to 30. Max 90."),
7114
7146
  limit: z9.number().min(1).max(50).optional().describe("Maximum number of insights to return. Defaults to 10."),
7147
+ project_id: z9.string().optional().describe("Project ID. Defaults to the active project context."),
7115
7148
  response_format: z9.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7116
7149
  },
7117
- async ({ insight_type, days, limit, response_format }) => {
7150
+ async ({ insight_type, days, limit, project_id, response_format }) => {
7118
7151
  const format = response_format ?? "text";
7119
7152
  const lookbackDays = days ?? 30;
7120
7153
  const maxRows = limit ?? 10;
7121
7154
  const effectiveDays = Math.min(lookbackDays, MAX_INSIGHT_AGE_DAYS);
7155
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
7122
7156
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
7123
7157
  action: "performance-insights",
7124
7158
  days: effectiveDays,
7125
- limit: maxRows
7159
+ limit: maxRows,
7160
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
7126
7161
  });
7127
7162
  if (efError || !result?.success) {
7128
7163
  return {
@@ -7209,19 +7244,22 @@ function registerInsightsTools(server2) {
7209
7244
  );
7210
7245
  server2.tool(
7211
7246
  "get_best_posting_times",
7212
- "Analyze post analytics data to find the best times to post for maximum engagement. Returns the top 5 time slots (day of week + hour) ranked by average engagement.",
7247
+ "Analyze project-scoped post analytics data to find the best times to post for maximum engagement. Returns the top 5 time slots (day of week + hour) ranked by average engagement.",
7213
7248
  {
7214
7249
  platform: z9.enum(PLATFORM_ENUM).optional().describe("Filter to a specific platform."),
7215
7250
  days: z9.number().min(1).max(90).optional().describe("Number of days to analyze. Defaults to 30. Max 90."),
7251
+ project_id: z9.string().optional().describe("Project ID. Defaults to the active project context."),
7216
7252
  response_format: z9.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7217
7253
  },
7218
- async ({ platform: platform3, days, response_format }) => {
7254
+ async ({ platform: platform3, days, project_id, response_format }) => {
7219
7255
  const format = response_format ?? "text";
7220
7256
  const lookbackDays = days ?? 30;
7257
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
7221
7258
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
7222
7259
  action: "best-posting-times",
7223
7260
  days: lookbackDays,
7224
- platform: platform3 ?? void 0
7261
+ platform: platform3 ?? void 0,
7262
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
7225
7263
  });
7226
7264
  if (efError || !result?.success) {
7227
7265
  return {
@@ -7335,6 +7373,7 @@ var init_insights = __esm({
7335
7373
  "src/tools/insights.ts"() {
7336
7374
  "use strict";
7337
7375
  init_edge_function();
7376
+ init_supabase();
7338
7377
  init_version();
7339
7378
  MAX_INSIGHT_AGE_DAYS = 30;
7340
7379
  PLATFORM_ENUM = [
@@ -8378,7 +8417,7 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
8378
8417
  }
8379
8418
  const statusData = {
8380
8419
  activeConfigs: result?.activeConfigs ?? 0,
8381
- recentRuns: [],
8420
+ recentRuns: result?.recentRuns ?? result?.recent_runs ?? [],
8382
8421
  pendingApprovals: result?.pendingApprovals ?? 0
8383
8422
  };
8384
8423
  if (format === "json") {
@@ -8400,8 +8439,20 @@ ${"=".repeat(40)}
8400
8439
  text += `Pending Approvals: ${statusData.pendingApprovals}
8401
8440
 
8402
8441
  `;
8403
- text += `No recent runs.
8442
+ if (statusData.recentRuns.length === 0) {
8443
+ text += `No recent runs.
8444
+ `;
8445
+ } else {
8446
+ text += `Recent Runs (${statusData.recentRuns.length}):
8447
+ `;
8448
+ for (const run of statusData.recentRuns.slice(0, 10)) {
8449
+ const id = String(run.id ?? run.run_id ?? "unknown");
8450
+ const status = String(run.status ?? "unknown");
8451
+ const credits = run.credits_used ?? run.creditsUsed;
8452
+ text += ` ${id}: ${status}${credits == null ? "" : ` (${String(credits)} credits)`}
8404
8453
  `;
8454
+ }
8455
+ }
8405
8456
  return {
8406
8457
  content: [{ type: "text", text }]
8407
8458
  };
@@ -13571,7 +13622,7 @@ function registerHyperframesTools(server2) {
13571
13622
  );
13572
13623
  server2.tool(
13573
13624
  "render_hyperframes",
13574
- "Render an HTML video composition (Hyperframes) to MP4. Author the composition as HTML with data-* timing attributes and GSAP timelines \u2014 frame-accurate, no React build step. Use list_hyperframes_blocks to see the pre-built block catalog. Returns a job ID \u2014 poll with check_status. Note: F4 v1 ships the EF + scaffold; the worker container needs the hyperframes runtime installed (Phase 2) before this returns finished MP4s.",
13625
+ "Render an HTML video composition (Hyperframes) to MP4. Author the composition as HTML with data-* timing attributes and GSAP timelines \u2014 frame-accurate, no React build step. Use list_hyperframes_blocks to see the pre-built block catalog. Pass project_id to keep the render with the correct brand/project. Returns a job ID \u2014 poll with check_status.",
13575
13626
  {
13576
13627
  composition_html: z30.string().max(5e5).optional().describe(
13577
13628
  "Inline HTML composition (full <html>...</html>). Max 500KB. Use composition_url for larger."
@@ -13583,7 +13634,8 @@ function registerHyperframesTools(server2) {
13583
13634
  aspect_ratio: z30.enum(["9:16", "16:9", "1:1"]).optional().describe('Output aspect ratio. Default "9:16".'),
13584
13635
  duration_sec: z30.number().min(0.1).max(600).describe("Output duration in seconds. Required. Max 600 (10 min)."),
13585
13636
  fps: z30.union([z30.literal(24), z30.literal(30), z30.literal(60)]).optional().describe("Frames per second. Default 30."),
13586
- quality: z30.enum(["draft", "standard", "high"]).optional().describe("draft: fast iteration; standard: production; high: final master.")
13637
+ quality: z30.enum(["draft", "standard", "high"]).optional().describe("draft: fast iteration; standard: production; high: final master."),
13638
+ project_id: z30.string().optional().describe("Project ID to associate the Hyperframes render with.")
13587
13639
  },
13588
13640
  async ({
13589
13641
  composition_html,
@@ -13592,7 +13644,8 @@ function registerHyperframesTools(server2) {
13592
13644
  aspect_ratio,
13593
13645
  duration_sec,
13594
13646
  fps,
13595
- quality
13647
+ quality,
13648
+ project_id
13596
13649
  }) => {
13597
13650
  const userId = await getDefaultUserId();
13598
13651
  const rateLimit = checkRateLimit("generation", `render_hyperframes:${userId}`);
@@ -13637,7 +13690,8 @@ function registerHyperframesTools(server2) {
13637
13690
  aspectRatio: aspect_ratio || "9:16",
13638
13691
  durationSec: duration_sec,
13639
13692
  fps: fps || 30,
13640
- quality: quality || "standard"
13693
+ quality: quality || "standard",
13694
+ ...project_id && { projectId: project_id }
13641
13695
  });
13642
13696
  if (error || !data?.jobId) {
13643
13697
  throw new Error(error || data?.error || "Failed to create Hyperframes render job");
@@ -13653,7 +13707,7 @@ function registerHyperframesTools(server2) {
13653
13707
  ` Duration: ${duration_sec}s @ ${fps || 30}fps (${aspect_ratio || "9:16"})`,
13654
13708
  ` Quality: ${quality || "standard"}`,
13655
13709
  ``,
13656
- `Poll with check_status. Note: until F4 Phase 2 wires the runtime, the job will fail with a clear "HyperframesNotInstalled" message.`
13710
+ `Poll with check_status.`
13657
13711
  ].join("\n")
13658
13712
  }
13659
13713
  ]
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.8.0";
22
+ MCP_VERSION = "1.8.1";
23
23
  }
24
24
  });
25
25
 
@@ -4164,7 +4164,7 @@ function registerContentTools(server) {
4164
4164
  );
4165
4165
  server.tool(
4166
4166
  "generate_image",
4167
- "Start an async AI image generation job \u2014 returns a job_id immediately. Poll with check_status every 5-15s until complete. Costs 2-10 credits depending on model. Use for social media posts, carousel slides, or as input to generate_video (image-to-video).",
4167
+ "Start an async AI image generation job \u2014 returns a job_id immediately. Poll with check_status every 5-15s until complete. Costs 15-50 credits depending on model. Use for social media posts, carousel slides, or as input to generate_video (image-to-video). Pass project_id so the asset is stored with the correct brand/project.",
4168
4168
  {
4169
4169
  prompt: z2.string().max(2e3).describe(
4170
4170
  "Text prompt describing the image to generate. Be specific about style, composition, colors, lighting, and subject matter."
@@ -4186,9 +4186,10 @@ function registerContentTools(server) {
4186
4186
  image_url: z2.string().optional().describe(
4187
4187
  "Reference image URL for image-to-image generation. Required for ideogram model. Optional for others."
4188
4188
  ),
4189
+ project_id: z2.string().optional().describe("Project ID to associate the generated image with."),
4189
4190
  response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
4190
4191
  },
4191
- async ({ prompt: prompt2, model, aspect_ratio, image_url, response_format }) => {
4192
+ async ({ prompt: prompt2, model, aspect_ratio, image_url, project_id, response_format }) => {
4192
4193
  const format = response_format ?? "text";
4193
4194
  const userId = await getDefaultUserId();
4194
4195
  const assetBudget = checkAssetBudget();
@@ -4224,7 +4225,8 @@ function registerContentTools(server) {
4224
4225
  prompt: prompt2,
4225
4226
  model,
4226
4227
  aspectRatio: aspect_ratio ?? "1:1",
4227
- imageUrl: image_url
4228
+ imageUrl: image_url,
4229
+ ...project_id && { projectId: project_id }
4228
4230
  },
4229
4231
  { timeoutMs: 3e4 }
4230
4232
  );
@@ -4263,7 +4265,8 @@ function registerContentTools(server) {
4263
4265
  jobId,
4264
4266
  taskId: data.taskId,
4265
4267
  asyncJobId: data.asyncJobId,
4266
- model: data.model
4268
+ model: data.model,
4269
+ projectId: project_id ?? null
4267
4270
  }),
4268
4271
  null,
4269
4272
  2
@@ -4444,7 +4447,7 @@ function registerContentTools(server) {
4444
4447
  );
4445
4448
  server.tool(
4446
4449
  "create_storyboard",
4447
- "Plan a multi-scene video storyboard with AI-generated prompts, durations, captions, and voiceover text per frame. Use before generate_video or generate_image to create cohesive multi-shot content. Include brand_context from get_brand_profile for consistent visual branding across frames.",
4450
+ "Plan a multi-scene video storyboard with AI-generated prompts, durations, captions, and voiceover text per frame. Use before generate_video or generate_image to create cohesive multi-shot content. Include brand_context from get_brand_profile and project_id for consistent, project-scoped production. Costs 10 credits.",
4448
4451
  {
4449
4452
  concept: z2.string().max(2e3).describe(
4450
4453
  'The video concept/idea. Include: hook, key messages, target audience, and desired outcome (e.g., "TikTok ad for VPN app targeting privacy-conscious millennials, hook with shocking stat about data leaks").'
@@ -4468,6 +4471,7 @@ function registerContentTools(server) {
4468
4471
  style: z2.string().optional().describe(
4469
4472
  'Visual style direction (e.g., "cinematic", "anime", "documentary", "motion graphics").'
4470
4473
  ),
4474
+ project_id: z2.string().optional().describe("Project ID for brand-scoped generation and attribution."),
4471
4475
  response_format: z2.enum(["text", "json"]).optional().describe(
4472
4476
  "Response format. Defaults to json for structured storyboard data."
4473
4477
  )
@@ -4479,6 +4483,7 @@ function registerContentTools(server) {
4479
4483
  target_duration,
4480
4484
  num_scenes,
4481
4485
  style,
4486
+ project_id,
4482
4487
  response_format
4483
4488
  }) => {
4484
4489
  const format = response_format ?? "json";
@@ -4561,7 +4566,8 @@ Return ONLY valid JSON in this exact format:
4561
4566
  prompt: storyboardPrompt,
4562
4567
  type: "storyboard",
4563
4568
  model: "gemini-2.5-flash",
4564
- responseFormat: "json"
4569
+ responseFormat: "json",
4570
+ ...project_id && { projectId: project_id }
4565
4571
  },
4566
4572
  { timeoutMs: 6e4 }
4567
4573
  );
@@ -4576,7 +4582,18 @@ Return ONLY valid JSON in this exact format:
4576
4582
  isError: true
4577
4583
  };
4578
4584
  }
4579
- const rawContent = data?.content ?? "";
4585
+ const rawContent = data?.content?.trim() || data?.text?.trim() || "";
4586
+ if (!rawContent) {
4587
+ return {
4588
+ content: [
4589
+ {
4590
+ type: "text",
4591
+ text: "Storyboard generation failed: the AI service returned an empty response."
4592
+ }
4593
+ ],
4594
+ isError: true
4595
+ };
4596
+ }
4580
4597
  addCreditsUsed(estimatedCost);
4581
4598
  if (format === "json") {
4582
4599
  try {
@@ -4612,16 +4629,17 @@ Return ONLY valid JSON in this exact format:
4612
4629
  );
4613
4630
  server.tool(
4614
4631
  "generate_voiceover",
4615
- "Generate a voiceover audio file for video narration. Returns an R2-hosted audio URL. Use after create_storyboard to add narration to each scene, or standalone for podcast intros and ad reads. Costs ~2 credits per generation.",
4632
+ "Generate a voiceover audio file for video narration. Returns an R2-hosted audio URL. Use after create_storyboard to add narration to each scene, or standalone for podcast intros and ad reads. Pass project_id to keep the asset with the correct brand/project. Costs 15 credits per generation.",
4616
4633
  {
4617
4634
  text: z2.string().max(5e3).describe("The script/text to convert to speech."),
4618
4635
  voice: z2.enum(["rachel", "domi"]).optional().describe(
4619
4636
  "Voice selection. rachel=warm female, domi=confident female. Defaults to rachel."
4620
4637
  ),
4621
4638
  speed: z2.number().min(0.5).max(2).optional().describe("Speech speed multiplier. 1.0 is normal. Defaults to 1.0."),
4639
+ project_id: z2.string().optional().describe("Project ID to associate the generated voiceover with."),
4622
4640
  response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
4623
4641
  },
4624
- async ({ text, voice, speed, response_format }) => {
4642
+ async ({ text, voice, speed, project_id, response_format }) => {
4625
4643
  const format = response_format ?? "text";
4626
4644
  const userId = await getDefaultUserId();
4627
4645
  const estimatedCost = 15;
@@ -4656,7 +4674,8 @@ Return ONLY valid JSON in this exact format:
4656
4674
  {
4657
4675
  text,
4658
4676
  voiceId: ELEVENLABS_VOICE_IDS[voice ?? "rachel"],
4659
- speed: speed ?? 1
4677
+ speed: speed ?? 1,
4678
+ ...project_id && { projectId: project_id }
4660
4679
  },
4661
4680
  { timeoutMs: 6e4 }
4662
4681
  );
@@ -4692,7 +4711,8 @@ Return ONLY valid JSON in this exact format:
4692
4711
  asEnvelope2({
4693
4712
  audioUrl: data.audioUrl,
4694
4713
  durationSeconds: data.durationSeconds,
4695
- voice: voice ?? "rachel"
4714
+ voice: voice ?? "rachel",
4715
+ projectId: project_id ?? null
4696
4716
  }),
4697
4717
  null,
4698
4718
  2
@@ -7118,7 +7138,7 @@ function asEnvelope4(data) {
7118
7138
  function registerAnalyticsTools(server) {
7119
7139
  server.tool(
7120
7140
  "fetch_analytics",
7121
- "Get post performance metrics \u2014 views, likes, comments, shares, and engagement rate. Filter by platform, time range (default 30 days), or specific content_id. Call refresh_platform_analytics first if data seems stale. Results sorted by most recent capture.",
7141
+ "Get project-scoped post performance metrics \u2014 views, likes, comments, shares, and engagement rate. Filter by platform, time range (default 30 days), or specific content_id. Call refresh_platform_analytics first if data seems stale. Results sorted by most recent capture.",
7122
7142
  {
7123
7143
  platform: z5.enum([
7124
7144
  "youtube",
@@ -7136,19 +7156,22 @@ function registerAnalyticsTools(server) {
7136
7156
  content_id: z5.string().uuid().optional().describe(
7137
7157
  "Filter to a specific content_history ID to see performance of one piece of content."
7138
7158
  ),
7159
+ project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
7139
7160
  limit: z5.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
7140
7161
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7141
7162
  },
7142
- async ({ platform: platform3, days, content_id, limit, response_format }) => {
7163
+ async ({ platform: platform3, days, content_id, project_id, limit, response_format }) => {
7143
7164
  const format = response_format ?? "text";
7144
7165
  const lookbackDays = days ?? 30;
7145
7166
  const maxPosts = limit ?? 20;
7167
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
7146
7168
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
7147
7169
  action: "analytics",
7148
7170
  platform: platform3,
7149
7171
  days: lookbackDays,
7150
7172
  limit: maxPosts,
7151
- contentId: content_id
7173
+ contentId: content_id,
7174
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
7152
7175
  });
7153
7176
  if (efError) {
7154
7177
  return {
@@ -7218,14 +7241,19 @@ function registerAnalyticsTools(server) {
7218
7241
  );
7219
7242
  server.tool(
7220
7243
  "refresh_platform_analytics",
7221
- "Queue analytics refresh jobs for all posts from the last 7 days across connected platforms. Call this before fetch_analytics if you need fresh data. Returns immediately \u2014 data updates asynchronously over the next 1-5 minutes.",
7244
+ "Queue analytics refresh jobs for posts from the last 7 days in one project across its connected platforms. Call this before fetch_analytics if you need fresh data. Returns immediately \u2014 data updates asynchronously over the next 1-5 minutes.",
7222
7245
  {
7246
+ project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
7223
7247
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7224
7248
  },
7225
- async ({ response_format }) => {
7249
+ async ({ project_id, response_format }) => {
7226
7250
  const format = response_format ?? "text";
7227
7251
  const userId = await getDefaultUserId();
7228
- const rateLimit = checkRateLimit("posting", `refresh_platform_analytics:${userId}`);
7252
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
7253
+ const rateLimit = checkRateLimit(
7254
+ "posting",
7255
+ `refresh_platform_analytics:${userId}:${resolvedProjectId ?? "default"}`
7256
+ );
7229
7257
  if (!rateLimit.allowed) {
7230
7258
  return {
7231
7259
  content: [
@@ -7237,7 +7265,10 @@ function registerAnalyticsTools(server) {
7237
7265
  isError: true
7238
7266
  };
7239
7267
  }
7240
- const { data, error } = await callEdgeFunction("fetch-analytics", { userId });
7268
+ const { data, error } = await callEdgeFunction("fetch-analytics", {
7269
+ userId,
7270
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
7271
+ });
7241
7272
  if (error) {
7242
7273
  return {
7243
7274
  content: [{ type: "text", text: `Error refreshing analytics: ${error}` }],
@@ -7266,7 +7297,8 @@ function registerAnalyticsTools(server) {
7266
7297
  success: true,
7267
7298
  postsProcessed: result.postsProcessed,
7268
7299
  queued,
7269
- errored
7300
+ errored,
7301
+ projectId: resolvedProjectId ?? null
7270
7302
  });
7271
7303
  return {
7272
7304
  structuredContent,
@@ -8369,22 +8401,25 @@ function asEnvelope6(data) {
8369
8401
  function registerInsightsTools(server) {
8370
8402
  server.tool(
8371
8403
  "get_performance_insights",
8372
- "Query performance insights derived from post analytics. Returns metrics like engagement rate, view velocity, and click rate aggregated over time. Use this to understand what content is performing well.",
8404
+ "Query project-scoped performance insights derived from post analytics. Returns metrics like engagement rate, view velocity, and click rate aggregated over time. Use this to understand what content is performing well.",
8373
8405
  {
8374
8406
  insight_type: z9.enum(["top_hooks", "optimal_timing", "best_models", "competitor_patterns"]).optional().describe("Filter to a specific insight type."),
8375
8407
  days: z9.number().min(1).max(90).optional().describe("Number of days to look back. Defaults to 30. Max 90."),
8376
8408
  limit: z9.number().min(1).max(50).optional().describe("Maximum number of insights to return. Defaults to 10."),
8409
+ project_id: z9.string().optional().describe("Project ID. Defaults to the active project context."),
8377
8410
  response_format: z9.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8378
8411
  },
8379
- async ({ insight_type, days, limit, response_format }) => {
8412
+ async ({ insight_type, days, limit, project_id, response_format }) => {
8380
8413
  const format = response_format ?? "text";
8381
8414
  const lookbackDays = days ?? 30;
8382
8415
  const maxRows = limit ?? 10;
8383
8416
  const effectiveDays = Math.min(lookbackDays, MAX_INSIGHT_AGE_DAYS);
8417
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
8384
8418
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
8385
8419
  action: "performance-insights",
8386
8420
  days: effectiveDays,
8387
- limit: maxRows
8421
+ limit: maxRows,
8422
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
8388
8423
  });
8389
8424
  if (efError || !result?.success) {
8390
8425
  return {
@@ -8471,19 +8506,22 @@ function registerInsightsTools(server) {
8471
8506
  );
8472
8507
  server.tool(
8473
8508
  "get_best_posting_times",
8474
- "Analyze post analytics data to find the best times to post for maximum engagement. Returns the top 5 time slots (day of week + hour) ranked by average engagement.",
8509
+ "Analyze project-scoped post analytics data to find the best times to post for maximum engagement. Returns the top 5 time slots (day of week + hour) ranked by average engagement.",
8475
8510
  {
8476
8511
  platform: z9.enum(PLATFORM_ENUM).optional().describe("Filter to a specific platform."),
8477
8512
  days: z9.number().min(1).max(90).optional().describe("Number of days to analyze. Defaults to 30. Max 90."),
8513
+ project_id: z9.string().optional().describe("Project ID. Defaults to the active project context."),
8478
8514
  response_format: z9.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8479
8515
  },
8480
- async ({ platform: platform3, days, response_format }) => {
8516
+ async ({ platform: platform3, days, project_id, response_format }) => {
8481
8517
  const format = response_format ?? "text";
8482
8518
  const lookbackDays = days ?? 30;
8519
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
8483
8520
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
8484
8521
  action: "best-posting-times",
8485
8522
  days: lookbackDays,
8486
- platform: platform3 ?? void 0
8523
+ platform: platform3 ?? void 0,
8524
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
8487
8525
  });
8488
8526
  if (efError || !result?.success) {
8489
8527
  return {
@@ -8597,6 +8635,7 @@ var init_insights = __esm({
8597
8635
  "src/tools/insights.ts"() {
8598
8636
  "use strict";
8599
8637
  init_edge_function();
8638
+ init_supabase();
8600
8639
  init_version();
8601
8640
  MAX_INSIGHT_AGE_DAYS = 30;
8602
8641
  PLATFORM_ENUM = [
@@ -9640,7 +9679,7 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
9640
9679
  }
9641
9680
  const statusData = {
9642
9681
  activeConfigs: result?.activeConfigs ?? 0,
9643
- recentRuns: [],
9682
+ recentRuns: result?.recentRuns ?? result?.recent_runs ?? [],
9644
9683
  pendingApprovals: result?.pendingApprovals ?? 0
9645
9684
  };
9646
9685
  if (format === "json") {
@@ -9662,8 +9701,20 @@ ${"=".repeat(40)}
9662
9701
  text += `Pending Approvals: ${statusData.pendingApprovals}
9663
9702
 
9664
9703
  `;
9665
- text += `No recent runs.
9704
+ if (statusData.recentRuns.length === 0) {
9705
+ text += `No recent runs.
9706
+ `;
9707
+ } else {
9708
+ text += `Recent Runs (${statusData.recentRuns.length}):
9709
+ `;
9710
+ for (const run of statusData.recentRuns.slice(0, 10)) {
9711
+ const id = String(run.id ?? run.run_id ?? "unknown");
9712
+ const status = String(run.status ?? "unknown");
9713
+ const credits = run.credits_used ?? run.creditsUsed;
9714
+ text += ` ${id}: ${status}${credits == null ? "" : ` (${String(credits)} credits)`}
9666
9715
  `;
9716
+ }
9717
+ }
9667
9718
  return {
9668
9719
  content: [{ type: "text", text }]
9669
9720
  };
@@ -14833,7 +14884,7 @@ function registerHyperframesTools(server) {
14833
14884
  );
14834
14885
  server.tool(
14835
14886
  "render_hyperframes",
14836
- "Render an HTML video composition (Hyperframes) to MP4. Author the composition as HTML with data-* timing attributes and GSAP timelines \u2014 frame-accurate, no React build step. Use list_hyperframes_blocks to see the pre-built block catalog. Returns a job ID \u2014 poll with check_status. Note: F4 v1 ships the EF + scaffold; the worker container needs the hyperframes runtime installed (Phase 2) before this returns finished MP4s.",
14887
+ "Render an HTML video composition (Hyperframes) to MP4. Author the composition as HTML with data-* timing attributes and GSAP timelines \u2014 frame-accurate, no React build step. Use list_hyperframes_blocks to see the pre-built block catalog. Pass project_id to keep the render with the correct brand/project. Returns a job ID \u2014 poll with check_status.",
14837
14888
  {
14838
14889
  composition_html: z30.string().max(5e5).optional().describe(
14839
14890
  "Inline HTML composition (full <html>...</html>). Max 500KB. Use composition_url for larger."
@@ -14845,7 +14896,8 @@ function registerHyperframesTools(server) {
14845
14896
  aspect_ratio: z30.enum(["9:16", "16:9", "1:1"]).optional().describe('Output aspect ratio. Default "9:16".'),
14846
14897
  duration_sec: z30.number().min(0.1).max(600).describe("Output duration in seconds. Required. Max 600 (10 min)."),
14847
14898
  fps: z30.union([z30.literal(24), z30.literal(30), z30.literal(60)]).optional().describe("Frames per second. Default 30."),
14848
- quality: z30.enum(["draft", "standard", "high"]).optional().describe("draft: fast iteration; standard: production; high: final master.")
14899
+ quality: z30.enum(["draft", "standard", "high"]).optional().describe("draft: fast iteration; standard: production; high: final master."),
14900
+ project_id: z30.string().optional().describe("Project ID to associate the Hyperframes render with.")
14849
14901
  },
14850
14902
  async ({
14851
14903
  composition_html,
@@ -14854,7 +14906,8 @@ function registerHyperframesTools(server) {
14854
14906
  aspect_ratio,
14855
14907
  duration_sec,
14856
14908
  fps,
14857
- quality
14909
+ quality,
14910
+ project_id
14858
14911
  }) => {
14859
14912
  const userId = await getDefaultUserId();
14860
14913
  const rateLimit = checkRateLimit("generation", `render_hyperframes:${userId}`);
@@ -14899,7 +14952,8 @@ function registerHyperframesTools(server) {
14899
14952
  aspectRatio: aspect_ratio || "9:16",
14900
14953
  durationSec: duration_sec,
14901
14954
  fps: fps || 30,
14902
- quality: quality || "standard"
14955
+ quality: quality || "standard",
14956
+ ...project_id && { projectId: project_id }
14903
14957
  });
14904
14958
  if (error || !data?.jobId) {
14905
14959
  throw new Error(error || data?.error || "Failed to create Hyperframes render job");
@@ -14915,7 +14969,7 @@ function registerHyperframesTools(server) {
14915
14969
  ` Duration: ${duration_sec}s @ ${fps || 30}fps (${aspect_ratio || "9:16"})`,
14916
14970
  ` Quality: ${quality || "standard"}`,
14917
14971
  ``,
14918
- `Poll with check_status. Note: until F4 Phase 2 wires the runtime, the job will fail with a clear "HyperframesNotInstalled" message.`
14972
+ `Poll with check_status.`
14919
14973
  ].join("\n")
14920
14974
  }
14921
14975
  ]
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.8.0",
4
+ "version": "1.8.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",
@@ -87,7 +87,7 @@
87
87
  "dependencies": {
88
88
  "@modelcontextprotocol/ext-apps": "1.7.4",
89
89
  "@modelcontextprotocol/sdk": "^1.27.1",
90
- "@supabase/supabase-js": "2.108.2",
90
+ "@supabase/supabase-js": "2.109.0",
91
91
  "express": "^5.1.0",
92
92
  "jose": "^6.2.1",
93
93
  "open": "10.0.0",
package/tools.lock.json CHANGED
@@ -30,7 +30,7 @@
30
30
  "create_autopilot_config": "e87d1e4a35a27ea161274731020ea250b0a737a8c2aa717d545f3d5078e4a7e3",
31
31
  "create_carousel": "b7d3c22e960fe0c8343319d41f5c602f97990fb5796b29ee8b278161c0f08280",
32
32
  "create_plan_approvals": "75eb7eee9d01e280c9bfe1415bc2dba8c55538abf5fdc452ac91fa84d65d3146",
33
- "create_storyboard": "de169bb9bfb42c52a80325fd41833c7edd84f0618739fd0ff4acc136d865b323",
33
+ "create_storyboard": "7660532549ddcd099b925ec0fc0152e7caf7c2975b9a1e57cf4e5ec77d069f2e",
34
34
  "delete_comment": "4ccba2a7dc337350a9867d9c95d56bcf6c70a2631ef86d0fb11b9dec4b424045",
35
35
  "detect_anomalies": "b7d25cccfe416b6a8d1b66dd3cbe39dc02d24ab7882ff7dca4a045fab3ce566f",
36
36
  "execute_recipe": "a0622fedcb6bc8cb55e3219bd27dad81908190bd0294908ef2392400133c6721",
@@ -39,21 +39,21 @@
39
39
  "extract_brand": "5c75c62f4c5b65ab1f23e8c7f897f23aaf54efc3df2a5487d62bd0018da28541",
40
40
  "extract_url_content": "f9590296a6209b9c445e0487fc12c7bf9f10eeba25dbe1fb8ccd578b405a03c3",
41
41
  "fetch": "22f077d7298f21c2eaae9e1f5c280d8f44da01a44a3bfd33f273f381132382e0",
42
- "fetch_analytics": "511a363a6f46bb9145177aa90dc72c44988e81be92db2370d247c5d2421ed333",
42
+ "fetch_analytics": "90980b107c4cb78611d13ab79c8851a2017fc1880212c500739b7e735e5bc1d6",
43
43
  "fetch_trends": "9efd2a24c9f9d9a3a6fa707f2355c8ef0d12dbc25f416e9237c0b9b73d15007b",
44
44
  "fetch_youtube_analytics": "c1d6cf67a5d0ad1696b4320ee91de7ddf58af6cf4a3be537636be746a4822af7",
45
45
  "find_next_slots": "fccf3bc7a8ac54b7ee243c59272252dbfeb307ca8d2987b1527b3b904408c909",
46
46
  "find_winning_content": "f124ae937ea5c3bda8ff9eae82c0ad5b6b095c9a8b1b1812931e10a5b6b35766",
47
47
  "generate_carousel": "ef7ec2137cd8a36c4cf0435e0b96337173807f147e29b6f73b5c5f8874f36b1e",
48
48
  "generate_content": "c741fd25d78e09fecc72fb1ee0b48371c2c2568567ad0f7697b4a95aa9f2640f",
49
- "generate_image": "e208e39cbe09fb64735d745f388d2487ba962dbc5e3e10ea86f05f3181667ee3",
49
+ "generate_image": "24e636035023069009c342c84648bbcc5c49814ce67b6cab245d34bc604b7832",
50
50
  "generate_performance_digest": "8bbb53edc46698927a10372444239ba9973c5c84e2d0a8160f74192495dae74b",
51
51
  "generate_video": "59132d0dde96d733b8b3635bbf9cf66eda7c8d8739caf6303f5d98fc36c519f8",
52
- "generate_voiceover": "46dbb1a9462af8a7fb66812192997bea6bb0b816db94ce27fd1284241ec5eb24",
52
+ "generate_voiceover": "989d385e10f5024906ece521a4ac9338bf478621655ae6384dbcc84efa56c27d",
53
53
  "get_active_campaigns": "2e593f72a0a4e1a7744049556444c64eda760a40f5c08d603c32b3b34b1d760a",
54
54
  "get_autopilot_status": "c6e689867a32a491a7afbbae46326d223bba412f0c66113ddf4be831c73fd769",
55
55
  "get_bandit_state": "1e58ef7836e24d6c9d474bea52916c15397611200e779ded692cc34fe882352f",
56
- "get_best_posting_times": "78932988a8a2238d81c7b2c065e95349fda3e226702998fb08262b74baff6a53",
56
+ "get_best_posting_times": "79c0eceb8da96265478bad0989cd543e70363f8708962a864e4a975b9a594564",
57
57
  "get_brand_profile": "a0ebd63ebd7032de497751d2b35c0ec78c847869d64e54f7019baa5e532fed0e",
58
58
  "get_brand_runtime": "5ac3149d38eac100aca5d769ddb0f5d900832fb29eef78c18dc97678902e3a41",
59
59
  "get_budget_status": "ca79cc516af7ca950b0435949bd801b1e7dd4403eeb4e6152f3bde011ed4b834",
@@ -64,7 +64,7 @@
64
64
  "get_loop_summary": "e37bbc18048c039876dd4f4ba640dbb5e1efaf968fb887cb5479683e2840c1cc",
65
65
  "get_mcp_usage": "3e75743113a8e232c831f915edec38143528e92c504219ac3921d524f738df10",
66
66
  "get_media_url": "2132e420dc3df1a3c6b8d421b3530be23a7282b8f03af53d26707b337de73370",
67
- "get_performance_insights": "88c6d44adddda1d8263bffdf6f924482f58b201ec4595afab9c7285bccd0379e",
67
+ "get_performance_insights": "d7d253877d8703da664085b1d0019824583856184152afc42211126cf3893865",
68
68
  "get_pipeline_status": "16ece649a0516d777ea75654b336ff1cffcfec5eb903960dc8f6d3499d823a0e",
69
69
  "get_recipe_details": "3d74e13a6b2814a414e89a4c75e6657c84aab611dce937ab117fd52e4f2b2eae",
70
70
  "get_recipe_run_status": "70e18a604307fc01f4fc87744c9caaf3d63abcd3ed7ce04af407954ecac2cbdc",
@@ -89,9 +89,9 @@
89
89
  "record_observation": "1c0e32121918b9d5891a28bde48edd364b64a73b3092a51788590f4c79ffb6c0",
90
90
  "record_outcome": "c38a7e37f243f5d0d2c64c5401cd1108ca33f239fdb714bc685104527d3494f0",
91
91
  "record_voice_lesson": "30590e1498a570c3ffd9a070e125b1e28e2cc5d8f98f347a76b43507e6adcc1f",
92
- "refresh_platform_analytics": "2cb4e3c060aa26c6616c09fd5c8831f324fb232a2cdb17c69413db26ee732503",
92
+ "refresh_platform_analytics": "495b9c132fdd094b66b00c582ec94c4ed8c24080704c13f502982761ec817d61",
93
93
  "render_demo_video": "d11f88e7e0dd6829a00e500276c8e31501a5e44e06f53ea38b667b6db2ce64ee",
94
- "render_hyperframes": "b085b9de37a49e027c88b028a926b9bf6f3bc6fce291864dcdcab60dc9ec1140",
94
+ "render_hyperframes": "2fbc2eaeca2da4ee7c19cf5e5b9e54f294d367cacfcbde333472d93238474a4d",
95
95
  "render_template_video": "1a64edb7fd60370bbabfa6751402c7ae32f3174c8c66d7269737297815d8ad92",
96
96
  "reply_to_comment": "ddf1ebefdeb25c1c1e95f7e5060224b88f8b85c4aec77cade4cfecf2c00abbf7",
97
97
  "respond_plan_approval": "60ef3827a8e199b140a1008746947d6753e766505b2dc69291dccddb276216e2",