@socialneuron/mcp-server 1.7.3 → 1.7.5

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/index.js CHANGED
@@ -14,7 +14,7 @@ var MCP_VERSION;
14
14
  var init_version = __esm({
15
15
  "src/lib/version.ts"() {
16
16
  "use strict";
17
- MCP_VERSION = "1.7.2";
17
+ MCP_VERSION = "1.7.5";
18
18
  }
19
19
  });
20
20
 
@@ -895,6 +895,12 @@ var init_tool_catalog = __esm({
895
895
  module: "content",
896
896
  scope: "mcp:write"
897
897
  },
898
+ {
899
+ name: "create_carousel",
900
+ description: "End-to-end carousel: generate text + kick off image jobs for each slide",
901
+ module: "carousel",
902
+ scope: "mcp:write"
903
+ },
898
904
  // media
899
905
  {
900
906
  name: "upload_media",
@@ -1252,6 +1258,45 @@ var init_tool_catalog = __esm({
1252
1258
  description: "Create a new autopilot configuration",
1253
1259
  module: "autopilot",
1254
1260
  scope: "mcp:autopilot"
1261
+ },
1262
+ // brand runtime (additions)
1263
+ {
1264
+ name: "audit_brand_colors",
1265
+ description: "Audit brand color palette for accessibility, contrast, and harmony",
1266
+ module: "brandRuntime",
1267
+ scope: "mcp:read"
1268
+ },
1269
+ {
1270
+ name: "export_design_tokens",
1271
+ description: "Export brand design tokens in CSS/Tailwind/JSON formats",
1272
+ module: "brandRuntime",
1273
+ scope: "mcp:read"
1274
+ },
1275
+ // carousel (already listed in content section above)
1276
+ // recipes
1277
+ {
1278
+ name: "list_recipes",
1279
+ description: "List available recipe templates for automated content workflows",
1280
+ module: "recipes",
1281
+ scope: "mcp:read"
1282
+ },
1283
+ {
1284
+ name: "get_recipe_details",
1285
+ description: "Get full details of a recipe template including steps and required inputs",
1286
+ module: "recipes",
1287
+ scope: "mcp:read"
1288
+ },
1289
+ {
1290
+ name: "execute_recipe",
1291
+ description: "Execute a recipe template with provided inputs to run a multi-step workflow",
1292
+ module: "recipes",
1293
+ scope: "mcp:write"
1294
+ },
1295
+ {
1296
+ name: "get_recipe_run_status",
1297
+ description: "Check the status and progress of a running recipe execution",
1298
+ module: "recipes",
1299
+ scope: "mcp:read"
1255
1300
  }
1256
1301
  ];
1257
1302
  }
@@ -3856,6 +3901,8 @@ var TOOL_SCOPES = {
3856
3901
  get_brand_runtime: "mcp:read",
3857
3902
  explain_brand_system: "mcp:read",
3858
3903
  check_brand_consistency: "mcp:read",
3904
+ audit_brand_colors: "mcp:read",
3905
+ export_design_tokens: "mcp:read",
3859
3906
  get_ideation_context: "mcp:read",
3860
3907
  get_credit_balance: "mcp:read",
3861
3908
  get_budget_status: "mcp:read",
@@ -3877,6 +3924,7 @@ var TOOL_SCOPES = {
3877
3924
  create_storyboard: "mcp:write",
3878
3925
  generate_voiceover: "mcp:write",
3879
3926
  generate_carousel: "mcp:write",
3927
+ create_carousel: "mcp:write",
3880
3928
  upload_media: "mcp:write",
3881
3929
  // mcp:read (media)
3882
3930
  get_media_url: "mcp:read",
@@ -3895,6 +3943,11 @@ var TOOL_SCOPES = {
3895
3943
  list_autopilot_configs: "mcp:autopilot",
3896
3944
  update_autopilot_config: "mcp:autopilot",
3897
3945
  get_autopilot_status: "mcp:autopilot",
3946
+ // Recipes
3947
+ list_recipes: "mcp:read",
3948
+ get_recipe_details: "mcp:read",
3949
+ execute_recipe: "mcp:write",
3950
+ get_recipe_run_status: "mcp:read",
3898
3951
  // mcp:read (content lifecycle — read-only tools)
3899
3952
  extract_url_content: "mcp:read",
3900
3953
  quality_check: "mcp:read",
@@ -5694,7 +5747,7 @@ var ERROR_PATTERNS = [
5694
5747
  // Generic sensitive patterns (API keys, URLs with secrets)
5695
5748
  [/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i, "An internal error occurred. Please try again."]
5696
5749
  ];
5697
- function sanitizeError2(error) {
5750
+ function sanitizeError(error) {
5698
5751
  const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
5699
5752
  if (process.env.NODE_ENV !== "production") {
5700
5753
  console.error("[Error]", msg);
@@ -5707,6 +5760,171 @@ function sanitizeError2(error) {
5707
5760
  return "An unexpected error occurred. Please try again.";
5708
5761
  }
5709
5762
 
5763
+ // src/lib/ssrf.ts
5764
+ var BLOCKED_IP_PATTERNS = [
5765
+ // IPv4 localhost/loopback
5766
+ /^127\./,
5767
+ /^0\./,
5768
+ // IPv4 private ranges (RFC 1918)
5769
+ /^10\./,
5770
+ /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
5771
+ /^192\.168\./,
5772
+ // IPv4 link-local
5773
+ /^169\.254\./,
5774
+ // Cloud metadata endpoint (AWS, GCP, Azure)
5775
+ /^169\.254\.169\.254$/,
5776
+ // IPv4 broadcast
5777
+ /^255\./,
5778
+ // Shared address space (RFC 6598)
5779
+ /^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./
5780
+ ];
5781
+ var BLOCKED_IPV6_PATTERNS = [
5782
+ /^::1$/i,
5783
+ // loopback
5784
+ /^::$/i,
5785
+ // unspecified
5786
+ /^fe[89ab][0-9a-f]:/i,
5787
+ // link-local fe80::/10
5788
+ /^fc[0-9a-f]:/i,
5789
+ // unique local fc00::/7
5790
+ /^fd[0-9a-f]:/i,
5791
+ // unique local fc00::/7
5792
+ /^::ffff:127\./i,
5793
+ // IPv4-mapped localhost
5794
+ /^::ffff:(0|10|127|169\.254|172\.(1[6-9]|2[0-9]|3[0-1])|192\.168)\./i
5795
+ // IPv4-mapped private
5796
+ ];
5797
+ var BLOCKED_HOSTNAMES = [
5798
+ "localhost",
5799
+ "localhost.localdomain",
5800
+ "local",
5801
+ "127.0.0.1",
5802
+ "0.0.0.0",
5803
+ "[::1]",
5804
+ "[::ffff:127.0.0.1]",
5805
+ // Cloud metadata endpoints
5806
+ "metadata.google.internal",
5807
+ "metadata.goog",
5808
+ "instance-data",
5809
+ "instance-data.ec2.internal"
5810
+ ];
5811
+ var ALLOWED_PROTOCOLS = ["http:", "https:"];
5812
+ var BLOCKED_PORTS = [22, 23, 25, 110, 143, 445, 3306, 5432, 6379, 27017, 11211];
5813
+ function isBlockedIP(ip) {
5814
+ const normalized = ip.replace(/^\[/, "").replace(/\]$/, "");
5815
+ if (normalized.includes(":")) {
5816
+ return BLOCKED_IPV6_PATTERNS.some((pattern) => pattern.test(normalized));
5817
+ }
5818
+ return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(normalized));
5819
+ }
5820
+ function isBlockedHostname(hostname) {
5821
+ return BLOCKED_HOSTNAMES.includes(hostname.toLowerCase());
5822
+ }
5823
+ function isIPAddress(hostname) {
5824
+ const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
5825
+ const ipv6Pattern = /^\[?[a-fA-F0-9:]+\]?$/;
5826
+ return ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname);
5827
+ }
5828
+ async function validateUrlForSSRF(urlString) {
5829
+ try {
5830
+ const url = new URL(urlString);
5831
+ if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
5832
+ return {
5833
+ isValid: false,
5834
+ error: `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are allowed.`
5835
+ };
5836
+ }
5837
+ if (url.username || url.password) {
5838
+ return {
5839
+ isValid: false,
5840
+ error: "URLs with embedded credentials are not allowed."
5841
+ };
5842
+ }
5843
+ const hostname = url.hostname.toLowerCase();
5844
+ if (isBlockedHostname(hostname)) {
5845
+ return {
5846
+ isValid: false,
5847
+ error: "Access to internal/localhost addresses is not allowed."
5848
+ };
5849
+ }
5850
+ if (isIPAddress(hostname) && isBlockedIP(hostname)) {
5851
+ return {
5852
+ isValid: false,
5853
+ error: "Access to private/internal IP addresses is not allowed."
5854
+ };
5855
+ }
5856
+ const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80;
5857
+ if (BLOCKED_PORTS.includes(port)) {
5858
+ return {
5859
+ isValid: false,
5860
+ error: `Access to port ${port} is not allowed.`
5861
+ };
5862
+ }
5863
+ let resolvedIP;
5864
+ if (!isIPAddress(hostname)) {
5865
+ try {
5866
+ const dns = await import("node:dns");
5867
+ const resolver = new dns.promises.Resolver();
5868
+ const resolvedIPs = [];
5869
+ try {
5870
+ const aRecords = await resolver.resolve4(hostname);
5871
+ resolvedIPs.push(...aRecords);
5872
+ } catch {
5873
+ }
5874
+ try {
5875
+ const aaaaRecords = await resolver.resolve6(hostname);
5876
+ resolvedIPs.push(...aaaaRecords);
5877
+ } catch {
5878
+ }
5879
+ if (resolvedIPs.length === 0) {
5880
+ return {
5881
+ isValid: false,
5882
+ error: "DNS resolution failed: hostname did not resolve to any address."
5883
+ };
5884
+ }
5885
+ for (const ip of resolvedIPs) {
5886
+ if (isBlockedIP(ip)) {
5887
+ return {
5888
+ isValid: false,
5889
+ error: "Hostname resolves to a private/internal IP address."
5890
+ };
5891
+ }
5892
+ }
5893
+ resolvedIP = resolvedIPs[0];
5894
+ } catch {
5895
+ return {
5896
+ isValid: false,
5897
+ error: "DNS resolution failed. Cannot verify hostname safety."
5898
+ };
5899
+ }
5900
+ }
5901
+ return { isValid: true, sanitizedUrl: url.toString(), resolvedIP };
5902
+ } catch (error) {
5903
+ return {
5904
+ isValid: false,
5905
+ error: `Invalid URL format: ${error instanceof Error ? error.message : "Unknown error"}`
5906
+ };
5907
+ }
5908
+ }
5909
+ function quickSSRFCheck(urlString) {
5910
+ try {
5911
+ const url = new URL(urlString);
5912
+ if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
5913
+ return { isValid: false, error: `Invalid protocol: ${url.protocol}` };
5914
+ }
5915
+ if (url.username || url.password) {
5916
+ return { isValid: false, error: "URLs with credentials not allowed" };
5917
+ }
5918
+ const hostname = url.hostname.toLowerCase();
5919
+ if (isBlockedHostname(hostname) || isIPAddress(hostname) && isBlockedIP(hostname)) {
5920
+ return { isValid: false, error: "Access to internal addresses not allowed" };
5921
+ }
5922
+ return { isValid: true, sanitizedUrl: url.toString() };
5923
+ } catch {
5924
+ return { isValid: false, error: "Invalid URL format" };
5925
+ }
5926
+ }
5927
+
5710
5928
  // src/tools/distribution.ts
5711
5929
  init_supabase();
5712
5930
  init_quality();
@@ -5746,16 +5964,42 @@ function asEnvelope2(data) {
5746
5964
  data
5747
5965
  };
5748
5966
  }
5967
+ function isAlreadyR2Signed(url) {
5968
+ try {
5969
+ const u = new URL(url);
5970
+ return u.searchParams.has("X-Amz-Signature");
5971
+ } catch {
5972
+ return false;
5973
+ }
5974
+ }
5975
+ async function rehostExternalUrl(mediaUrl, projectId) {
5976
+ if (isAlreadyR2Signed(mediaUrl)) {
5977
+ return { signedUrl: mediaUrl, r2Key: "" };
5978
+ }
5979
+ const ssrf = quickSSRFCheck(mediaUrl);
5980
+ if (!ssrf.isValid) {
5981
+ return { error: ssrf.error ?? "URL rejected by SSRF check" };
5982
+ }
5983
+ const { data, error } = await callEdgeFunction(
5984
+ "upload-to-r2",
5985
+ { url: ssrf.sanitizedUrl ?? mediaUrl, projectId },
5986
+ { timeoutMs: 6e4 }
5987
+ );
5988
+ if (error || !data?.key || !data?.url) {
5989
+ return { error: error ?? "upload-to-r2 returned no key" };
5990
+ }
5991
+ return { signedUrl: data.url, r2Key: data.key };
5992
+ }
5749
5993
  function registerDistributionTools(server2) {
5750
5994
  server2.tool(
5751
5995
  "schedule_post",
5752
5996
  'Publish or schedule a post to connected social platforms. Check list_connected_accounts first to verify active OAuth for each target platform. For Instagram carousels: use media_type=CAROUSEL_ALBUM with 2-10 media_urls. For YouTube: title is required. schedule_at uses ISO 8601 (e.g. "2026-03-20T14:00:00Z") \u2014 omit to post immediately.',
5753
5997
  {
5754
5998
  media_url: z3.string().optional().describe(
5755
- "URL of the media file to post. Public URL or R2 signed URL. Not needed if media_urls, r2_key, or job_id is provided."
5999
+ "URL of the media file to post. Any public HTTPS URL works \u2014 including ephemeral generator URLs (Replicate, OpenAI, DALL-E). The server persists non-R2 URLs into R2 before posting so scheduled posts and byte-upload platforms (X, LinkedIn, YouTube, Bluesky) do not 404 when the source URL expires. Set auto_rehost=false to skip. Not needed if media_urls, r2_key, or job_id is provided."
5756
6000
  ),
5757
6001
  media_urls: z3.array(z3.string()).optional().describe(
5758
- "Array of 2-10 image URLs for carousel posts. Each must be publicly accessible or R2 signed URL. Use with media_type=CAROUSEL_ALBUM."
6002
+ "Array of 2-10 image URLs for carousel posts. Same rehosting rules as media_url \u2014 ephemeral URLs are persisted automatically. Use with media_type=CAROUSEL_ALBUM."
5759
6003
  ),
5760
6004
  r2_key: z3.string().optional().describe(
5761
6005
  "R2 object key from upload_media. Signed on demand at post time \u2014 survives scheduling delays. Alternative to media_url."
@@ -5844,7 +6088,10 @@ function registerDistributionTools(server2) {
5844
6088
  ),
5845
6089
  project_id: z3.string().optional().describe("Social Neuron project ID to associate this post with."),
5846
6090
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
5847
- attribution: z3.boolean().optional().describe('If true, appends "Created with Social Neuron" to the caption. Default: false.')
6091
+ attribution: z3.boolean().optional().describe('If true, appends "Created with Social Neuron" to the caption. Default: false.'),
6092
+ auto_rehost: z3.boolean().optional().describe(
6093
+ "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."
6094
+ )
5848
6095
  },
5849
6096
  async ({
5850
6097
  media_url,
@@ -5858,6 +6105,7 @@ function registerDistributionTools(server2) {
5858
6105
  project_id,
5859
6106
  response_format,
5860
6107
  attribution,
6108
+ auto_rehost,
5861
6109
  r2_key,
5862
6110
  r2_keys,
5863
6111
  job_id,
@@ -5969,12 +6217,54 @@ function registerDistributionTools(server2) {
5969
6217
  }
5970
6218
  resolvedMediaUrls = resolved;
5971
6219
  }
6220
+ const shouldRehost = auto_rehost !== false;
6221
+ if (shouldRehost && resolvedMediaUrl && !isAlreadyR2Signed(resolvedMediaUrl)) {
6222
+ const rehost = await rehostExternalUrl(resolvedMediaUrl, project_id);
6223
+ if ("error" in rehost) {
6224
+ return {
6225
+ content: [
6226
+ {
6227
+ type: "text",
6228
+ text: `Failed to persist media URL into R2: ${rehost.error}. Try upload_media first and pass r2_key instead, or set auto_rehost=false if you're sure the URL is publicly durable and every target platform accepts URL ingest.`
6229
+ }
6230
+ ],
6231
+ isError: true
6232
+ };
6233
+ }
6234
+ resolvedMediaUrl = rehost.signedUrl;
6235
+ }
6236
+ if (shouldRehost && resolvedMediaUrls && resolvedMediaUrls.length > 0) {
6237
+ const needsRehost = resolvedMediaUrls.map((u) => !isAlreadyR2Signed(u));
6238
+ if (needsRehost.some(Boolean)) {
6239
+ const rehosted = await Promise.all(
6240
+ resolvedMediaUrls.map(
6241
+ (u, i) => needsRehost[i] ? rehostExternalUrl(u, project_id) : Promise.resolve({ signedUrl: u, r2Key: "" })
6242
+ )
6243
+ );
6244
+ const failIdx = rehosted.findIndex((r) => "error" in r);
6245
+ if (failIdx !== -1) {
6246
+ const failed = rehosted[failIdx];
6247
+ return {
6248
+ content: [
6249
+ {
6250
+ type: "text",
6251
+ text: `Failed to persist media_urls[${failIdx}] into R2: ${failed.error}. Try upload_media first and pass r2_keys instead, or set auto_rehost=false.`
6252
+ }
6253
+ ],
6254
+ isError: true
6255
+ };
6256
+ }
6257
+ resolvedMediaUrls = rehosted.map(
6258
+ (r) => r.signedUrl
6259
+ );
6260
+ }
6261
+ }
5972
6262
  } catch (resolveErr) {
5973
6263
  return {
5974
6264
  content: [
5975
6265
  {
5976
6266
  type: "text",
5977
- text: `Failed to resolve media: ${sanitizeError2(resolveErr)}`
6267
+ text: `Failed to resolve media: ${sanitizeError(resolveErr)}`
5978
6268
  }
5979
6269
  ],
5980
6270
  isError: true
@@ -6339,7 +6629,7 @@ Created with Social Neuron`;
6339
6629
  return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
6340
6630
  } catch (err) {
6341
6631
  const durationMs = Date.now() - startedAt;
6342
- const message = sanitizeError2(err);
6632
+ const message = sanitizeError(err);
6343
6633
  logMcpToolInvocation({
6344
6634
  toolName: "find_next_slots",
6345
6635
  status: "error",
@@ -6859,7 +7149,7 @@ Created with Social Neuron`;
6859
7149
  };
6860
7150
  } catch (err) {
6861
7151
  const durationMs = Date.now() - startedAt;
6862
- const message = sanitizeError2(err);
7152
+ const message = sanitizeError(err);
6863
7153
  logMcpToolInvocation({
6864
7154
  toolName: "schedule_content_plan",
6865
7155
  status: "error",
@@ -6882,6 +7172,19 @@ import { readFile } from "node:fs/promises";
6882
7172
  import { basename, extname } from "node:path";
6883
7173
  init_supabase();
6884
7174
  var MAX_BASE64_SIZE = 10 * 1024 * 1024;
7175
+ var ALLOWED_UPLOAD_TYPES = /* @__PURE__ */ new Set([
7176
+ "image/png",
7177
+ "image/jpeg",
7178
+ "image/gif",
7179
+ "image/webp",
7180
+ "image/svg+xml",
7181
+ "video/mp4",
7182
+ "video/quicktime",
7183
+ "video/x-msvideo",
7184
+ "video/webm"
7185
+ ]);
7186
+ var BASE64_CHARS = /^[A-Za-z0-9+/]+={0,2}$/;
7187
+ var DATA_URI_PREFIX = /^data:([^;,]+);base64,/;
6885
7188
  function maskR2Key(key) {
6886
7189
  const segments = key.split("/");
6887
7190
  return segments.length >= 3 ? `\u2026/${segments.slice(-2).join("/")}` : key;
@@ -6902,21 +7205,35 @@ function inferContentType(filePath) {
6902
7205
  };
6903
7206
  return map[ext] || "application/octet-stream";
6904
7207
  }
7208
+ function approxBase64Size(raw) {
7209
+ const len = raw.length;
7210
+ if (len === 0) return 0;
7211
+ let padding = 0;
7212
+ if (raw.endsWith("==")) padding = 2;
7213
+ else if (raw.endsWith("=")) padding = 1;
7214
+ return Math.floor(len * 3 / 4) - padding;
7215
+ }
6905
7216
  function registerMediaTools(server2) {
6906
7217
  server2.tool(
6907
7218
  "upload_media",
6908
- "Upload a local file or external URL to persistent R2 storage. Returns a durable r2_key that can be passed to schedule_post. Use for images, videos, or any media that needs to be posted to social platforms. Accepts local file paths (in MCP stdio mode) or public URLs. Max 10MB for base64 upload \u2014 larger files return an error with guidance.",
7219
+ "Upload media to persistent R2 storage. Returns a durable r2_key that can be passed to schedule_post. Three input modes: (1) local file path (stdio mode only), (2) public URL fetched by the server, (3) inline base64 via file_data \u2014 use this from Claude Desktop, Claude Web, or any remote agent that cannot hand the server a filesystem path. Base64 uploads are capped at 10MB decoded; larger files still need stdio + presigned PUT.",
6909
7220
  {
6910
- source: z4.string().describe(
6911
- 'Local file path (e.g. "/Users/me/image.png") or public URL (e.g. "https://example.com/photo.jpg"). Local files are read and uploaded as base64. URLs are fetched by the server.'
7221
+ source: z4.string().optional().describe(
7222
+ 'Local file path (e.g. "/Users/me/image.png") or public URL (e.g. "https://example.com/photo.jpg"). Leave empty when passing file_data instead.'
7223
+ ),
7224
+ file_data: z4.string().optional().describe(
7225
+ 'Base64-encoded file bytes, with or without a "data:<mime>;base64," prefix. Use this from remote agents (Claude Web/Desktop) that cannot provide a filesystem path. Max 10MB decoded.'
7226
+ ),
7227
+ file_name: z4.string().optional().describe(
7228
+ 'Optional filename for the upload (e.g. "hero.png"). Path components are stripped \u2014 only the basename is used.'
6912
7229
  ),
6913
7230
  content_type: z4.string().optional().describe(
6914
- 'MIME type (e.g. "image/png", "video/mp4"). Auto-detected from file extension if omitted.'
7231
+ 'MIME type (e.g. "image/png", "video/mp4"). Auto-detected from file extension for paths/URLs, or from the data: prefix on file_data. Required when passing raw base64 with no prefix.'
6915
7232
  ),
6916
7233
  project_id: z4.string().optional().describe("Project ID for R2 path organization."),
6917
7234
  response_format: z4.enum(["text", "json"]).optional().describe("Response format. Default: text.")
6918
7235
  },
6919
- async ({ source, content_type, project_id, response_format }) => {
7236
+ async ({ source, file_data, file_name, content_type, project_id, response_format }) => {
6920
7237
  const format = response_format ?? "text";
6921
7238
  const startedAt = Date.now();
6922
7239
  const userId = await getDefaultUserId();
@@ -6938,54 +7255,195 @@ function registerMediaTools(server2) {
6938
7255
  isError: true
6939
7256
  };
6940
7257
  }
6941
- const isUrl = source.startsWith("http://") || source.startsWith("https://");
6942
- const isLocalFile = !isUrl;
6943
- let uploadBody;
6944
- if (isUrl) {
6945
- const ct = content_type || inferContentType(source);
6946
- uploadBody = {
6947
- url: source,
6948
- contentType: ct,
6949
- fileName: basename(new URL(source).pathname) || "upload",
6950
- projectId: project_id
7258
+ if (!source && !file_data) {
7259
+ return {
7260
+ content: [
7261
+ {
7262
+ type: "text",
7263
+ text: "upload_media requires either `source` (path or URL) or `file_data` (base64)."
7264
+ }
7265
+ ],
7266
+ isError: true
6951
7267
  };
6952
- } else {
6953
- let fileBuffer;
6954
- try {
6955
- fileBuffer = await readFile(source);
6956
- } catch (err) {
6957
- await logMcpToolInvocation({
6958
- toolName: "upload_media",
6959
- status: "error",
6960
- durationMs: Date.now() - startedAt,
6961
- details: { error: `File not found: ${source}` }
6962
- });
7268
+ }
7269
+ if (file_data) {
7270
+ let raw = file_data;
7271
+ let detectedType;
7272
+ const prefixMatch = raw.match(DATA_URI_PREFIX);
7273
+ if (prefixMatch) {
7274
+ detectedType = prefixMatch[1].trim().toLowerCase();
7275
+ raw = raw.slice(prefixMatch[0].length);
7276
+ }
7277
+ const ct = (content_type ?? detectedType ?? "").trim().toLowerCase();
7278
+ if (!ct) {
6963
7279
  return {
6964
7280
  content: [
6965
7281
  {
6966
7282
  type: "text",
6967
- text: `File not found or not readable: ${source}`
7283
+ text: "content_type is required when file_data has no data: prefix."
6968
7284
  }
6969
7285
  ],
6970
7286
  isError: true
6971
7287
  };
6972
7288
  }
6973
- const ct = content_type || inferContentType(source);
6974
- if (fileBuffer.length > MAX_BASE64_SIZE) {
6975
- const { data: putData, error: putError } = await callEdgeFunction(
6976
- "get-signed-url",
6977
- {
6978
- operation: "put",
6979
- contentType: ct,
6980
- filename: basename(source),
6981
- projectId: project_id
6982
- },
6983
- { timeoutMs: 1e4 }
6984
- );
6985
- if (putError || !putData?.signedUrl) {
6986
- return {
6987
- content: [
6988
- {
7289
+ if (!ALLOWED_UPLOAD_TYPES.has(ct)) {
7290
+ return {
7291
+ content: [
7292
+ {
7293
+ type: "text",
7294
+ text: `content_type "${ct}" is not supported. Allowed: ${[...ALLOWED_UPLOAD_TYPES].sort().join(", ")}.`
7295
+ }
7296
+ ],
7297
+ isError: true
7298
+ };
7299
+ }
7300
+ const stripped = raw.replace(/\s+/g, "");
7301
+ if (!BASE64_CHARS.test(stripped)) {
7302
+ return {
7303
+ content: [
7304
+ {
7305
+ type: "text",
7306
+ text: "file_data is not valid base64 \u2014 only A-Z, a-z, 0-9, +, /, = are allowed."
7307
+ }
7308
+ ],
7309
+ isError: true
7310
+ };
7311
+ }
7312
+ const approxSize = approxBase64Size(stripped);
7313
+ if (approxSize > MAX_BASE64_SIZE) {
7314
+ return {
7315
+ content: [
7316
+ {
7317
+ type: "text",
7318
+ text: `file_data exceeds the 10MB base64 cap (got ~${(approxSize / 1024 / 1024).toFixed(1)}MB). For larger files, run the stdio MCP server locally and pass a file path so the server can use presigned PUT upload.`
7319
+ }
7320
+ ],
7321
+ isError: true
7322
+ };
7323
+ }
7324
+ const safeName = basename(file_name ?? "upload");
7325
+ const uploadBody2 = {
7326
+ fileData: `data:${ct};base64,${stripped}`,
7327
+ contentType: ct,
7328
+ fileName: safeName,
7329
+ projectId: project_id
7330
+ };
7331
+ const { data: data2, error: error2 } = await callEdgeFunction("upload-to-r2", uploadBody2, { timeoutMs: 6e4 });
7332
+ if (error2) {
7333
+ await logMcpToolInvocation({
7334
+ toolName: "upload_media",
7335
+ status: "error",
7336
+ durationMs: Date.now() - startedAt,
7337
+ details: { error: error2, source: "base64", contentType: ct, size: approxSize }
7338
+ });
7339
+ return {
7340
+ content: [{ type: "text", text: `Upload failed: ${error2}` }],
7341
+ isError: true
7342
+ };
7343
+ }
7344
+ if (!data2?.key) {
7345
+ return {
7346
+ content: [{ type: "text", text: "Upload returned no R2 key." }],
7347
+ isError: true
7348
+ };
7349
+ }
7350
+ await logMcpToolInvocation({
7351
+ toolName: "upload_media",
7352
+ status: "success",
7353
+ durationMs: Date.now() - startedAt,
7354
+ details: {
7355
+ source: "base64",
7356
+ r2Key: data2.key,
7357
+ size: data2.size,
7358
+ contentType: data2.contentType
7359
+ }
7360
+ });
7361
+ if (format === "json") {
7362
+ return {
7363
+ content: [
7364
+ {
7365
+ type: "text",
7366
+ text: JSON.stringify(
7367
+ {
7368
+ r2_key: data2.key,
7369
+ signed_url: data2.url,
7370
+ size: data2.size,
7371
+ content_type: data2.contentType
7372
+ },
7373
+ null,
7374
+ 2
7375
+ )
7376
+ }
7377
+ ],
7378
+ isError: false
7379
+ };
7380
+ }
7381
+ return {
7382
+ content: [
7383
+ {
7384
+ type: "text",
7385
+ text: [
7386
+ "Media uploaded successfully.",
7387
+ `Media key: ${maskR2Key(data2.key)}`,
7388
+ `Signed URL: ${data2.url}`,
7389
+ `Size: ${(data2.size / 1024).toFixed(0)}KB`,
7390
+ `Type: ${data2.contentType}`,
7391
+ "",
7392
+ "Use job_id or response_format=json with schedule_post to post to any platform."
7393
+ ].join("\n")
7394
+ }
7395
+ ],
7396
+ isError: false
7397
+ };
7398
+ }
7399
+ const src = source;
7400
+ const isUrl = src.startsWith("http://") || src.startsWith("https://");
7401
+ let uploadBody;
7402
+ if (isUrl) {
7403
+ const ct = content_type || inferContentType(src);
7404
+ uploadBody = {
7405
+ url: src,
7406
+ contentType: ct,
7407
+ fileName: basename(file_name ?? new URL(src).pathname) || "upload",
7408
+ projectId: project_id
7409
+ };
7410
+ } else {
7411
+ let fileBuffer;
7412
+ try {
7413
+ fileBuffer = await readFile(src);
7414
+ } catch {
7415
+ await logMcpToolInvocation({
7416
+ toolName: "upload_media",
7417
+ status: "error",
7418
+ durationMs: Date.now() - startedAt,
7419
+ details: { error: "File not found", source: "local" }
7420
+ });
7421
+ return {
7422
+ content: [
7423
+ {
7424
+ type: "text",
7425
+ text: `File not found or not readable: ${src}. Remote agents (Claude Web/Desktop) cannot see the MCP server's filesystem \u2014 pass the bytes via the \`file_data\` parameter (base64, up to 10MB) instead.`
7426
+ }
7427
+ ],
7428
+ isError: true
7429
+ };
7430
+ }
7431
+ const ct = content_type || inferContentType(src);
7432
+ if (fileBuffer.length > MAX_BASE64_SIZE) {
7433
+ const { data: putData, error: putError } = await callEdgeFunction(
7434
+ "get-signed-url",
7435
+ {
7436
+ operation: "put",
7437
+ contentType: ct,
7438
+ filename: basename(file_name ?? src),
7439
+ projectId: project_id
7440
+ },
7441
+ { timeoutMs: 1e4 }
7442
+ );
7443
+ if (putError || !putData?.signedUrl) {
7444
+ return {
7445
+ content: [
7446
+ {
6989
7447
  type: "text",
6990
7448
  text: `Failed to get presigned upload URL: ${putError || "No URL returned"}`
6991
7449
  }
@@ -7079,7 +7537,7 @@ function registerMediaTools(server2) {
7079
7537
  uploadBody = {
7080
7538
  fileData: base64,
7081
7539
  contentType: ct,
7082
- fileName: basename(source),
7540
+ fileName: basename(file_name ?? src),
7083
7541
  projectId: project_id
7084
7542
  };
7085
7543
  }
@@ -7471,164 +7929,15 @@ function formatAnalytics(summary, days, format) {
7471
7929
  init_edge_function();
7472
7930
  init_supabase();
7473
7931
  import { z as z6 } from "zod";
7474
-
7475
- // src/lib/ssrf.ts
7476
- var BLOCKED_IP_PATTERNS = [
7477
- // IPv4 localhost/loopback
7478
- /^127\./,
7479
- /^0\./,
7480
- // IPv4 private ranges (RFC 1918)
7481
- /^10\./,
7482
- /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
7483
- /^192\.168\./,
7484
- // IPv4 link-local
7485
- /^169\.254\./,
7486
- // Cloud metadata endpoint (AWS, GCP, Azure)
7487
- /^169\.254\.169\.254$/,
7488
- // IPv4 broadcast
7489
- /^255\./,
7490
- // Shared address space (RFC 6598)
7491
- /^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./
7492
- ];
7493
- var BLOCKED_IPV6_PATTERNS = [
7494
- /^::1$/i,
7495
- // loopback
7496
- /^::$/i,
7497
- // unspecified
7498
- /^fe[89ab][0-9a-f]:/i,
7499
- // link-local fe80::/10
7500
- /^fc[0-9a-f]:/i,
7501
- // unique local fc00::/7
7502
- /^fd[0-9a-f]:/i,
7503
- // unique local fc00::/7
7504
- /^::ffff:127\./i,
7505
- // IPv4-mapped localhost
7506
- /^::ffff:(0|10|127|169\.254|172\.(1[6-9]|2[0-9]|3[0-1])|192\.168)\./i
7507
- // IPv4-mapped private
7508
- ];
7509
- var BLOCKED_HOSTNAMES = [
7510
- "localhost",
7511
- "localhost.localdomain",
7512
- "local",
7513
- "127.0.0.1",
7514
- "0.0.0.0",
7515
- "[::1]",
7516
- "[::ffff:127.0.0.1]",
7517
- // Cloud metadata endpoints
7518
- "metadata.google.internal",
7519
- "metadata.goog",
7520
- "instance-data",
7521
- "instance-data.ec2.internal"
7522
- ];
7523
- var ALLOWED_PROTOCOLS = ["http:", "https:"];
7524
- var BLOCKED_PORTS = [22, 23, 25, 110, 143, 445, 3306, 5432, 6379, 27017, 11211];
7525
- function isBlockedIP(ip) {
7526
- const normalized = ip.replace(/^\[/, "").replace(/\]$/, "");
7527
- if (normalized.includes(":")) {
7528
- return BLOCKED_IPV6_PATTERNS.some((pattern) => pattern.test(normalized));
7529
- }
7530
- return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(normalized));
7531
- }
7532
- function isBlockedHostname(hostname) {
7533
- return BLOCKED_HOSTNAMES.includes(hostname.toLowerCase());
7534
- }
7535
- function isIPAddress(hostname) {
7536
- const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
7537
- const ipv6Pattern = /^\[?[a-fA-F0-9:]+\]?$/;
7538
- return ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname);
7539
- }
7540
- async function validateUrlForSSRF(urlString) {
7541
- try {
7542
- const url = new URL(urlString);
7543
- if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
7544
- return {
7545
- isValid: false,
7546
- error: `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are allowed.`
7547
- };
7548
- }
7549
- if (url.username || url.password) {
7550
- return {
7551
- isValid: false,
7552
- error: "URLs with embedded credentials are not allowed."
7553
- };
7554
- }
7555
- const hostname = url.hostname.toLowerCase();
7556
- if (isBlockedHostname(hostname)) {
7557
- return {
7558
- isValid: false,
7559
- error: "Access to internal/localhost addresses is not allowed."
7560
- };
7561
- }
7562
- if (isIPAddress(hostname) && isBlockedIP(hostname)) {
7563
- return {
7564
- isValid: false,
7565
- error: "Access to private/internal IP addresses is not allowed."
7566
- };
7567
- }
7568
- const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80;
7569
- if (BLOCKED_PORTS.includes(port)) {
7570
- return {
7571
- isValid: false,
7572
- error: `Access to port ${port} is not allowed.`
7573
- };
7574
- }
7575
- let resolvedIP;
7576
- if (!isIPAddress(hostname)) {
7577
- try {
7578
- const dns = await import("node:dns");
7579
- const resolver = new dns.promises.Resolver();
7580
- const resolvedIPs = [];
7581
- try {
7582
- const aRecords = await resolver.resolve4(hostname);
7583
- resolvedIPs.push(...aRecords);
7584
- } catch {
7585
- }
7586
- try {
7587
- const aaaaRecords = await resolver.resolve6(hostname);
7588
- resolvedIPs.push(...aaaaRecords);
7589
- } catch {
7590
- }
7591
- if (resolvedIPs.length === 0) {
7592
- return {
7593
- isValid: false,
7594
- error: "DNS resolution failed: hostname did not resolve to any address."
7595
- };
7596
- }
7597
- for (const ip of resolvedIPs) {
7598
- if (isBlockedIP(ip)) {
7599
- return {
7600
- isValid: false,
7601
- error: "Hostname resolves to a private/internal IP address."
7602
- };
7603
- }
7604
- }
7605
- resolvedIP = resolvedIPs[0];
7606
- } catch {
7607
- return {
7608
- isValid: false,
7609
- error: "DNS resolution failed. Cannot verify hostname safety."
7610
- };
7611
- }
7612
- }
7613
- return { isValid: true, sanitizedUrl: url.toString(), resolvedIP };
7614
- } catch (error) {
7615
- return {
7616
- isValid: false,
7617
- error: `Invalid URL format: ${error instanceof Error ? error.message : "Unknown error"}`
7618
- };
7619
- }
7620
- }
7621
-
7622
- // src/tools/brand.ts
7623
- init_version();
7624
- function asEnvelope4(data) {
7625
- return {
7626
- _meta: {
7627
- version: MCP_VERSION,
7628
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
7629
- },
7630
- data
7631
- };
7932
+ init_version();
7933
+ function asEnvelope4(data) {
7934
+ return {
7935
+ _meta: {
7936
+ version: MCP_VERSION,
7937
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
7938
+ },
7939
+ data
7940
+ };
7632
7941
  }
7633
7942
  function registerBrandTools(server2) {
7634
7943
  server2.tool(
@@ -8143,7 +8452,7 @@ function registerScreenshotTools(server2) {
8143
8452
  };
8144
8453
  } catch (err) {
8145
8454
  await closeBrowser();
8146
- const message = sanitizeError2(err);
8455
+ const message = sanitizeError(err);
8147
8456
  await logMcpToolInvocation({
8148
8457
  toolName: "capture_app_page",
8149
8458
  status: "error",
@@ -8299,7 +8608,7 @@ function registerScreenshotTools(server2) {
8299
8608
  };
8300
8609
  } catch (err) {
8301
8610
  await closeBrowser();
8302
- const message = sanitizeError2(err);
8611
+ const message = sanitizeError(err);
8303
8612
  await logMcpToolInvocation({
8304
8613
  toolName: "capture_screenshot",
8305
8614
  status: "error",
@@ -8593,7 +8902,7 @@ function registerRemotionTools(server2) {
8593
8902
  ]
8594
8903
  };
8595
8904
  } catch (err) {
8596
- const message = sanitizeError2(err);
8905
+ const message = sanitizeError(err);
8597
8906
  await logMcpToolInvocation({
8598
8907
  toolName: "render_demo_video",
8599
8908
  status: "error",
@@ -8721,7 +9030,7 @@ function registerRemotionTools(server2) {
8721
9030
  ]
8722
9031
  };
8723
9032
  } catch (err) {
8724
- const message = sanitizeError2(err);
9033
+ const message = sanitizeError(err);
8725
9034
  await logMcpToolInvocation({
8726
9035
  toolName: "render_template_video",
8727
9036
  status: "error",
@@ -10181,12 +10490,280 @@ Active: ${is_active}`
10181
10490
  );
10182
10491
  }
10183
10492
 
10184
- // src/tools/extraction.ts
10493
+ // src/tools/recipes.ts
10185
10494
  init_edge_function();
10495
+ init_version();
10186
10496
  import { z as z17 } from "zod";
10497
+ function asEnvelope13(data) {
10498
+ return {
10499
+ _meta: {
10500
+ version: MCP_VERSION,
10501
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
10502
+ },
10503
+ data
10504
+ };
10505
+ }
10506
+ function registerRecipeTools(server2) {
10507
+ server2.tool(
10508
+ "list_recipes",
10509
+ 'List available recipe templates. Recipes are pre-built multi-step workflows like "Weekly Instagram Calendar" or "Product Launch Sequence" that automate common content operations. Use this to discover what recipes are available before running one.',
10510
+ {
10511
+ category: z17.enum([
10512
+ "content_creation",
10513
+ "distribution",
10514
+ "repurposing",
10515
+ "analytics",
10516
+ "engagement",
10517
+ "general"
10518
+ ]).optional().describe("Filter by category. Omit to list all."),
10519
+ featured_only: z17.boolean().optional().describe("If true, only return featured recipes. Defaults to false."),
10520
+ response_format: z17.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
10521
+ },
10522
+ async ({ category, featured_only, response_format }) => {
10523
+ const format = response_format ?? "text";
10524
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
10525
+ action: "list-recipes",
10526
+ category: category ?? null,
10527
+ featured_only: featured_only ?? false
10528
+ });
10529
+ if (efError) {
10530
+ return {
10531
+ content: [
10532
+ {
10533
+ type: "text",
10534
+ text: `Error fetching recipes: ${efError}`
10535
+ }
10536
+ ],
10537
+ isError: true
10538
+ };
10539
+ }
10540
+ const recipes = result?.recipes ?? [];
10541
+ if (format === "json") {
10542
+ return {
10543
+ content: [
10544
+ {
10545
+ type: "text",
10546
+ text: JSON.stringify(asEnvelope13(recipes))
10547
+ }
10548
+ ]
10549
+ };
10550
+ }
10551
+ if (recipes.length === 0) {
10552
+ return {
10553
+ content: [
10554
+ {
10555
+ type: "text",
10556
+ text: "No recipes found. Recipes are pre-built automation templates \u2014 check back after setup."
10557
+ }
10558
+ ]
10559
+ };
10560
+ }
10561
+ const lines = recipes.map(
10562
+ (r) => `**${r.name}** (${r.slug})
10563
+ ${r.description}
10564
+ Category: ${r.category} | Credits: ~${r.estimated_credits} | Steps: ${r.steps.length}${r.is_featured ? " | \u2B50 Featured" : ""}
10565
+ Inputs: ${r.inputs_schema.map((i) => `${i.label}${i.required ? "*" : ""}`).join(", ")}`
10566
+ );
10567
+ return {
10568
+ content: [
10569
+ {
10570
+ type: "text",
10571
+ text: `## Available Recipes (${recipes.length})
10572
+
10573
+ ${lines.join("\n\n")}`
10574
+ }
10575
+ ]
10576
+ };
10577
+ }
10578
+ );
10579
+ server2.tool(
10580
+ "get_recipe_details",
10581
+ "Get full details of a recipe template including all steps, input schema, and estimated costs. Use this before execute_recipe to understand what inputs are required.",
10582
+ {
10583
+ slug: z17.string().describe('Recipe slug (e.g., "weekly-instagram-calendar")'),
10584
+ response_format: z17.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
10585
+ },
10586
+ async ({ slug, response_format }) => {
10587
+ const format = response_format ?? "text";
10588
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
10589
+ action: "get-recipe-details",
10590
+ slug
10591
+ });
10592
+ if (efError) {
10593
+ return {
10594
+ content: [{ type: "text", text: `Error: ${efError}` }],
10595
+ isError: true
10596
+ };
10597
+ }
10598
+ const recipe = result?.recipe;
10599
+ if (!recipe) {
10600
+ return {
10601
+ content: [
10602
+ {
10603
+ type: "text",
10604
+ text: `Recipe "${slug}" not found. Use list_recipes to see available recipes.`
10605
+ }
10606
+ ],
10607
+ isError: true
10608
+ };
10609
+ }
10610
+ if (format === "json") {
10611
+ return {
10612
+ content: [
10613
+ {
10614
+ type: "text",
10615
+ text: JSON.stringify(asEnvelope13(recipe))
10616
+ }
10617
+ ]
10618
+ };
10619
+ }
10620
+ const stepsText = recipe.steps.map((s, i) => ` ${i + 1}. **${s.name}** (${s.type})`).join("\n");
10621
+ const inputsText = recipe.inputs_schema.map(
10622
+ (i) => ` - **${i.label}**${i.required ? " (required)" : ""}: ${i.type}${i.placeholder ? ` \u2014 e.g., "${i.placeholder}"` : ""}`
10623
+ ).join("\n");
10624
+ return {
10625
+ content: [
10626
+ {
10627
+ type: "text",
10628
+ text: [
10629
+ `## ${recipe.name}`,
10630
+ recipe.description,
10631
+ "",
10632
+ `**Category:** ${recipe.category}`,
10633
+ `**Estimated credits:** ~${recipe.estimated_credits}`,
10634
+ `**Estimated time:** ~${Math.round(recipe.estimated_duration_seconds / 60)} minutes`,
10635
+ "",
10636
+ "### Steps",
10637
+ stepsText,
10638
+ "",
10639
+ "### Required Inputs",
10640
+ inputsText
10641
+ ].join("\n")
10642
+ }
10643
+ ]
10644
+ };
10645
+ }
10646
+ );
10647
+ server2.tool(
10648
+ "execute_recipe",
10649
+ "Execute a recipe template with the provided inputs. This creates a recipe run that processes each step sequentially. Long-running recipes will return a run_id you can check with get_recipe_run_status.",
10650
+ {
10651
+ slug: z17.string().describe('Recipe slug (e.g., "weekly-instagram-calendar")'),
10652
+ inputs: z17.record(z17.unknown()).describe(
10653
+ "Input values matching the recipe input schema. Use get_recipe_details to see required inputs."
10654
+ ),
10655
+ response_format: z17.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
10656
+ },
10657
+ async ({ slug, inputs, response_format }) => {
10658
+ const format = response_format ?? "text";
10659
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
10660
+ action: "execute-recipe",
10661
+ slug,
10662
+ inputs
10663
+ });
10664
+ if (efError) {
10665
+ return {
10666
+ content: [{ type: "text", text: `Error: ${efError}` }],
10667
+ isError: true
10668
+ };
10669
+ }
10670
+ if (format === "json") {
10671
+ return {
10672
+ content: [
10673
+ {
10674
+ type: "text",
10675
+ text: JSON.stringify(asEnvelope13(result))
10676
+ }
10677
+ ]
10678
+ };
10679
+ }
10680
+ return {
10681
+ content: [
10682
+ {
10683
+ type: "text",
10684
+ text: `Recipe "${slug}" started.
10685
+
10686
+ **Run ID:** ${result?.run_id}
10687
+ **Status:** ${result?.status}
10688
+
10689
+ ${result?.message || "Use get_recipe_run_status to check progress."}`
10690
+ }
10691
+ ]
10692
+ };
10693
+ }
10694
+ );
10695
+ server2.tool(
10696
+ "get_recipe_run_status",
10697
+ "Check the status of a running recipe execution. Shows progress, current step, credits used, and outputs when complete.",
10698
+ {
10699
+ run_id: z17.string().describe("The recipe run ID returned by execute_recipe"),
10700
+ response_format: z17.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
10701
+ },
10702
+ async ({ run_id, response_format }) => {
10703
+ const format = response_format ?? "text";
10704
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
10705
+ action: "get-recipe-run-status",
10706
+ run_id
10707
+ });
10708
+ if (efError) {
10709
+ return {
10710
+ content: [{ type: "text", text: `Error: ${efError}` }],
10711
+ isError: true
10712
+ };
10713
+ }
10714
+ const run = result?.run;
10715
+ if (!run) {
10716
+ return {
10717
+ content: [
10718
+ {
10719
+ type: "text",
10720
+ text: `Run "${run_id}" not found.`
10721
+ }
10722
+ ],
10723
+ isError: true
10724
+ };
10725
+ }
10726
+ if (format === "json") {
10727
+ return {
10728
+ content: [
10729
+ {
10730
+ type: "text",
10731
+ text: JSON.stringify(asEnvelope13(run))
10732
+ }
10733
+ ]
10734
+ };
10735
+ }
10736
+ const statusEmoji = run.status === "completed" ? "Done" : run.status === "failed" ? "Failed" : run.status === "running" ? "Running" : run.status;
10737
+ return {
10738
+ content: [
10739
+ {
10740
+ type: "text",
10741
+ text: [
10742
+ `**Recipe Run:** ${run.id}`,
10743
+ `**Status:** ${statusEmoji}`,
10744
+ `**Progress:** ${run.progress}%`,
10745
+ run.current_step ? `**Current step:** ${run.current_step}` : "",
10746
+ `**Credits used:** ${run.credits_used}`,
10747
+ run.completed_at ? `**Completed:** ${run.completed_at}` : "",
10748
+ run.outputs ? `
10749
+ **Outputs:**
10750
+ \`\`\`json
10751
+ ${JSON.stringify(run.outputs, null, 2)}
10752
+ \`\`\`` : ""
10753
+ ].filter(Boolean).join("\n")
10754
+ }
10755
+ ]
10756
+ };
10757
+ }
10758
+ );
10759
+ }
10760
+
10761
+ // src/tools/extraction.ts
10762
+ init_edge_function();
10763
+ import { z as z18 } from "zod";
10187
10764
  init_supabase();
10188
10765
  init_version();
10189
- function asEnvelope13(data) {
10766
+ function asEnvelope14(data) {
10190
10767
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
10191
10768
  }
10192
10769
  function isYouTubeUrl(url) {
@@ -10240,11 +10817,11 @@ function registerExtractionTools(server2) {
10240
10817
  "extract_url_content",
10241
10818
  "Extract text content from any URL \u2014 YouTube video transcripts, article text, or product page features/benefits/USP. YouTube URLs auto-route to transcript extraction with optional comments. Use before generate_content to repurpose existing content, or before plan_content_week to base a content plan on a source URL.",
10242
10819
  {
10243
- url: z17.string().url().describe("URL to extract content from"),
10244
- extract_type: z17.enum(["auto", "transcript", "article", "product"]).default("auto").describe("Type of extraction"),
10245
- include_comments: z17.boolean().default(false).describe("Include top comments (YouTube only)"),
10246
- max_results: z17.number().min(1).max(100).default(10).describe("Max comments to include"),
10247
- response_format: z17.enum(["text", "json"]).default("text")
10820
+ url: z18.string().url().describe("URL to extract content from"),
10821
+ extract_type: z18.enum(["auto", "transcript", "article", "product"]).default("auto").describe("Type of extraction"),
10822
+ include_comments: z18.boolean().default(false).describe("Include top comments (YouTube only)"),
10823
+ max_results: z18.number().min(1).max(100).default(10).describe("Max comments to include"),
10824
+ response_format: z18.enum(["text", "json"]).default("text")
10248
10825
  },
10249
10826
  async ({ url, extract_type, include_comments, max_results, response_format }) => {
10250
10827
  const startedAt = Date.now();
@@ -10379,7 +10956,7 @@ function registerExtractionTools(server2) {
10379
10956
  if (response_format === "json") {
10380
10957
  return {
10381
10958
  content: [
10382
- { type: "text", text: JSON.stringify(asEnvelope13(extracted), null, 2) }
10959
+ { type: "text", text: JSON.stringify(asEnvelope14(extracted), null, 2) }
10383
10960
  ],
10384
10961
  isError: false
10385
10962
  };
@@ -10390,7 +10967,7 @@ function registerExtractionTools(server2) {
10390
10967
  };
10391
10968
  } catch (err) {
10392
10969
  const durationMs = Date.now() - startedAt;
10393
- const message = sanitizeError2(err);
10970
+ const message = sanitizeError(err);
10394
10971
  logMcpToolInvocation({
10395
10972
  toolName: "extract_url_content",
10396
10973
  status: "error",
@@ -10410,8 +10987,8 @@ function registerExtractionTools(server2) {
10410
10987
  init_quality();
10411
10988
  init_supabase();
10412
10989
  init_version();
10413
- import { z as z18 } from "zod";
10414
- function asEnvelope14(data) {
10990
+ import { z as z19 } from "zod";
10991
+ function asEnvelope15(data) {
10415
10992
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
10416
10993
  }
10417
10994
  function registerQualityTools(server2) {
@@ -10419,12 +10996,12 @@ function registerQualityTools(server2) {
10419
10996
  "quality_check",
10420
10997
  "Score post quality across 7 categories: Hook Strength, Message Clarity, Platform Fit, Brand Alignment, Novelty, CTA Strength, and Safety/Claims. Each scored 0-5, total 35. Default pass threshold is 26 (~75%). Run after generate_content and before schedule_post. Include hashtags in caption if they will be published \u2014 they affect Platform Fit and Safety scores.",
10421
10998
  {
10422
- caption: z18.string().describe(
10999
+ caption: z19.string().describe(
10423
11000
  "The post text to score. Include hashtags if they will be published \u2014 they affect Platform Fit and Safety/Claims scores."
10424
11001
  ),
10425
- title: z18.string().optional().describe("Post title (important for YouTube)"),
10426
- platforms: z18.array(
10427
- z18.enum([
11002
+ title: z19.string().optional().describe("Post title (important for YouTube)"),
11003
+ platforms: z19.array(
11004
+ z19.enum([
10428
11005
  "youtube",
10429
11006
  "tiktok",
10430
11007
  "instagram",
@@ -10435,13 +11012,13 @@ function registerQualityTools(server2) {
10435
11012
  "bluesky"
10436
11013
  ])
10437
11014
  ).min(1).describe("Target platforms"),
10438
- threshold: z18.number().min(0).max(35).default(26).describe(
11015
+ threshold: z19.number().min(0).max(35).default(26).describe(
10439
11016
  "Minimum total score to pass (max 35, scored across 7 categories at 0-5 each). Default 26 (~75%). Use 20 for rough drafts, 28+ for final posts going to large audiences."
10440
11017
  ),
10441
- brand_keyword: z18.string().optional().describe("Brand keyword for alignment check"),
10442
- brand_avoid_patterns: z18.array(z18.string()).optional(),
10443
- custom_banned_terms: z18.array(z18.string()).optional(),
10444
- response_format: z18.enum(["text", "json"]).default("text")
11018
+ brand_keyword: z19.string().optional().describe("Brand keyword for alignment check"),
11019
+ brand_avoid_patterns: z19.array(z19.string()).optional(),
11020
+ custom_banned_terms: z19.array(z19.string()).optional(),
11021
+ response_format: z19.enum(["text", "json"]).default("text")
10445
11022
  },
10446
11023
  async ({
10447
11024
  caption,
@@ -10472,7 +11049,7 @@ function registerQualityTools(server2) {
10472
11049
  });
10473
11050
  if (response_format === "json") {
10474
11051
  return {
10475
- content: [{ type: "text", text: JSON.stringify(asEnvelope14(result), null, 2) }],
11052
+ content: [{ type: "text", text: JSON.stringify(asEnvelope15(result), null, 2) }],
10476
11053
  isError: false
10477
11054
  };
10478
11055
  }
@@ -10500,20 +11077,20 @@ function registerQualityTools(server2) {
10500
11077
  "quality_check_plan",
10501
11078
  "Batch quality check all posts in a content plan. Returns per-post scores and aggregate pass/fail summary. Use after plan_content_week and before schedule_content_plan to catch low-quality posts before publishing.",
10502
11079
  {
10503
- plan: z18.object({
10504
- posts: z18.array(
10505
- z18.object({
10506
- id: z18.string(),
10507
- caption: z18.string(),
10508
- title: z18.string().optional(),
10509
- platform: z18.string()
11080
+ plan: z19.object({
11081
+ posts: z19.array(
11082
+ z19.object({
11083
+ id: z19.string(),
11084
+ caption: z19.string(),
11085
+ title: z19.string().optional(),
11086
+ platform: z19.string()
10510
11087
  })
10511
11088
  )
10512
11089
  }).passthrough().describe("Content plan with posts array"),
10513
- threshold: z18.number().min(0).max(35).default(26).describe(
11090
+ threshold: z19.number().min(0).max(35).default(26).describe(
10514
11091
  "Minimum total score to pass (max 35, scored across 7 categories at 0-5 each). Default 26 (~75%). Use 20 for rough drafts, 28+ for final posts going to large audiences."
10515
11092
  ),
10516
- response_format: z18.enum(["text", "json"]).default("text")
11093
+ response_format: z19.enum(["text", "json"]).default("text")
10517
11094
  },
10518
11095
  async ({ plan, threshold, response_format }) => {
10519
11096
  const startedAt = Date.now();
@@ -10555,7 +11132,7 @@ function registerQualityTools(server2) {
10555
11132
  content: [
10556
11133
  {
10557
11134
  type: "text",
10558
- text: JSON.stringify(asEnvelope14({ posts: postsWithQuality, summary }), null, 2)
11135
+ text: JSON.stringify(asEnvelope15({ posts: postsWithQuality, summary }), null, 2)
10559
11136
  }
10560
11137
  ],
10561
11138
  isError: false
@@ -10580,7 +11157,7 @@ function registerQualityTools(server2) {
10580
11157
 
10581
11158
  // src/tools/planning.ts
10582
11159
  init_edge_function();
10583
- import { z as z19 } from "zod";
11160
+ import { z as z20 } from "zod";
10584
11161
  import { randomUUID as randomUUID2 } from "node:crypto";
10585
11162
  init_supabase();
10586
11163
  init_version();
@@ -10609,7 +11186,7 @@ function extractJsonArray(text) {
10609
11186
  function toRecord(value) {
10610
11187
  return value && typeof value === "object" ? value : void 0;
10611
11188
  }
10612
- function asEnvelope15(data) {
11189
+ function asEnvelope16(data) {
10613
11190
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
10614
11191
  }
10615
11192
  function tomorrowIsoDate() {
@@ -10679,10 +11256,10 @@ function registerPlanningTools(server2) {
10679
11256
  "plan_content_week",
10680
11257
  "Generate a full content plan with platform-specific drafts, hooks, angles, and optimal schedule times. Pass a topic or source_url \u2014 brand context and performance insights auto-load via project_id. Output feeds directly into quality_check_plan then schedule_content_plan. Costs ~5-15 credits depending on post count.",
10681
11258
  {
10682
- topic: z19.string().describe("Main topic or content theme"),
10683
- source_url: z19.string().optional().describe("URL to extract content from (YouTube, article)"),
10684
- platforms: z19.array(
10685
- z19.enum([
11259
+ topic: z20.string().describe("Main topic or content theme"),
11260
+ source_url: z20.string().optional().describe("URL to extract content from (YouTube, article)"),
11261
+ platforms: z20.array(
11262
+ z20.enum([
10686
11263
  "youtube",
10687
11264
  "tiktok",
10688
11265
  "instagram",
@@ -10693,12 +11270,12 @@ function registerPlanningTools(server2) {
10693
11270
  "bluesky"
10694
11271
  ])
10695
11272
  ).min(1).describe("Target platforms"),
10696
- posts_per_day: z19.number().min(1).max(5).default(1).describe("Posts per platform per day"),
10697
- days: z19.number().min(1).max(7).default(5).describe("Number of days to plan"),
10698
- start_date: z19.string().optional().describe("ISO date, defaults to tomorrow"),
10699
- brand_voice: z19.string().optional().describe("Override brand voice description"),
10700
- project_id: z19.string().optional().describe("Project ID for brand/insights context"),
10701
- response_format: z19.enum(["text", "json"]).default("json")
11273
+ posts_per_day: z20.number().min(1).max(5).default(1).describe("Posts per platform per day"),
11274
+ days: z20.number().min(1).max(7).default(5).describe("Number of days to plan"),
11275
+ start_date: z20.string().optional().describe("ISO date, defaults to tomorrow"),
11276
+ brand_voice: z20.string().optional().describe("Override brand voice description"),
11277
+ project_id: z20.string().optional().describe("Project ID for brand/insights context"),
11278
+ response_format: z20.enum(["text", "json"]).default("json")
10702
11279
  },
10703
11280
  async ({
10704
11281
  topic,
@@ -10950,7 +11527,7 @@ ${rawText.slice(0, 1e3)}`
10950
11527
  }
10951
11528
  } catch (persistErr) {
10952
11529
  const durationMs2 = Date.now() - startedAt;
10953
- const message = sanitizeError2(persistErr);
11530
+ const message = sanitizeError(persistErr);
10954
11531
  logMcpToolInvocation({
10955
11532
  toolName: "plan_content_week",
10956
11533
  status: "error",
@@ -10972,7 +11549,7 @@ ${rawText.slice(0, 1e3)}`
10972
11549
  });
10973
11550
  if (response_format === "json") {
10974
11551
  return {
10975
- content: [{ type: "text", text: JSON.stringify(asEnvelope15(plan), null, 2) }],
11552
+ content: [{ type: "text", text: JSON.stringify(asEnvelope16(plan), null, 2) }],
10976
11553
  isError: false
10977
11554
  };
10978
11555
  }
@@ -10982,7 +11559,7 @@ ${rawText.slice(0, 1e3)}`
10982
11559
  };
10983
11560
  } catch (err) {
10984
11561
  const durationMs = Date.now() - startedAt;
10985
- const message = sanitizeError2(err);
11562
+ const message = sanitizeError(err);
10986
11563
  logMcpToolInvocation({
10987
11564
  toolName: "plan_content_week",
10988
11565
  status: "error",
@@ -11000,13 +11577,13 @@ ${rawText.slice(0, 1e3)}`
11000
11577
  "save_content_plan",
11001
11578
  "Save a content plan to the database for team review, approval workflows, and scheduled publishing. Creates a plan_id you can reference in get_content_plan, update_content_plan, and schedule_content_plan.",
11002
11579
  {
11003
- plan: z19.object({
11004
- topic: z19.string(),
11005
- posts: z19.array(z19.record(z19.string(), z19.unknown()))
11580
+ plan: z20.object({
11581
+ topic: z20.string(),
11582
+ posts: z20.array(z20.record(z20.string(), z20.unknown()))
11006
11583
  }).passthrough(),
11007
- project_id: z19.string().uuid().optional(),
11008
- status: z19.enum(["draft", "in_review", "approved", "scheduled", "completed"]).default("draft"),
11009
- response_format: z19.enum(["text", "json"]).default("json")
11584
+ project_id: z20.string().uuid().optional(),
11585
+ status: z20.enum(["draft", "in_review", "approved", "scheduled", "completed"]).default("draft"),
11586
+ response_format: z20.enum(["text", "json"]).default("json")
11010
11587
  },
11011
11588
  async ({ plan, project_id, status, response_format }) => {
11012
11589
  const startedAt = Date.now();
@@ -11062,7 +11639,7 @@ ${rawText.slice(0, 1e3)}`
11062
11639
  };
11063
11640
  if (response_format === "json") {
11064
11641
  return {
11065
- content: [{ type: "text", text: JSON.stringify(asEnvelope15(result), null, 2) }],
11642
+ content: [{ type: "text", text: JSON.stringify(asEnvelope16(result), null, 2) }],
11066
11643
  isError: false
11067
11644
  };
11068
11645
  }
@@ -11074,7 +11651,7 @@ ${rawText.slice(0, 1e3)}`
11074
11651
  };
11075
11652
  } catch (err) {
11076
11653
  const durationMs = Date.now() - startedAt;
11077
- const message = sanitizeError2(err);
11654
+ const message = sanitizeError(err);
11078
11655
  logMcpToolInvocation({
11079
11656
  toolName: "save_content_plan",
11080
11657
  status: "error",
@@ -11092,8 +11669,8 @@ ${rawText.slice(0, 1e3)}`
11092
11669
  "get_content_plan",
11093
11670
  "Retrieve a persisted content plan by ID.",
11094
11671
  {
11095
- plan_id: z19.string().uuid().describe("Persisted content plan ID"),
11096
- response_format: z19.enum(["text", "json"]).default("json")
11672
+ plan_id: z20.string().uuid().describe("Persisted content plan ID"),
11673
+ response_format: z20.enum(["text", "json"]).default("json")
11097
11674
  },
11098
11675
  async ({ plan_id, response_format }) => {
11099
11676
  const { data: result, error } = await callEdgeFunction("mcp-data", { action: "get-content-plan", plan_id }, { timeoutMs: 1e4 });
@@ -11128,7 +11705,7 @@ ${rawText.slice(0, 1e3)}`
11128
11705
  };
11129
11706
  if (response_format === "json") {
11130
11707
  return {
11131
- content: [{ type: "text", text: JSON.stringify(asEnvelope15(payload), null, 2) }],
11708
+ content: [{ type: "text", text: JSON.stringify(asEnvelope16(payload), null, 2) }],
11132
11709
  isError: false
11133
11710
  };
11134
11711
  }
@@ -11146,23 +11723,23 @@ ${rawText.slice(0, 1e3)}`
11146
11723
  "update_content_plan",
11147
11724
  "Update individual posts in a persisted content plan.",
11148
11725
  {
11149
- plan_id: z19.string().uuid(),
11150
- post_updates: z19.array(
11151
- z19.object({
11152
- post_id: z19.string(),
11153
- caption: z19.string().optional(),
11154
- title: z19.string().optional(),
11155
- hashtags: z19.array(z19.string()).optional(),
11156
- hook: z19.string().optional(),
11157
- angle: z19.string().optional(),
11158
- visual_direction: z19.string().optional(),
11159
- media_url: z19.string().optional(),
11160
- schedule_at: z19.string().optional(),
11161
- platform: z19.string().optional(),
11162
- status: z19.enum(["approved", "rejected", "needs_edit"]).optional()
11726
+ plan_id: z20.string().uuid(),
11727
+ post_updates: z20.array(
11728
+ z20.object({
11729
+ post_id: z20.string(),
11730
+ caption: z20.string().optional(),
11731
+ title: z20.string().optional(),
11732
+ hashtags: z20.array(z20.string()).optional(),
11733
+ hook: z20.string().optional(),
11734
+ angle: z20.string().optional(),
11735
+ visual_direction: z20.string().optional(),
11736
+ media_url: z20.string().optional(),
11737
+ schedule_at: z20.string().optional(),
11738
+ platform: z20.string().optional(),
11739
+ status: z20.enum(["approved", "rejected", "needs_edit"]).optional()
11163
11740
  })
11164
11741
  ).min(1),
11165
- response_format: z19.enum(["text", "json"]).default("json")
11742
+ response_format: z20.enum(["text", "json"]).default("json")
11166
11743
  },
11167
11744
  async ({ plan_id, post_updates, response_format }) => {
11168
11745
  const { data: result, error } = await callEdgeFunction(
@@ -11200,7 +11777,7 @@ ${rawText.slice(0, 1e3)}`
11200
11777
  };
11201
11778
  if (response_format === "json") {
11202
11779
  return {
11203
- content: [{ type: "text", text: JSON.stringify(asEnvelope15(payload), null, 2) }],
11780
+ content: [{ type: "text", text: JSON.stringify(asEnvelope16(payload), null, 2) }],
11204
11781
  isError: false
11205
11782
  };
11206
11783
  }
@@ -11219,8 +11796,8 @@ ${rawText.slice(0, 1e3)}`
11219
11796
  "submit_content_plan_for_approval",
11220
11797
  "Create pending approval items for each post in a plan and mark plan status as in_review.",
11221
11798
  {
11222
- plan_id: z19.string().uuid(),
11223
- response_format: z19.enum(["text", "json"]).default("json")
11799
+ plan_id: z20.string().uuid(),
11800
+ response_format: z20.enum(["text", "json"]).default("json")
11224
11801
  },
11225
11802
  async ({ plan_id, response_format }) => {
11226
11803
  const { data: result, error } = await callEdgeFunction("mcp-data", { action: "submit-plan-approval", plan_id }, { timeoutMs: 15e3 });
@@ -11253,7 +11830,7 @@ ${rawText.slice(0, 1e3)}`
11253
11830
  };
11254
11831
  if (response_format === "json") {
11255
11832
  return {
11256
- content: [{ type: "text", text: JSON.stringify(asEnvelope15(payload), null, 2) }],
11833
+ content: [{ type: "text", text: JSON.stringify(asEnvelope16(payload), null, 2) }],
11257
11834
  isError: false
11258
11835
  };
11259
11836
  }
@@ -11274,8 +11851,8 @@ ${rawText.slice(0, 1e3)}`
11274
11851
  init_edge_function();
11275
11852
  init_supabase();
11276
11853
  init_version();
11277
- import { z as z20 } from "zod";
11278
- function asEnvelope16(data) {
11854
+ import { z as z21 } from "zod";
11855
+ function asEnvelope17(data) {
11279
11856
  return {
11280
11857
  _meta: {
11281
11858
  version: MCP_VERSION,
@@ -11289,19 +11866,19 @@ function registerPlanApprovalTools(server2) {
11289
11866
  "create_plan_approvals",
11290
11867
  "Create pending approval rows for each post in a content plan.",
11291
11868
  {
11292
- plan_id: z20.string().uuid().describe("Content plan ID"),
11293
- posts: z20.array(
11294
- z20.object({
11295
- id: z20.string(),
11296
- platform: z20.string().optional(),
11297
- caption: z20.string().optional(),
11298
- title: z20.string().optional(),
11299
- media_url: z20.string().optional(),
11300
- schedule_at: z20.string().optional()
11869
+ plan_id: z21.string().uuid().describe("Content plan ID"),
11870
+ posts: z21.array(
11871
+ z21.object({
11872
+ id: z21.string(),
11873
+ platform: z21.string().optional(),
11874
+ caption: z21.string().optional(),
11875
+ title: z21.string().optional(),
11876
+ media_url: z21.string().optional(),
11877
+ schedule_at: z21.string().optional()
11301
11878
  }).passthrough()
11302
11879
  ).min(1).describe("Posts to create approval entries for."),
11303
- project_id: z20.string().uuid().optional().describe("Project ID. Defaults to active project context."),
11304
- response_format: z20.enum(["text", "json"]).optional()
11880
+ project_id: z21.string().uuid().optional().describe("Project ID. Defaults to active project context."),
11881
+ response_format: z21.enum(["text", "json"]).optional()
11305
11882
  },
11306
11883
  async ({ plan_id, posts, project_id, response_format }) => {
11307
11884
  const projectId = project_id || await getDefaultProjectId();
@@ -11350,7 +11927,7 @@ function registerPlanApprovalTools(server2) {
11350
11927
  };
11351
11928
  if ((response_format || "text") === "json") {
11352
11929
  return {
11353
- content: [{ type: "text", text: JSON.stringify(asEnvelope16(payload), null, 2) }],
11930
+ content: [{ type: "text", text: JSON.stringify(asEnvelope17(payload), null, 2) }],
11354
11931
  isError: false
11355
11932
  };
11356
11933
  }
@@ -11369,9 +11946,9 @@ function registerPlanApprovalTools(server2) {
11369
11946
  "list_plan_approvals",
11370
11947
  "List MCP-native approval items for a specific content plan.",
11371
11948
  {
11372
- plan_id: z20.string().uuid().describe("Content plan ID"),
11373
- status: z20.enum(["pending", "approved", "rejected", "edited"]).optional(),
11374
- response_format: z20.enum(["text", "json"]).optional()
11949
+ plan_id: z21.string().uuid().describe("Content plan ID"),
11950
+ status: z21.enum(["pending", "approved", "rejected", "edited"]).optional(),
11951
+ response_format: z21.enum(["text", "json"]).optional()
11375
11952
  },
11376
11953
  async ({ plan_id, status, response_format }) => {
11377
11954
  const { data: result, error } = await callEdgeFunction(
@@ -11402,7 +11979,7 @@ function registerPlanApprovalTools(server2) {
11402
11979
  };
11403
11980
  if ((response_format || "text") === "json") {
11404
11981
  return {
11405
- content: [{ type: "text", text: JSON.stringify(asEnvelope16(payload), null, 2) }],
11982
+ content: [{ type: "text", text: JSON.stringify(asEnvelope17(payload), null, 2) }],
11406
11983
  isError: false
11407
11984
  };
11408
11985
  }
@@ -11429,11 +12006,11 @@ function registerPlanApprovalTools(server2) {
11429
12006
  "respond_plan_approval",
11430
12007
  "Approve, reject, or edit a pending plan approval item.",
11431
12008
  {
11432
- approval_id: z20.string().uuid().describe("Approval item ID"),
11433
- decision: z20.enum(["approved", "rejected", "edited"]),
11434
- edited_post: z20.record(z20.string(), z20.unknown()).optional(),
11435
- reason: z20.string().max(1e3).optional(),
11436
- response_format: z20.enum(["text", "json"]).optional()
12009
+ approval_id: z21.string().uuid().describe("Approval item ID"),
12010
+ decision: z21.enum(["approved", "rejected", "edited"]),
12011
+ edited_post: z21.record(z21.string(), z21.unknown()).optional(),
12012
+ reason: z21.string().max(1e3).optional(),
12013
+ response_format: z21.enum(["text", "json"]).optional()
11437
12014
  },
11438
12015
  async ({ approval_id, decision, edited_post, reason, response_format }) => {
11439
12016
  if (decision === "edited" && !edited_post) {
@@ -11480,7 +12057,7 @@ function registerPlanApprovalTools(server2) {
11480
12057
  }
11481
12058
  if ((response_format || "text") === "json") {
11482
12059
  return {
11483
- content: [{ type: "text", text: JSON.stringify(asEnvelope16(data), null, 2) }],
12060
+ content: [{ type: "text", text: JSON.stringify(asEnvelope17(data), null, 2) }],
11484
12061
  isError: false
11485
12062
  };
11486
12063
  }
@@ -11499,16 +12076,16 @@ function registerPlanApprovalTools(server2) {
11499
12076
 
11500
12077
  // src/tools/discovery.ts
11501
12078
  init_tool_catalog();
11502
- import { z as z21 } from "zod";
12079
+ import { z as z22 } from "zod";
11503
12080
  function registerDiscoveryTools(server2) {
11504
12081
  server2.tool(
11505
12082
  "search_tools",
11506
12083
  'Search available tools by name, description, module, or scope. Use "name" detail (~50 tokens) for quick lookup, "summary" (~500 tokens) for descriptions, "full" for complete input schemas. Start here if unsure which tool to call \u2014 filter by module (e.g. "planning", "content", "analytics") to narrow results.',
11507
12084
  {
11508
- query: z21.string().optional().describe("Search query to filter tools by name or description"),
11509
- module: z21.string().optional().describe('Filter by module name (e.g. "planning", "content", "analytics")'),
11510
- scope: z21.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
11511
- detail: z21.enum(["name", "summary", "full"]).default("summary").describe(
12085
+ query: z22.string().optional().describe("Search query to filter tools by name or description"),
12086
+ module: z22.string().optional().describe('Filter by module name (e.g. "planning", "content", "analytics")'),
12087
+ scope: z22.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
12088
+ detail: z22.enum(["name", "summary", "full"]).default("summary").describe(
11512
12089
  'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
11513
12090
  )
11514
12091
  },
@@ -11552,15 +12129,15 @@ function registerDiscoveryTools(server2) {
11552
12129
 
11553
12130
  // src/tools/pipeline.ts
11554
12131
  init_edge_function();
11555
- import { z as z22 } from "zod";
12132
+ import { z as z23 } from "zod";
11556
12133
  import { randomUUID as randomUUID3 } from "node:crypto";
11557
12134
  init_supabase();
11558
12135
  init_quality();
11559
12136
  init_version();
11560
- function asEnvelope17(data) {
12137
+ function asEnvelope18(data) {
11561
12138
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
11562
12139
  }
11563
- var PLATFORM_ENUM2 = z22.enum([
12140
+ var PLATFORM_ENUM2 = z23.enum([
11564
12141
  "youtube",
11565
12142
  "tiktok",
11566
12143
  "instagram",
@@ -11577,10 +12154,10 @@ function registerPipelineTools(server2) {
11577
12154
  "check_pipeline_readiness",
11578
12155
  "Pre-flight check before run_content_pipeline. Verifies: sufficient credits for estimated_posts, active OAuth on target platforms, brand profile exists, no stale insights. Returns pass/fail with specific issues to fix before running the pipeline.",
11579
12156
  {
11580
- project_id: z22.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
11581
- platforms: z22.array(PLATFORM_ENUM2).min(1).describe("Target platforms to check"),
11582
- estimated_posts: z22.number().min(1).max(50).default(5).describe("Estimated posts to generate"),
11583
- response_format: z22.enum(["text", "json"]).optional().describe("Response format")
12157
+ project_id: z23.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
12158
+ platforms: z23.array(PLATFORM_ENUM2).min(1).describe("Target platforms to check"),
12159
+ estimated_posts: z23.number().min(1).max(50).default(5).describe("Estimated posts to generate"),
12160
+ response_format: z23.enum(["text", "json"]).optional().describe("Response format")
11584
12161
  },
11585
12162
  async ({ project_id, platforms, estimated_posts, response_format }) => {
11586
12163
  const format = response_format ?? "text";
@@ -11656,7 +12233,7 @@ function registerPipelineTools(server2) {
11656
12233
  });
11657
12234
  if (format === "json") {
11658
12235
  return {
11659
- content: [{ type: "text", text: JSON.stringify(asEnvelope17(result), null, 2) }]
12236
+ content: [{ type: "text", text: JSON.stringify(asEnvelope18(result), null, 2) }]
11660
12237
  };
11661
12238
  }
11662
12239
  const lines = [];
@@ -11686,7 +12263,7 @@ function registerPipelineTools(server2) {
11686
12263
  return { content: [{ type: "text", text: lines.join("\n") }] };
11687
12264
  } catch (err) {
11688
12265
  const durationMs = Date.now() - startedAt;
11689
- const message = sanitizeError2(err);
12266
+ const message = sanitizeError(err);
11690
12267
  logMcpToolInvocation({
11691
12268
  toolName: "check_pipeline_readiness",
11692
12269
  status: "error",
@@ -11704,22 +12281,22 @@ function registerPipelineTools(server2) {
11704
12281
  "run_content_pipeline",
11705
12282
  "Run the full content pipeline: research trends \u2192 generate plan \u2192 quality check \u2192 auto-approve \u2192 schedule posts. Chains all stages in one call for maximum efficiency. Set dry_run=true to preview the plan without publishing. Check check_pipeline_readiness first to verify credits, OAuth, and brand profile are ready.",
11706
12283
  {
11707
- project_id: z22.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
11708
- topic: z22.string().optional().describe("Content topic (required if no source_url)"),
11709
- source_url: z22.string().optional().describe("URL to extract content from"),
11710
- platforms: z22.array(PLATFORM_ENUM2).min(1).describe("Target platforms"),
11711
- days: z22.number().min(1).max(7).default(5).describe("Days to plan"),
11712
- posts_per_day: z22.number().min(1).max(3).default(1).describe("Posts per platform per day"),
11713
- approval_mode: z22.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe(
12284
+ project_id: z23.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
12285
+ topic: z23.string().optional().describe("Content topic (required if no source_url)"),
12286
+ source_url: z23.string().optional().describe("URL to extract content from"),
12287
+ platforms: z23.array(PLATFORM_ENUM2).min(1).describe("Target platforms"),
12288
+ days: z23.number().min(1).max(7).default(5).describe("Days to plan"),
12289
+ posts_per_day: z23.number().min(1).max(3).default(1).describe("Posts per platform per day"),
12290
+ approval_mode: z23.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe(
11714
12291
  "auto: approve all passing quality. review_all: flag everything. review_low_confidence: auto-approve high scorers."
11715
12292
  ),
11716
- auto_approve_threshold: z22.number().min(0).max(35).default(28).describe(
12293
+ auto_approve_threshold: z23.number().min(0).max(35).default(28).describe(
11717
12294
  "Quality score threshold for auto-approval (used in auto/review_low_confidence modes)"
11718
12295
  ),
11719
- max_credits: z22.number().optional().describe("Credit budget cap"),
11720
- dry_run: z22.boolean().default(false).describe("If true, skip scheduling and return plan only"),
11721
- skip_stages: z22.array(z22.enum(["research", "quality", "schedule"])).optional().describe("Stages to skip"),
11722
- response_format: z22.enum(["text", "json"]).default("json")
12296
+ max_credits: z23.number().optional().describe("Credit budget cap"),
12297
+ dry_run: z23.boolean().default(false).describe("If true, skip scheduling and return plan only"),
12298
+ skip_stages: z23.array(z23.enum(["research", "quality", "schedule"])).optional().describe("Stages to skip"),
12299
+ response_format: z23.enum(["text", "json"]).default("json")
11723
12300
  },
11724
12301
  async ({
11725
12302
  project_id,
@@ -11847,7 +12424,7 @@ function registerPipelineTools(server2) {
11847
12424
  } catch (deductErr) {
11848
12425
  errors.push({
11849
12426
  stage: "planning",
11850
- message: `Credit deduction failed: ${sanitizeError2(deductErr)}`
12427
+ message: `Credit deduction failed: ${sanitizeError(deductErr)}`
11851
12428
  });
11852
12429
  }
11853
12430
  }
@@ -12018,7 +12595,7 @@ function registerPipelineTools(server2) {
12018
12595
  } catch (schedErr) {
12019
12596
  errors.push({
12020
12597
  stage: "schedule",
12021
- message: `Failed to schedule ${post.id}: ${sanitizeError2(schedErr)}`
12598
+ message: `Failed to schedule ${post.id}: ${sanitizeError(schedErr)}`
12022
12599
  });
12023
12600
  }
12024
12601
  }
@@ -12081,7 +12658,7 @@ function registerPipelineTools(server2) {
12081
12658
  if (response_format === "json") {
12082
12659
  return {
12083
12660
  content: [
12084
- { type: "text", text: JSON.stringify(asEnvelope17(resultPayload), null, 2) }
12661
+ { type: "text", text: JSON.stringify(asEnvelope18(resultPayload), null, 2) }
12085
12662
  ]
12086
12663
  };
12087
12664
  }
@@ -12107,7 +12684,7 @@ function registerPipelineTools(server2) {
12107
12684
  return { content: [{ type: "text", text: lines.join("\n") }] };
12108
12685
  } catch (err) {
12109
12686
  const durationMs = Date.now() - startedAt;
12110
- const message = sanitizeError2(err);
12687
+ const message = sanitizeError(err);
12111
12688
  logMcpToolInvocation({
12112
12689
  toolName: "run_content_pipeline",
12113
12690
  status: "error",
@@ -12142,8 +12719,8 @@ function registerPipelineTools(server2) {
12142
12719
  "get_pipeline_status",
12143
12720
  "Check status of a pipeline run, including stages completed, pending approvals, and scheduled posts.",
12144
12721
  {
12145
- pipeline_id: z22.string().uuid().optional().describe("Pipeline run ID (omit for latest)"),
12146
- response_format: z22.enum(["text", "json"]).optional()
12722
+ pipeline_id: z23.string().uuid().optional().describe("Pipeline run ID (omit for latest)"),
12723
+ response_format: z23.enum(["text", "json"]).optional()
12147
12724
  },
12148
12725
  async ({ pipeline_id, response_format }) => {
12149
12726
  const format = response_format ?? "text";
@@ -12169,7 +12746,7 @@ function registerPipelineTools(server2) {
12169
12746
  }
12170
12747
  if (format === "json") {
12171
12748
  return {
12172
- content: [{ type: "text", text: JSON.stringify(asEnvelope17(data), null, 2) }]
12749
+ content: [{ type: "text", text: JSON.stringify(asEnvelope18(data), null, 2) }]
12173
12750
  };
12174
12751
  }
12175
12752
  const lines = [];
@@ -12203,9 +12780,9 @@ function registerPipelineTools(server2) {
12203
12780
  "auto_approve_plan",
12204
12781
  "Batch auto-approve posts in a content plan that meet quality thresholds. Posts below the threshold are flagged for manual review.",
12205
12782
  {
12206
- plan_id: z22.string().uuid().describe("Content plan ID"),
12207
- quality_threshold: z22.number().min(0).max(35).default(26).describe("Minimum quality score to auto-approve"),
12208
- response_format: z22.enum(["text", "json"]).default("json")
12783
+ plan_id: z23.string().uuid().describe("Content plan ID"),
12784
+ quality_threshold: z23.number().min(0).max(35).default(26).describe("Minimum quality score to auto-approve"),
12785
+ response_format: z23.enum(["text", "json"]).default("json")
12209
12786
  },
12210
12787
  async ({ plan_id, quality_threshold, response_format }) => {
12211
12788
  const startedAt = Date.now();
@@ -12311,7 +12888,7 @@ function registerPipelineTools(server2) {
12311
12888
  if (response_format === "json") {
12312
12889
  return {
12313
12890
  content: [
12314
- { type: "text", text: JSON.stringify(asEnvelope17(resultPayload), null, 2) }
12891
+ { type: "text", text: JSON.stringify(asEnvelope18(resultPayload), null, 2) }
12315
12892
  ]
12316
12893
  };
12317
12894
  }
@@ -12331,7 +12908,7 @@ function registerPipelineTools(server2) {
12331
12908
  return { content: [{ type: "text", text: lines.join("\n") }] };
12332
12909
  } catch (err) {
12333
12910
  const durationMs = Date.now() - startedAt;
12334
- const message = sanitizeError2(err);
12911
+ const message = sanitizeError(err);
12335
12912
  logMcpToolInvocation({
12336
12913
  toolName: "auto_approve_plan",
12337
12914
  status: "error",
@@ -12366,10 +12943,10 @@ function buildPlanPrompt(topic, platforms, days, postsPerDay, sourceUrl) {
12366
12943
 
12367
12944
  // src/tools/suggest.ts
12368
12945
  init_edge_function();
12369
- import { z as z23 } from "zod";
12946
+ import { z as z24 } from "zod";
12370
12947
  init_supabase();
12371
12948
  init_version();
12372
- function asEnvelope18(data) {
12949
+ function asEnvelope19(data) {
12373
12950
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
12374
12951
  }
12375
12952
  function registerSuggestTools(server2) {
@@ -12377,9 +12954,9 @@ function registerSuggestTools(server2) {
12377
12954
  "suggest_next_content",
12378
12955
  "Suggest next content topics based on performance insights, past content, and competitor patterns. No AI call, no credit cost \u2014 purely data-driven recommendations.",
12379
12956
  {
12380
- project_id: z23.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
12381
- count: z23.number().min(1).max(10).default(3).describe("Number of suggestions to return"),
12382
- response_format: z23.enum(["text", "json"]).optional()
12957
+ project_id: z24.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
12958
+ count: z24.number().min(1).max(10).default(3).describe("Number of suggestions to return"),
12959
+ response_format: z24.enum(["text", "json"]).optional()
12383
12960
  },
12384
12961
  async ({ project_id, count, response_format }) => {
12385
12962
  const format = response_format ?? "text";
@@ -12489,7 +13066,7 @@ function registerSuggestTools(server2) {
12489
13066
  if (format === "json") {
12490
13067
  return {
12491
13068
  content: [
12492
- { type: "text", text: JSON.stringify(asEnvelope18(resultPayload), null, 2) }
13069
+ { type: "text", text: JSON.stringify(asEnvelope19(resultPayload), null, 2) }
12493
13070
  ]
12494
13071
  };
12495
13072
  }
@@ -12511,7 +13088,7 @@ ${i + 1}. ${s.topic}`);
12511
13088
  return { content: [{ type: "text", text: lines.join("\n") }] };
12512
13089
  } catch (err) {
12513
13090
  const durationMs = Date.now() - startedAt;
12514
- const message = sanitizeError2(err);
13091
+ const message = sanitizeError(err);
12515
13092
  logMcpToolInvocation({
12516
13093
  toolName: "suggest_next_content",
12517
13094
  status: "error",
@@ -12529,7 +13106,7 @@ ${i + 1}. ${s.topic}`);
12529
13106
 
12530
13107
  // src/tools/digest.ts
12531
13108
  init_edge_function();
12532
- import { z as z24 } from "zod";
13109
+ import { z as z25 } from "zod";
12533
13110
  init_supabase();
12534
13111
 
12535
13112
  // src/lib/anomaly-detector.ts
@@ -12634,10 +13211,10 @@ function detectAnomalies(currentData, previousData, sensitivity = "medium", aver
12634
13211
 
12635
13212
  // src/tools/digest.ts
12636
13213
  init_version();
12637
- function asEnvelope19(data) {
13214
+ function asEnvelope20(data) {
12638
13215
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
12639
13216
  }
12640
- var PLATFORM_ENUM3 = z24.enum([
13217
+ var PLATFORM_ENUM3 = z25.enum([
12641
13218
  "youtube",
12642
13219
  "tiktok",
12643
13220
  "instagram",
@@ -12652,10 +13229,10 @@ function registerDigestTools(server2) {
12652
13229
  "generate_performance_digest",
12653
13230
  "Generate a performance summary for a time period. Includes metrics, trends vs previous period, top/bottom performers, platform breakdown, and actionable recommendations. No AI call, no credit cost.",
12654
13231
  {
12655
- project_id: z24.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
12656
- period: z24.enum(["7d", "14d", "30d"]).default("7d").describe("Time period to analyze"),
12657
- include_recommendations: z24.boolean().default(true),
12658
- response_format: z24.enum(["text", "json"]).optional()
13232
+ project_id: z25.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
13233
+ period: z25.enum(["7d", "14d", "30d"]).default("7d").describe("Time period to analyze"),
13234
+ include_recommendations: z25.boolean().default(true),
13235
+ response_format: z25.enum(["text", "json"]).optional()
12659
13236
  },
12660
13237
  async ({ project_id, period, include_recommendations, response_format }) => {
12661
13238
  const format = response_format ?? "text";
@@ -12811,7 +13388,7 @@ function registerDigestTools(server2) {
12811
13388
  });
12812
13389
  if (format === "json") {
12813
13390
  return {
12814
- content: [{ type: "text", text: JSON.stringify(asEnvelope19(digest), null, 2) }]
13391
+ content: [{ type: "text", text: JSON.stringify(asEnvelope20(digest), null, 2) }]
12815
13392
  };
12816
13393
  }
12817
13394
  const lines = [];
@@ -12852,7 +13429,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
12852
13429
  return { content: [{ type: "text", text: lines.join("\n") }] };
12853
13430
  } catch (err) {
12854
13431
  const durationMs = Date.now() - startedAt;
12855
- const message = sanitizeError2(err);
13432
+ const message = sanitizeError(err);
12856
13433
  logMcpToolInvocation({
12857
13434
  toolName: "generate_performance_digest",
12858
13435
  status: "error",
@@ -12870,11 +13447,11 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
12870
13447
  "detect_anomalies",
12871
13448
  "Detect significant performance changes: spikes, drops, viral content, trend shifts. Compares current period against previous equal-length period. No AI call, no credit cost.",
12872
13449
  {
12873
- project_id: z24.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
12874
- days: z24.number().min(7).max(90).default(14).describe("Days to analyze"),
12875
- sensitivity: z24.enum(["low", "medium", "high"]).default("medium").describe("Detection sensitivity: low=50%+, medium=30%+, high=15%+ changes"),
12876
- platforms: z24.array(PLATFORM_ENUM3).optional().describe("Filter to specific platforms"),
12877
- response_format: z24.enum(["text", "json"]).optional()
13450
+ project_id: z25.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
13451
+ days: z25.number().min(7).max(90).default(14).describe("Days to analyze"),
13452
+ sensitivity: z25.enum(["low", "medium", "high"]).default("medium").describe("Detection sensitivity: low=50%+, medium=30%+, high=15%+ changes"),
13453
+ platforms: z25.array(PLATFORM_ENUM3).optional().describe("Filter to specific platforms"),
13454
+ response_format: z25.enum(["text", "json"]).optional()
12878
13455
  },
12879
13456
  async ({ project_id, days, sensitivity, platforms, response_format }) => {
12880
13457
  const format = response_format ?? "text";
@@ -12925,7 +13502,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
12925
13502
  if (format === "json") {
12926
13503
  return {
12927
13504
  content: [
12928
- { type: "text", text: JSON.stringify(asEnvelope19(resultPayload), null, 2) }
13505
+ { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
12929
13506
  ]
12930
13507
  };
12931
13508
  }
@@ -12949,7 +13526,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
12949
13526
  return { content: [{ type: "text", text: lines.join("\n") }] };
12950
13527
  } catch (err) {
12951
13528
  const durationMs = Date.now() - startedAt;
12952
- const message = sanitizeError2(err);
13529
+ const message = sanitizeError(err);
12953
13530
  logMcpToolInvocation({
12954
13531
  toolName: "detect_anomalies",
12955
13532
  status: "error",
@@ -12969,7 +13546,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
12969
13546
  init_edge_function();
12970
13547
  init_supabase();
12971
13548
  init_version();
12972
- import { z as z25 } from "zod";
13549
+ import { z as z26 } from "zod";
12973
13550
 
12974
13551
  // src/lib/brandScoring.ts
12975
13552
  var WEIGHTS = {
@@ -13238,8 +13815,181 @@ function computeBrandConsistency(content, profile, threshold = 60) {
13238
13815
  };
13239
13816
  }
13240
13817
 
13818
+ // src/lib/colorAudit.ts
13819
+ function hexToRgb(hex) {
13820
+ const h = hex.replace("#", "");
13821
+ const full = h.length === 3 ? h[0] + h[0] + h[1] + h[1] + h[2] + h[2] : h;
13822
+ const n = parseInt(full, 16);
13823
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
13824
+ }
13825
+ function srgbToLinear(c) {
13826
+ const s = c / 255;
13827
+ return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
13828
+ }
13829
+ function hexToLab(hex) {
13830
+ const [r, g, b] = hexToRgb(hex);
13831
+ const lr = srgbToLinear(r);
13832
+ const lg = srgbToLinear(g);
13833
+ const lb = srgbToLinear(b);
13834
+ const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
13835
+ const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
13836
+ const z29 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
13837
+ const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
13838
+ const fx = f(x / 0.95047);
13839
+ const fy = f(y / 1);
13840
+ const fz = f(z29 / 1.08883);
13841
+ return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
13842
+ }
13843
+ function deltaE2000(lab1, lab2) {
13844
+ const { L: L1, a: a1, b: b1 } = lab1;
13845
+ const { L: L2, a: a2, b: b2 } = lab2;
13846
+ const C1 = Math.sqrt(a1 * a1 + b1 * b1);
13847
+ const C2 = Math.sqrt(a2 * a2 + b2 * b2);
13848
+ const Cab = (C1 + C2) / 2;
13849
+ const Cab7 = Math.pow(Cab, 7);
13850
+ const G = 0.5 * (1 - Math.sqrt(Cab7 / (Cab7 + Math.pow(25, 7))));
13851
+ const a1p = a1 * (1 + G);
13852
+ const a2p = a2 * (1 + G);
13853
+ const C1p = Math.sqrt(a1p * a1p + b1 * b1);
13854
+ const C2p = Math.sqrt(a2p * a2p + b2 * b2);
13855
+ let h1p = Math.atan2(b1, a1p) * (180 / Math.PI);
13856
+ if (h1p < 0) h1p += 360;
13857
+ let h2p = Math.atan2(b2, a2p) * (180 / Math.PI);
13858
+ if (h2p < 0) h2p += 360;
13859
+ const dLp = L2 - L1;
13860
+ const dCp = C2p - C1p;
13861
+ let dhp;
13862
+ if (C1p * C2p === 0) dhp = 0;
13863
+ else if (Math.abs(h2p - h1p) <= 180) dhp = h2p - h1p;
13864
+ else if (h2p - h1p > 180) dhp = h2p - h1p - 360;
13865
+ else dhp = h2p - h1p + 360;
13866
+ const dHp = 2 * Math.sqrt(C1p * C2p) * Math.sin(dhp * Math.PI / 360);
13867
+ const Lp = (L1 + L2) / 2;
13868
+ const Cp = (C1p + C2p) / 2;
13869
+ let hp;
13870
+ if (C1p * C2p === 0) hp = h1p + h2p;
13871
+ else if (Math.abs(h1p - h2p) <= 180) hp = (h1p + h2p) / 2;
13872
+ else if (h1p + h2p < 360) hp = (h1p + h2p + 360) / 2;
13873
+ else hp = (h1p + h2p - 360) / 2;
13874
+ const T = 1 - 0.17 * Math.cos((hp - 30) * Math.PI / 180) + 0.24 * Math.cos(2 * hp * Math.PI / 180) + 0.32 * Math.cos((3 * hp + 6) * Math.PI / 180) - 0.2 * Math.cos((4 * hp - 63) * Math.PI / 180);
13875
+ const SL = 1 + 0.015 * (Lp - 50) * (Lp - 50) / Math.sqrt(20 + (Lp - 50) * (Lp - 50));
13876
+ const SC = 1 + 0.045 * Cp;
13877
+ const SH = 1 + 0.015 * Cp * T;
13878
+ const Cp7 = Math.pow(Cp, 7);
13879
+ const RT = -2 * Math.sqrt(Cp7 / (Cp7 + Math.pow(25, 7))) * Math.sin(60 * Math.exp(-Math.pow((hp - 275) / 25, 2)) * Math.PI / 180);
13880
+ return Math.sqrt(
13881
+ Math.pow(dLp / SL, 2) + Math.pow(dCp / SC, 2) + Math.pow(dHp / SH, 2) + RT * (dCp / SC) * (dHp / SH)
13882
+ );
13883
+ }
13884
+ var COLOR_SLOTS = [
13885
+ "primary",
13886
+ "secondary",
13887
+ "accent",
13888
+ "background",
13889
+ "success",
13890
+ "warning",
13891
+ "error",
13892
+ "text",
13893
+ "textSecondary"
13894
+ ];
13895
+ function auditBrandColors(palette, contentColors, threshold = 10) {
13896
+ if (!contentColors.length) return { entries: [], overallScore: 100, passed: true };
13897
+ const brandColors = [];
13898
+ for (const slot of COLOR_SLOTS) {
13899
+ const hex = palette[slot];
13900
+ if (typeof hex === "string" && hex.startsWith("#")) {
13901
+ brandColors.push({ slot, hex, lab: hexToLab(hex) });
13902
+ }
13903
+ }
13904
+ if (!brandColors.length) return { entries: [], overallScore: 50, passed: false };
13905
+ const entries = contentColors.map((color) => {
13906
+ const colorLab = hexToLab(color);
13907
+ let minDE = Infinity;
13908
+ let closest = brandColors[0];
13909
+ for (const bc of brandColors) {
13910
+ const de = deltaE2000(colorLab, bc.lab);
13911
+ if (de < minDE) {
13912
+ minDE = de;
13913
+ closest = bc;
13914
+ }
13915
+ }
13916
+ return {
13917
+ color,
13918
+ closestBrandColor: closest.hex,
13919
+ closestBrandSlot: closest.slot,
13920
+ deltaE: Math.round(minDE * 100) / 100,
13921
+ passed: minDE <= threshold
13922
+ };
13923
+ });
13924
+ const passedCount = entries.filter((e) => e.passed).length;
13925
+ const overallScore = Math.round(passedCount / entries.length * 100);
13926
+ return { entries, overallScore, passed: overallScore >= 80 };
13927
+ }
13928
+ function exportDesignTokens(palette, typography, format) {
13929
+ if (format === "css") return exportCSS(palette, typography);
13930
+ if (format === "tailwind") return JSON.stringify(exportTailwind(palette), null, 2);
13931
+ return JSON.stringify(exportFigma(palette, typography), null, 2);
13932
+ }
13933
+ function exportCSS(palette, typography) {
13934
+ const lines = [":root {"];
13935
+ const slots = [
13936
+ ["--brand-primary", "primary"],
13937
+ ["--brand-secondary", "secondary"],
13938
+ ["--brand-accent", "accent"],
13939
+ ["--brand-background", "background"],
13940
+ ["--brand-success", "success"],
13941
+ ["--brand-warning", "warning"],
13942
+ ["--brand-error", "error"],
13943
+ ["--brand-text", "text"],
13944
+ ["--brand-text-secondary", "textSecondary"]
13945
+ ];
13946
+ for (const [varName, key] of slots) {
13947
+ const v = palette[key];
13948
+ if (typeof v === "string" && v) lines.push(` ${varName}: ${v};`);
13949
+ }
13950
+ if (typography) {
13951
+ const hf = typography.headingFont;
13952
+ const bf = typography.bodyFont;
13953
+ if (hf) lines.push(` --brand-font-heading: ${hf};`);
13954
+ if (bf) lines.push(` --brand-font-body: ${bf};`);
13955
+ }
13956
+ lines.push("}");
13957
+ return lines.join("\n");
13958
+ }
13959
+ function exportTailwind(palette) {
13960
+ const colors = {};
13961
+ const map = [
13962
+ ["brand-primary", "primary"],
13963
+ ["brand-secondary", "secondary"],
13964
+ ["brand-accent", "accent"],
13965
+ ["brand-bg", "background"]
13966
+ ];
13967
+ for (const [tw, key] of map) {
13968
+ const v = palette[key];
13969
+ if (typeof v === "string" && v) colors[tw] = v;
13970
+ }
13971
+ return colors;
13972
+ }
13973
+ function exportFigma(palette, typography) {
13974
+ const tokens = { color: {} };
13975
+ for (const slot of ["primary", "secondary", "accent", "background"]) {
13976
+ const v = palette[slot];
13977
+ if (typeof v === "string") tokens.color[slot] = { value: v, type: "color" };
13978
+ }
13979
+ if (typography) {
13980
+ const hf = typography.headingFont;
13981
+ const bf = typography.bodyFont;
13982
+ if (hf || bf) {
13983
+ tokens.fontFamily = {};
13984
+ if (hf) tokens.fontFamily.heading = { value: String(hf), type: "fontFamilies" };
13985
+ if (bf) tokens.fontFamily.body = { value: String(bf), type: "fontFamilies" };
13986
+ }
13987
+ }
13988
+ return tokens;
13989
+ }
13990
+
13241
13991
  // src/tools/brandRuntime.ts
13242
- function asEnvelope20(data) {
13992
+ function asEnvelope21(data) {
13243
13993
  return {
13244
13994
  _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
13245
13995
  data
@@ -13250,7 +14000,7 @@ function registerBrandRuntimeTools(server2) {
13250
14000
  "get_brand_runtime",
13251
14001
  "Get the full brand runtime for a project. Returns the 4-layer brand system: messaging (value props, pillars, proof points), voice (tone, vocabulary, avoid patterns), visual (palette, typography, composition), and operating constraints (audience, archetype). Also returns extraction confidence metadata.",
13252
14002
  {
13253
- project_id: z25.string().optional().describe("Project ID. Defaults to current project.")
14003
+ project_id: z26.string().optional().describe("Project ID. Defaults to current project.")
13254
14004
  },
13255
14005
  async ({ project_id }) => {
13256
14006
  const projectId = project_id || await getDefaultProjectId();
@@ -13310,7 +14060,7 @@ function registerBrandRuntimeTools(server2) {
13310
14060
  pagesScraped: meta.pagesScraped || 0
13311
14061
  }
13312
14062
  };
13313
- const envelope = asEnvelope20(runtime);
14063
+ const envelope = asEnvelope21(runtime);
13314
14064
  return {
13315
14065
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
13316
14066
  };
@@ -13320,7 +14070,7 @@ function registerBrandRuntimeTools(server2) {
13320
14070
  "explain_brand_system",
13321
14071
  "Explains what brand data is available vs missing for a project. Returns a human-readable summary of completeness, confidence levels, and recommendations for improving the brand profile.",
13322
14072
  {
13323
- project_id: z25.string().optional().describe("Project ID. Defaults to current project.")
14073
+ project_id: z26.string().optional().describe("Project ID. Defaults to current project.")
13324
14074
  },
13325
14075
  async ({ project_id }) => {
13326
14076
  const projectId = project_id || await getDefaultProjectId();
@@ -13432,8 +14182,8 @@ function registerBrandRuntimeTools(server2) {
13432
14182
  "check_brand_consistency",
13433
14183
  "Check if content text is consistent with the brand voice, vocabulary, messaging, and factual claims. Returns per-dimension scores (0-100) and specific issues found. Use this to validate scripts, captions, or post copy before publishing.",
13434
14184
  {
13435
- content: z25.string().describe("The content text to check for brand consistency."),
13436
- project_id: z25.string().optional().describe("Project ID. Defaults to current project.")
14185
+ content: z26.string().describe("The content text to check for brand consistency."),
14186
+ project_id: z26.string().optional().describe("Project ID. Defaults to current project.")
13437
14187
  },
13438
14188
  async ({ content, project_id }) => {
13439
14189
  const projectId = project_id || await getDefaultProjectId();
@@ -13452,7 +14202,76 @@ function registerBrandRuntimeTools(server2) {
13452
14202
  }
13453
14203
  const profile = row.profile_data;
13454
14204
  const checkResult = computeBrandConsistency(content, profile);
13455
- const envelope = asEnvelope20(checkResult);
14205
+ const envelope = asEnvelope21(checkResult);
14206
+ return {
14207
+ content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
14208
+ };
14209
+ }
14210
+ );
14211
+ server2.tool(
14212
+ "audit_brand_colors",
14213
+ "Audit content colors against the brand palette using perceptual color distance (Delta E 2000). Returns per-color compliance scores and identifies the closest brand color for each input.",
14214
+ {
14215
+ content_colors: z26.array(z26.string()).describe('Hex color strings used in the content (e.g., ["#FF0000", "#00FF00"])'),
14216
+ project_id: z26.string().optional().describe("Project ID. Defaults to current project."),
14217
+ threshold: z26.number().optional().describe("Max Delta E for on-brand (default 10). Lower = stricter.")
14218
+ },
14219
+ async ({ content_colors, project_id, threshold }) => {
14220
+ const projectId = project_id || await getDefaultProjectId();
14221
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", { action: "brand-profile", projectId });
14222
+ const row = !efError && result?.success ? result.profile : null;
14223
+ if (!row?.profile_data?.colorPalette) {
14224
+ return {
14225
+ content: [
14226
+ {
14227
+ type: "text",
14228
+ text: "No brand color palette found. Extract a brand profile first."
14229
+ }
14230
+ ],
14231
+ isError: true
14232
+ };
14233
+ }
14234
+ const auditResult = auditBrandColors(
14235
+ row.profile_data.colorPalette,
14236
+ content_colors,
14237
+ threshold ?? 10
14238
+ );
14239
+ const envelope = asEnvelope21(auditResult);
14240
+ return {
14241
+ content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
14242
+ };
14243
+ }
14244
+ );
14245
+ server2.tool(
14246
+ "export_design_tokens",
14247
+ "Export brand palette and typography as design tokens. Supports CSS custom properties, Tailwind config, and Figma Tokens JSON formats.",
14248
+ {
14249
+ format: z26.enum(["css", "tailwind", "figma"]).describe(
14250
+ "Output format: css (CSS variables), tailwind (theme.extend.colors), figma (Figma Tokens JSON)"
14251
+ ),
14252
+ project_id: z26.string().optional().describe("Project ID. Defaults to current project.")
14253
+ },
14254
+ async ({ format, project_id }) => {
14255
+ const projectId = project_id || await getDefaultProjectId();
14256
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", { action: "brand-profile", projectId });
14257
+ const row = !efError && result?.success ? result.profile : null;
14258
+ if (!row?.profile_data?.colorPalette) {
14259
+ return {
14260
+ content: [
14261
+ {
14262
+ type: "text",
14263
+ text: "No brand color palette found. Extract a brand profile first."
14264
+ }
14265
+ ],
14266
+ isError: true
14267
+ };
14268
+ }
14269
+ const output = exportDesignTokens(
14270
+ row.profile_data.colorPalette,
14271
+ row.profile_data.typography,
14272
+ format
14273
+ );
14274
+ const envelope = asEnvelope21({ format, tokens: output });
13456
14275
  return {
13457
14276
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
13458
14277
  };
@@ -13460,6 +14279,391 @@ function registerBrandRuntimeTools(server2) {
13460
14279
  );
13461
14280
  }
13462
14281
 
14282
+ // src/tools/carousel.ts
14283
+ init_edge_function();
14284
+ import { z as z27 } from "zod";
14285
+ init_supabase();
14286
+ init_request_context();
14287
+ init_version();
14288
+ var MAX_CREDITS_PER_RUN2 = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
14289
+ var MAX_ASSETS_PER_RUN2 = Math.max(0, Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0));
14290
+ var _globalCreditsUsed2 = 0;
14291
+ var _globalAssetsGenerated2 = 0;
14292
+ function getCreditsUsed2() {
14293
+ const ctx = requestContext.getStore();
14294
+ return ctx ? ctx.creditsUsed : _globalCreditsUsed2;
14295
+ }
14296
+ function addCreditsUsed2(amount) {
14297
+ const ctx = requestContext.getStore();
14298
+ if (ctx) {
14299
+ ctx.creditsUsed += amount;
14300
+ } else {
14301
+ _globalCreditsUsed2 += amount;
14302
+ }
14303
+ }
14304
+ function getAssetsGenerated2() {
14305
+ const ctx = requestContext.getStore();
14306
+ return ctx ? ctx.assetsGenerated : _globalAssetsGenerated2;
14307
+ }
14308
+ function addAssetsGenerated2(count) {
14309
+ const ctx = requestContext.getStore();
14310
+ if (ctx) {
14311
+ ctx.assetsGenerated += count;
14312
+ } else {
14313
+ _globalAssetsGenerated2 += count;
14314
+ }
14315
+ }
14316
+ function checkCreditBudget2(estimatedCost) {
14317
+ if (MAX_CREDITS_PER_RUN2 <= 0) return { ok: true };
14318
+ const used = getCreditsUsed2();
14319
+ if (used + estimatedCost > MAX_CREDITS_PER_RUN2) {
14320
+ return {
14321
+ ok: false,
14322
+ message: `Credit budget exceeded: ${used} used + ${estimatedCost} estimated > ${MAX_CREDITS_PER_RUN2} limit. Use a smaller slide count or cheaper image model.`
14323
+ };
14324
+ }
14325
+ return { ok: true };
14326
+ }
14327
+ function checkAssetBudget2() {
14328
+ if (MAX_ASSETS_PER_RUN2 <= 0) return { ok: true };
14329
+ const gen = getAssetsGenerated2();
14330
+ if (gen >= MAX_ASSETS_PER_RUN2) {
14331
+ return {
14332
+ ok: false,
14333
+ message: `Asset limit reached: ${gen}/${MAX_ASSETS_PER_RUN2} assets generated this run.`
14334
+ };
14335
+ }
14336
+ return { ok: true };
14337
+ }
14338
+ var IMAGE_CREDIT_ESTIMATES2 = {
14339
+ midjourney: 20,
14340
+ "nano-banana": 15,
14341
+ "nano-banana-pro": 25,
14342
+ "flux-pro": 30,
14343
+ "flux-max": 50,
14344
+ "gpt4o-image": 40,
14345
+ imagen4: 35,
14346
+ "imagen4-fast": 25,
14347
+ seedream: 20
14348
+ };
14349
+ async function fetchBrandVisualContext(projectId) {
14350
+ const { data, error } = await callEdgeFunction("mcp-data", { action: "brand-profile", projectId });
14351
+ if (error || !data?.success || !data.profile?.profile_data) return null;
14352
+ const profile = data.profile.profile_data;
14353
+ const parts = [];
14354
+ const palette = profile.colorPalette;
14355
+ if (palette) {
14356
+ const colors = Object.entries(palette).filter(([, v]) => typeof v === "string" && v.startsWith("#")).map(([k, v]) => `${k}: ${v}`).slice(0, 5);
14357
+ if (colors.length > 0) {
14358
+ parts.push(`Brand color palette: ${colors.join(", ")}`);
14359
+ }
14360
+ }
14361
+ const logoUrl = profile.logoUrl;
14362
+ let logoDesc = null;
14363
+ if (logoUrl) {
14364
+ const brandName = profile.name || "brand";
14365
+ logoDesc = `Include a small "${brandName}" logo watermark in the bottom-right corner`;
14366
+ parts.push(logoDesc);
14367
+ }
14368
+ const voice = profile.voiceProfile;
14369
+ if (voice?.tone && Array.isArray(voice.tone) && voice.tone.length > 0) {
14370
+ parts.push(`Visual mood: ${voice.tone.slice(0, 3).join(", ")}`);
14371
+ }
14372
+ if (parts.length === 0) return null;
14373
+ return {
14374
+ stylePrefix: parts.join(". "),
14375
+ brandName: profile.name || null,
14376
+ logoDescription: logoDesc
14377
+ };
14378
+ }
14379
+ function registerCarouselTools(server2) {
14380
+ server2.tool(
14381
+ "create_carousel",
14382
+ "End-to-end carousel creation: generates slide text + kicks off image generation for each slide in parallel. When brand_id is provided, auto-injects brand colors, logo watermark, and visual mood into every image prompt. Returns carousel data + image job_ids. Poll each job_id with check_status until complete, then call schedule_post with job_ids to publish as Instagram carousel (media_type=CAROUSEL_ALBUM).",
14383
+ {
14384
+ topic: z27.string().max(200).describe(
14385
+ 'Carousel topic/hook \u2014 be specific. Example: "5 pricing mistakes that kill SaaS startups" beats "SaaS tips".'
14386
+ ),
14387
+ image_model: z27.enum([
14388
+ "midjourney",
14389
+ "nano-banana",
14390
+ "nano-banana-pro",
14391
+ "flux-pro",
14392
+ "flux-max",
14393
+ "gpt4o-image",
14394
+ "imagen4",
14395
+ "imagen4-fast",
14396
+ "seedream"
14397
+ ]).describe(
14398
+ "Image model for slide visuals. flux-pro for general purpose, imagen4 for photorealistic, midjourney for artistic."
14399
+ ),
14400
+ template_id: z27.enum([
14401
+ "educational-series",
14402
+ "product-showcase",
14403
+ "story-arc",
14404
+ "before-after",
14405
+ "step-by-step",
14406
+ "quote-collection",
14407
+ "data-stats",
14408
+ "myth-vs-reality",
14409
+ "hormozi-authority"
14410
+ ]).optional().describe("Carousel template. Default: hormozi-authority."),
14411
+ slide_count: z27.number().min(3).max(10).optional().describe("Number of slides (3-10). Default: 7."),
14412
+ aspect_ratio: z27.enum(["1:1", "4:5", "9:16"]).optional().describe("Aspect ratio for both carousel and images. Default: 1:1."),
14413
+ style: z27.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe("Visual style. Default: hormozi for hormozi-authority template."),
14414
+ image_style_suffix: z27.string().max(500).optional().describe(
14415
+ 'Style suffix appended to every image prompt for visual consistency across slides. Example: "dark moody lighting, cinematic, 35mm film grain".'
14416
+ ),
14417
+ project_id: z27.string().optional().describe("Project ID to associate the carousel with."),
14418
+ response_format: z27.enum(["text", "json"]).optional().describe("Response format. Default: text.")
14419
+ },
14420
+ async ({
14421
+ topic,
14422
+ image_model,
14423
+ template_id,
14424
+ slide_count,
14425
+ aspect_ratio,
14426
+ style,
14427
+ image_style_suffix,
14428
+ brand_id,
14429
+ project_id,
14430
+ response_format
14431
+ }) => {
14432
+ const format = response_format ?? "text";
14433
+ const startedAt = Date.now();
14434
+ const templateId = template_id ?? "hormozi-authority";
14435
+ const resolvedStyle = style ?? (templateId === "hormozi-authority" ? "hormozi" : "professional");
14436
+ const slideCount = slide_count ?? 7;
14437
+ const ratio = aspect_ratio ?? "1:1";
14438
+ let brandContext = null;
14439
+ const brandProjectId = brand_id || project_id || await getDefaultProjectId();
14440
+ if (brandProjectId) {
14441
+ brandContext = await fetchBrandVisualContext(brandProjectId);
14442
+ }
14443
+ const carouselTextCost = 10 + slideCount * 2;
14444
+ const perImageCost = IMAGE_CREDIT_ESTIMATES2[image_model] ?? 30;
14445
+ const totalEstimatedCost = carouselTextCost + slideCount * perImageCost;
14446
+ const budgetCheck = checkCreditBudget2(totalEstimatedCost);
14447
+ if (!budgetCheck.ok) {
14448
+ await logMcpToolInvocation({
14449
+ toolName: "create_carousel",
14450
+ status: "error",
14451
+ durationMs: Date.now() - startedAt,
14452
+ details: { error: budgetCheck.message, totalEstimatedCost }
14453
+ });
14454
+ return {
14455
+ content: [{ type: "text", text: budgetCheck.message }],
14456
+ isError: true
14457
+ };
14458
+ }
14459
+ const assetBudget = checkAssetBudget2();
14460
+ if (!assetBudget.ok) {
14461
+ await logMcpToolInvocation({
14462
+ toolName: "create_carousel",
14463
+ status: "error",
14464
+ durationMs: Date.now() - startedAt,
14465
+ details: { error: assetBudget.message }
14466
+ });
14467
+ return {
14468
+ content: [{ type: "text", text: assetBudget.message }],
14469
+ isError: true
14470
+ };
14471
+ }
14472
+ const userId = await getDefaultUserId();
14473
+ const rateLimit = checkRateLimit("posting", `create_carousel:${userId}`);
14474
+ if (!rateLimit.allowed) {
14475
+ await logMcpToolInvocation({
14476
+ toolName: "create_carousel",
14477
+ status: "rate_limited",
14478
+ durationMs: Date.now() - startedAt,
14479
+ details: { retryAfter: rateLimit.retryAfter }
14480
+ });
14481
+ return {
14482
+ content: [
14483
+ {
14484
+ type: "text",
14485
+ text: `Rate limit exceeded. Retry in ~${rateLimit.retryAfter}s.`
14486
+ }
14487
+ ],
14488
+ isError: true
14489
+ };
14490
+ }
14491
+ const { data: carouselData, error: carouselError } = await callEdgeFunction(
14492
+ "generate-carousel",
14493
+ {
14494
+ topic,
14495
+ templateId,
14496
+ slideCount,
14497
+ aspectRatio: ratio,
14498
+ style: resolvedStyle,
14499
+ projectId: project_id
14500
+ },
14501
+ { timeoutMs: 6e4 }
14502
+ );
14503
+ if (carouselError || !carouselData?.carousel) {
14504
+ const errMsg = carouselError ?? "No carousel data returned";
14505
+ await logMcpToolInvocation({
14506
+ toolName: "create_carousel",
14507
+ status: "error",
14508
+ durationMs: Date.now() - startedAt,
14509
+ details: { phase: "text_generation", error: errMsg }
14510
+ });
14511
+ return {
14512
+ content: [{ type: "text", text: `Carousel text generation failed: ${errMsg}` }],
14513
+ isError: true
14514
+ };
14515
+ }
14516
+ const carousel = carouselData.carousel;
14517
+ const textCredits = carousel.credits?.used ?? carouselTextCost;
14518
+ addCreditsUsed2(textCredits);
14519
+ const imageJobs = await Promise.all(
14520
+ carousel.slides.map(async (slide) => {
14521
+ const promptParts = [];
14522
+ if (brandContext) promptParts.push(brandContext.stylePrefix);
14523
+ if (slide.headline) promptParts.push(slide.headline);
14524
+ if (slide.body) promptParts.push(slide.body);
14525
+ if (promptParts.length === 0) promptParts.push(topic);
14526
+ if (image_style_suffix) promptParts.push(image_style_suffix);
14527
+ const imagePrompt = promptParts.join(". ");
14528
+ try {
14529
+ const { data, error } = await callEdgeFunction(
14530
+ "kie-image-generate",
14531
+ {
14532
+ prompt: imagePrompt,
14533
+ model: image_model,
14534
+ aspectRatio: ratio
14535
+ },
14536
+ { timeoutMs: 3e4 }
14537
+ );
14538
+ if (error || !data?.taskId && !data?.asyncJobId) {
14539
+ return {
14540
+ slideNumber: slide.slideNumber,
14541
+ jobId: null,
14542
+ model: image_model,
14543
+ error: error ?? "No job ID returned"
14544
+ };
14545
+ }
14546
+ const jobId = data.asyncJobId ?? data.taskId ?? null;
14547
+ if (jobId) {
14548
+ addCreditsUsed2(perImageCost);
14549
+ addAssetsGenerated2(1);
14550
+ }
14551
+ return {
14552
+ slideNumber: slide.slideNumber,
14553
+ jobId,
14554
+ model: image_model,
14555
+ error: null
14556
+ };
14557
+ } catch (err) {
14558
+ return {
14559
+ slideNumber: slide.slideNumber,
14560
+ jobId: null,
14561
+ model: image_model,
14562
+ error: sanitizeError(err)
14563
+ };
14564
+ }
14565
+ })
14566
+ );
14567
+ const successfulJobs = imageJobs.filter((j) => j.jobId !== null);
14568
+ const failedJobs = imageJobs.filter((j) => j.jobId === null);
14569
+ await logMcpToolInvocation({
14570
+ toolName: "create_carousel",
14571
+ status: failedJobs.length === imageJobs.length ? "error" : "success",
14572
+ durationMs: Date.now() - startedAt,
14573
+ details: {
14574
+ carouselId: carousel.id,
14575
+ templateId,
14576
+ slideCount: carousel.slides.length,
14577
+ imagesStarted: successfulJobs.length,
14578
+ imagesFailed: failedJobs.length,
14579
+ imageModel: image_model,
14580
+ creditsUsed: getCreditsUsed2()
14581
+ }
14582
+ });
14583
+ if (format === "json") {
14584
+ return {
14585
+ content: [
14586
+ {
14587
+ type: "text",
14588
+ text: JSON.stringify(
14589
+ {
14590
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
14591
+ data: {
14592
+ carouselId: carousel.id,
14593
+ templateId,
14594
+ style: resolvedStyle,
14595
+ slideCount: carousel.slides.length,
14596
+ slides: carousel.slides.map((s) => {
14597
+ const job = imageJobs.find((j) => j.slideNumber === s.slideNumber);
14598
+ return {
14599
+ ...s,
14600
+ imageJobId: job?.jobId ?? null,
14601
+ imageError: job?.error ?? null
14602
+ };
14603
+ }),
14604
+ imageModel: image_model,
14605
+ brandApplied: brandContext ? {
14606
+ brandName: brandContext.brandName,
14607
+ hasLogo: !!brandContext.logoDescription,
14608
+ stylePrefix: brandContext.stylePrefix
14609
+ } : null,
14610
+ jobIds: successfulJobs.map((j) => j.jobId),
14611
+ failedSlides: failedJobs.map((j) => ({
14612
+ slideNumber: j.slideNumber,
14613
+ error: j.error
14614
+ })),
14615
+ credits: {
14616
+ textGeneration: textCredits,
14617
+ imagesEstimated: successfulJobs.length * perImageCost,
14618
+ totalEstimated: textCredits + successfulJobs.length * perImageCost
14619
+ }
14620
+ }
14621
+ },
14622
+ null,
14623
+ 2
14624
+ )
14625
+ }
14626
+ ]
14627
+ };
14628
+ }
14629
+ const lines = [
14630
+ `Carousel created: ${carousel.slides.length} slides + ${successfulJobs.length} image jobs started.`,
14631
+ ` Carousel ID: ${carousel.id}`,
14632
+ ` Template: ${templateId} | Style: ${resolvedStyle}`,
14633
+ ` Image model: ${image_model}`,
14634
+ ` Credits: ~${textCredits + successfulJobs.length * perImageCost} (${textCredits} text + ${successfulJobs.length * perImageCost} images)`
14635
+ ];
14636
+ if (brandContext) {
14637
+ lines.push(
14638
+ ` Brand: ${brandContext.brandName || "unnamed"}${brandContext.logoDescription ? " (logo overlay via prompt)" : ""}`
14639
+ );
14640
+ }
14641
+ lines.push("", "Slides:");
14642
+ for (const slide of carousel.slides) {
14643
+ const job = imageJobs.find((j) => j.slideNumber === slide.slideNumber);
14644
+ const status = job?.jobId ? `image: ${job.jobId}` : `image FAILED: ${job?.error}`;
14645
+ lines.push(` ${slide.slideNumber}. ${slide.headline || "(no headline)"} [${status}]`);
14646
+ }
14647
+ if (failedJobs.length > 0) {
14648
+ lines.push("");
14649
+ lines.push(
14650
+ `WARNING: ${failedJobs.length}/${imageJobs.length} image generations failed. Use generate_image manually for failed slides.`
14651
+ );
14652
+ }
14653
+ const jobIdList = successfulJobs.map((j) => j.jobId).join(", ");
14654
+ lines.push("");
14655
+ lines.push("Next steps:");
14656
+ lines.push(` 1. Poll each job: check_status with job_id for each of: ${jobIdList}`);
14657
+ lines.push(
14658
+ " 2. When all complete: schedule_post with job_ids=[...] and media_type=CAROUSEL_ALBUM"
14659
+ );
14660
+ return {
14661
+ content: [{ type: "text", text: lines.join("\n") }]
14662
+ };
14663
+ }
14664
+ );
14665
+ }
14666
+
13463
14667
  // src/lib/register-tools.ts
13464
14668
  function applyScopeEnforcement(server2, scopeResolver) {
13465
14669
  const originalTool = server2.tool.bind(server2);
@@ -13554,6 +14758,7 @@ function registerAllTools(server2, options) {
13554
14758
  registerLoopSummaryTools(server2);
13555
14759
  registerUsageTools(server2);
13556
14760
  registerAutopilotTools(server2);
14761
+ registerRecipeTools(server2);
13557
14762
  registerExtractionTools(server2);
13558
14763
  registerQualityTools(server2);
13559
14764
  registerPlanningTools(server2);
@@ -13563,21 +14768,22 @@ function registerAllTools(server2, options) {
13563
14768
  registerSuggestTools(server2);
13564
14769
  registerDigestTools(server2);
13565
14770
  registerBrandRuntimeTools(server2);
14771
+ registerCarouselTools(server2);
13566
14772
  applyAnnotations(server2);
13567
14773
  }
13568
14774
 
13569
14775
  // src/prompts.ts
13570
- import { z as z26 } from "zod";
14776
+ import { z as z28 } from "zod";
13571
14777
  function registerPrompts(server2) {
13572
14778
  server2.prompt(
13573
14779
  "create_weekly_content_plan",
13574
14780
  "Generate a full week of social media content (7 days, multiple platforms). Returns a structured plan with topics, formats, and posting times.",
13575
14781
  {
13576
- niche: z26.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
13577
- platforms: z26.string().optional().describe(
14782
+ niche: z28.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
14783
+ platforms: z28.string().optional().describe(
13578
14784
  'Comma-separated platforms to target (default: "YouTube, Instagram, TikTok, LinkedIn")'
13579
14785
  ),
13580
- tone: z26.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
14786
+ tone: z28.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
13581
14787
  },
13582
14788
  ({ niche, platforms, tone }) => {
13583
14789
  const targetPlatforms = platforms || "YouTube, Instagram, TikTok, LinkedIn";
@@ -13619,8 +14825,8 @@ After building the plan, use \`save_content_plan\` to save it.`
13619
14825
  "analyze_top_content",
13620
14826
  "Analyze your best-performing posts to identify patterns and replicate success. Returns insights on hooks, formats, timing, and topics that resonate.",
13621
14827
  {
13622
- timeframe: z26.string().optional().describe('Analysis period (default: "30 days"). E.g., "7 days", "90 days"'),
13623
- platform: z26.string().optional().describe('Filter to a specific platform (e.g., "youtube", "instagram")')
14828
+ timeframe: z28.string().optional().describe('Analysis period (default: "30 days"). E.g., "7 days", "90 days"'),
14829
+ platform: z28.string().optional().describe('Filter to a specific platform (e.g., "youtube", "instagram")')
13624
14830
  },
13625
14831
  ({ timeframe, platform: platform3 }) => {
13626
14832
  const period = timeframe || "30 days";
@@ -13657,10 +14863,10 @@ Format as a clear, actionable performance report.`
13657
14863
  "repurpose_content",
13658
14864
  "Take one piece of content and transform it into 8-10 pieces across multiple platforms and formats.",
13659
14865
  {
13660
- source: z26.string().describe(
14866
+ source: z28.string().describe(
13661
14867
  "The source content to repurpose \u2014 a URL, transcript, or the content text itself"
13662
14868
  ),
13663
- target_platforms: z26.string().optional().describe(
14869
+ target_platforms: z28.string().optional().describe(
13664
14870
  'Comma-separated target platforms (default: "Twitter, LinkedIn, Instagram, TikTok, YouTube")'
13665
14871
  )
13666
14872
  },
@@ -13702,9 +14908,9 @@ For each piece, include the platform, format, character count, and suggested pos
13702
14908
  "setup_brand_voice",
13703
14909
  "Define or refine your brand voice profile so all generated content stays on-brand. Walks through tone, audience, values, and style.",
13704
14910
  {
13705
- brand_name: z26.string().describe("Your brand or business name"),
13706
- industry: z26.string().optional().describe('Your industry or niche (e.g., "B2B SaaS", "fitness coaching")'),
13707
- website: z26.string().optional().describe("Your website URL for context")
14911
+ brand_name: z28.string().describe("Your brand or business name"),
14912
+ industry: z28.string().optional().describe('Your industry or niche (e.g., "B2B SaaS", "fitness coaching")'),
14913
+ website: z28.string().optional().describe("Your website URL for context")
13708
14914
  },
13709
14915
  ({ brand_name, industry, website }) => {
13710
14916
  const industryContext = industry ? ` in the ${industry} space` : "";