@socialneuron/mcp-server 1.7.4 → 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.4";
17
+ MCP_VERSION = "1.7.5";
18
18
  }
19
19
  });
20
20
 
@@ -5747,7 +5747,7 @@ var ERROR_PATTERNS = [
5747
5747
  // Generic sensitive patterns (API keys, URLs with secrets)
5748
5748
  [/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i, "An internal error occurred. Please try again."]
5749
5749
  ];
5750
- function sanitizeError2(error) {
5750
+ function sanitizeError(error) {
5751
5751
  const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
5752
5752
  if (process.env.NODE_ENV !== "production") {
5753
5753
  console.error("[Error]", msg);
@@ -5760,6 +5760,171 @@ function sanitizeError2(error) {
5760
5760
  return "An unexpected error occurred. Please try again.";
5761
5761
  }
5762
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
+
5763
5928
  // src/tools/distribution.ts
5764
5929
  init_supabase();
5765
5930
  init_quality();
@@ -5799,16 +5964,42 @@ function asEnvelope2(data) {
5799
5964
  data
5800
5965
  };
5801
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
+ }
5802
5993
  function registerDistributionTools(server2) {
5803
5994
  server2.tool(
5804
5995
  "schedule_post",
5805
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.',
5806
5997
  {
5807
5998
  media_url: z3.string().optional().describe(
5808
- "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."
5809
6000
  ),
5810
6001
  media_urls: z3.array(z3.string()).optional().describe(
5811
- "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."
5812
6003
  ),
5813
6004
  r2_key: z3.string().optional().describe(
5814
6005
  "R2 object key from upload_media. Signed on demand at post time \u2014 survives scheduling delays. Alternative to media_url."
@@ -5897,7 +6088,10 @@ function registerDistributionTools(server2) {
5897
6088
  ),
5898
6089
  project_id: z3.string().optional().describe("Social Neuron project ID to associate this post with."),
5899
6090
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
5900
- 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
+ )
5901
6095
  },
5902
6096
  async ({
5903
6097
  media_url,
@@ -5911,6 +6105,7 @@ function registerDistributionTools(server2) {
5911
6105
  project_id,
5912
6106
  response_format,
5913
6107
  attribution,
6108
+ auto_rehost,
5914
6109
  r2_key,
5915
6110
  r2_keys,
5916
6111
  job_id,
@@ -6022,12 +6217,54 @@ function registerDistributionTools(server2) {
6022
6217
  }
6023
6218
  resolvedMediaUrls = resolved;
6024
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
+ }
6025
6262
  } catch (resolveErr) {
6026
6263
  return {
6027
6264
  content: [
6028
6265
  {
6029
6266
  type: "text",
6030
- text: `Failed to resolve media: ${sanitizeError2(resolveErr)}`
6267
+ text: `Failed to resolve media: ${sanitizeError(resolveErr)}`
6031
6268
  }
6032
6269
  ],
6033
6270
  isError: true
@@ -6392,7 +6629,7 @@ Created with Social Neuron`;
6392
6629
  return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
6393
6630
  } catch (err) {
6394
6631
  const durationMs = Date.now() - startedAt;
6395
- const message = sanitizeError2(err);
6632
+ const message = sanitizeError(err);
6396
6633
  logMcpToolInvocation({
6397
6634
  toolName: "find_next_slots",
6398
6635
  status: "error",
@@ -6912,7 +7149,7 @@ Created with Social Neuron`;
6912
7149
  };
6913
7150
  } catch (err) {
6914
7151
  const durationMs = Date.now() - startedAt;
6915
- const message = sanitizeError2(err);
7152
+ const message = sanitizeError(err);
6916
7153
  logMcpToolInvocation({
6917
7154
  toolName: "schedule_content_plan",
6918
7155
  status: "error",
@@ -6935,6 +7172,19 @@ import { readFile } from "node:fs/promises";
6935
7172
  import { basename, extname } from "node:path";
6936
7173
  init_supabase();
6937
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,/;
6938
7188
  function maskR2Key(key) {
6939
7189
  const segments = key.split("/");
6940
7190
  return segments.length >= 3 ? `\u2026/${segments.slice(-2).join("/")}` : key;
@@ -6955,21 +7205,35 @@ function inferContentType(filePath) {
6955
7205
  };
6956
7206
  return map[ext] || "application/octet-stream";
6957
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
+ }
6958
7216
  function registerMediaTools(server2) {
6959
7217
  server2.tool(
6960
7218
  "upload_media",
6961
- "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.",
6962
7220
  {
6963
- source: z4.string().describe(
6964
- '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.'
6965
7229
  ),
6966
7230
  content_type: z4.string().optional().describe(
6967
- '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.'
6968
7232
  ),
6969
7233
  project_id: z4.string().optional().describe("Project ID for R2 path organization."),
6970
7234
  response_format: z4.enum(["text", "json"]).optional().describe("Response format. Default: text.")
6971
7235
  },
6972
- async ({ source, content_type, project_id, response_format }) => {
7236
+ async ({ source, file_data, file_name, content_type, project_id, response_format }) => {
6973
7237
  const format = response_format ?? "text";
6974
7238
  const startedAt = Date.now();
6975
7239
  const userId = await getDefaultUserId();
@@ -6991,46 +7255,187 @@ function registerMediaTools(server2) {
6991
7255
  isError: true
6992
7256
  };
6993
7257
  }
6994
- const isUrl = source.startsWith("http://") || source.startsWith("https://");
6995
- const isLocalFile = !isUrl;
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
7267
+ };
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) {
7279
+ return {
7280
+ content: [
7281
+ {
7282
+ type: "text",
7283
+ text: "content_type is required when file_data has no data: prefix."
7284
+ }
7285
+ ],
7286
+ isError: true
7287
+ };
7288
+ }
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://");
6996
7401
  let uploadBody;
6997
7402
  if (isUrl) {
6998
- const ct = content_type || inferContentType(source);
7403
+ const ct = content_type || inferContentType(src);
6999
7404
  uploadBody = {
7000
- url: source,
7405
+ url: src,
7001
7406
  contentType: ct,
7002
- fileName: basename(new URL(source).pathname) || "upload",
7407
+ fileName: basename(file_name ?? new URL(src).pathname) || "upload",
7003
7408
  projectId: project_id
7004
7409
  };
7005
7410
  } else {
7006
7411
  let fileBuffer;
7007
7412
  try {
7008
- fileBuffer = await readFile(source);
7009
- } catch (err) {
7413
+ fileBuffer = await readFile(src);
7414
+ } catch {
7010
7415
  await logMcpToolInvocation({
7011
7416
  toolName: "upload_media",
7012
7417
  status: "error",
7013
7418
  durationMs: Date.now() - startedAt,
7014
- details: { error: `File not found: ${source}` }
7419
+ details: { error: "File not found", source: "local" }
7015
7420
  });
7016
7421
  return {
7017
7422
  content: [
7018
7423
  {
7019
7424
  type: "text",
7020
- text: `File not found or not readable: ${source}`
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.`
7021
7426
  }
7022
7427
  ],
7023
7428
  isError: true
7024
7429
  };
7025
7430
  }
7026
- const ct = content_type || inferContentType(source);
7431
+ const ct = content_type || inferContentType(src);
7027
7432
  if (fileBuffer.length > MAX_BASE64_SIZE) {
7028
7433
  const { data: putData, error: putError } = await callEdgeFunction(
7029
7434
  "get-signed-url",
7030
7435
  {
7031
7436
  operation: "put",
7032
7437
  contentType: ct,
7033
- filename: basename(source),
7438
+ filename: basename(file_name ?? src),
7034
7439
  projectId: project_id
7035
7440
  },
7036
7441
  { timeoutMs: 1e4 }
@@ -7132,7 +7537,7 @@ function registerMediaTools(server2) {
7132
7537
  uploadBody = {
7133
7538
  fileData: base64,
7134
7539
  contentType: ct,
7135
- fileName: basename(source),
7540
+ fileName: basename(file_name ?? src),
7136
7541
  projectId: project_id
7137
7542
  };
7138
7543
  }
@@ -7524,155 +7929,6 @@ function formatAnalytics(summary, days, format) {
7524
7929
  init_edge_function();
7525
7930
  init_supabase();
7526
7931
  import { z as z6 } from "zod";
7527
-
7528
- // src/lib/ssrf.ts
7529
- var BLOCKED_IP_PATTERNS = [
7530
- // IPv4 localhost/loopback
7531
- /^127\./,
7532
- /^0\./,
7533
- // IPv4 private ranges (RFC 1918)
7534
- /^10\./,
7535
- /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
7536
- /^192\.168\./,
7537
- // IPv4 link-local
7538
- /^169\.254\./,
7539
- // Cloud metadata endpoint (AWS, GCP, Azure)
7540
- /^169\.254\.169\.254$/,
7541
- // IPv4 broadcast
7542
- /^255\./,
7543
- // Shared address space (RFC 6598)
7544
- /^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./
7545
- ];
7546
- var BLOCKED_IPV6_PATTERNS = [
7547
- /^::1$/i,
7548
- // loopback
7549
- /^::$/i,
7550
- // unspecified
7551
- /^fe[89ab][0-9a-f]:/i,
7552
- // link-local fe80::/10
7553
- /^fc[0-9a-f]:/i,
7554
- // unique local fc00::/7
7555
- /^fd[0-9a-f]:/i,
7556
- // unique local fc00::/7
7557
- /^::ffff:127\./i,
7558
- // IPv4-mapped localhost
7559
- /^::ffff:(0|10|127|169\.254|172\.(1[6-9]|2[0-9]|3[0-1])|192\.168)\./i
7560
- // IPv4-mapped private
7561
- ];
7562
- var BLOCKED_HOSTNAMES = [
7563
- "localhost",
7564
- "localhost.localdomain",
7565
- "local",
7566
- "127.0.0.1",
7567
- "0.0.0.0",
7568
- "[::1]",
7569
- "[::ffff:127.0.0.1]",
7570
- // Cloud metadata endpoints
7571
- "metadata.google.internal",
7572
- "metadata.goog",
7573
- "instance-data",
7574
- "instance-data.ec2.internal"
7575
- ];
7576
- var ALLOWED_PROTOCOLS = ["http:", "https:"];
7577
- var BLOCKED_PORTS = [22, 23, 25, 110, 143, 445, 3306, 5432, 6379, 27017, 11211];
7578
- function isBlockedIP(ip) {
7579
- const normalized = ip.replace(/^\[/, "").replace(/\]$/, "");
7580
- if (normalized.includes(":")) {
7581
- return BLOCKED_IPV6_PATTERNS.some((pattern) => pattern.test(normalized));
7582
- }
7583
- return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(normalized));
7584
- }
7585
- function isBlockedHostname(hostname) {
7586
- return BLOCKED_HOSTNAMES.includes(hostname.toLowerCase());
7587
- }
7588
- function isIPAddress(hostname) {
7589
- const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
7590
- const ipv6Pattern = /^\[?[a-fA-F0-9:]+\]?$/;
7591
- return ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname);
7592
- }
7593
- async function validateUrlForSSRF(urlString) {
7594
- try {
7595
- const url = new URL(urlString);
7596
- if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
7597
- return {
7598
- isValid: false,
7599
- error: `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are allowed.`
7600
- };
7601
- }
7602
- if (url.username || url.password) {
7603
- return {
7604
- isValid: false,
7605
- error: "URLs with embedded credentials are not allowed."
7606
- };
7607
- }
7608
- const hostname = url.hostname.toLowerCase();
7609
- if (isBlockedHostname(hostname)) {
7610
- return {
7611
- isValid: false,
7612
- error: "Access to internal/localhost addresses is not allowed."
7613
- };
7614
- }
7615
- if (isIPAddress(hostname) && isBlockedIP(hostname)) {
7616
- return {
7617
- isValid: false,
7618
- error: "Access to private/internal IP addresses is not allowed."
7619
- };
7620
- }
7621
- const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80;
7622
- if (BLOCKED_PORTS.includes(port)) {
7623
- return {
7624
- isValid: false,
7625
- error: `Access to port ${port} is not allowed.`
7626
- };
7627
- }
7628
- let resolvedIP;
7629
- if (!isIPAddress(hostname)) {
7630
- try {
7631
- const dns = await import("node:dns");
7632
- const resolver = new dns.promises.Resolver();
7633
- const resolvedIPs = [];
7634
- try {
7635
- const aRecords = await resolver.resolve4(hostname);
7636
- resolvedIPs.push(...aRecords);
7637
- } catch {
7638
- }
7639
- try {
7640
- const aaaaRecords = await resolver.resolve6(hostname);
7641
- resolvedIPs.push(...aaaaRecords);
7642
- } catch {
7643
- }
7644
- if (resolvedIPs.length === 0) {
7645
- return {
7646
- isValid: false,
7647
- error: "DNS resolution failed: hostname did not resolve to any address."
7648
- };
7649
- }
7650
- for (const ip of resolvedIPs) {
7651
- if (isBlockedIP(ip)) {
7652
- return {
7653
- isValid: false,
7654
- error: "Hostname resolves to a private/internal IP address."
7655
- };
7656
- }
7657
- }
7658
- resolvedIP = resolvedIPs[0];
7659
- } catch {
7660
- return {
7661
- isValid: false,
7662
- error: "DNS resolution failed. Cannot verify hostname safety."
7663
- };
7664
- }
7665
- }
7666
- return { isValid: true, sanitizedUrl: url.toString(), resolvedIP };
7667
- } catch (error) {
7668
- return {
7669
- isValid: false,
7670
- error: `Invalid URL format: ${error instanceof Error ? error.message : "Unknown error"}`
7671
- };
7672
- }
7673
- }
7674
-
7675
- // src/tools/brand.ts
7676
7932
  init_version();
7677
7933
  function asEnvelope4(data) {
7678
7934
  return {
@@ -8196,7 +8452,7 @@ function registerScreenshotTools(server2) {
8196
8452
  };
8197
8453
  } catch (err) {
8198
8454
  await closeBrowser();
8199
- const message = sanitizeError2(err);
8455
+ const message = sanitizeError(err);
8200
8456
  await logMcpToolInvocation({
8201
8457
  toolName: "capture_app_page",
8202
8458
  status: "error",
@@ -8352,7 +8608,7 @@ function registerScreenshotTools(server2) {
8352
8608
  };
8353
8609
  } catch (err) {
8354
8610
  await closeBrowser();
8355
- const message = sanitizeError2(err);
8611
+ const message = sanitizeError(err);
8356
8612
  await logMcpToolInvocation({
8357
8613
  toolName: "capture_screenshot",
8358
8614
  status: "error",
@@ -8646,7 +8902,7 @@ function registerRemotionTools(server2) {
8646
8902
  ]
8647
8903
  };
8648
8904
  } catch (err) {
8649
- const message = sanitizeError2(err);
8905
+ const message = sanitizeError(err);
8650
8906
  await logMcpToolInvocation({
8651
8907
  toolName: "render_demo_video",
8652
8908
  status: "error",
@@ -8774,7 +9030,7 @@ function registerRemotionTools(server2) {
8774
9030
  ]
8775
9031
  };
8776
9032
  } catch (err) {
8777
- const message = sanitizeError2(err);
9033
+ const message = sanitizeError(err);
8778
9034
  await logMcpToolInvocation({
8779
9035
  toolName: "render_template_video",
8780
9036
  status: "error",
@@ -10711,7 +10967,7 @@ function registerExtractionTools(server2) {
10711
10967
  };
10712
10968
  } catch (err) {
10713
10969
  const durationMs = Date.now() - startedAt;
10714
- const message = sanitizeError2(err);
10970
+ const message = sanitizeError(err);
10715
10971
  logMcpToolInvocation({
10716
10972
  toolName: "extract_url_content",
10717
10973
  status: "error",
@@ -11271,7 +11527,7 @@ ${rawText.slice(0, 1e3)}`
11271
11527
  }
11272
11528
  } catch (persistErr) {
11273
11529
  const durationMs2 = Date.now() - startedAt;
11274
- const message = sanitizeError2(persistErr);
11530
+ const message = sanitizeError(persistErr);
11275
11531
  logMcpToolInvocation({
11276
11532
  toolName: "plan_content_week",
11277
11533
  status: "error",
@@ -11303,7 +11559,7 @@ ${rawText.slice(0, 1e3)}`
11303
11559
  };
11304
11560
  } catch (err) {
11305
11561
  const durationMs = Date.now() - startedAt;
11306
- const message = sanitizeError2(err);
11562
+ const message = sanitizeError(err);
11307
11563
  logMcpToolInvocation({
11308
11564
  toolName: "plan_content_week",
11309
11565
  status: "error",
@@ -11395,7 +11651,7 @@ ${rawText.slice(0, 1e3)}`
11395
11651
  };
11396
11652
  } catch (err) {
11397
11653
  const durationMs = Date.now() - startedAt;
11398
- const message = sanitizeError2(err);
11654
+ const message = sanitizeError(err);
11399
11655
  logMcpToolInvocation({
11400
11656
  toolName: "save_content_plan",
11401
11657
  status: "error",
@@ -12007,7 +12263,7 @@ function registerPipelineTools(server2) {
12007
12263
  return { content: [{ type: "text", text: lines.join("\n") }] };
12008
12264
  } catch (err) {
12009
12265
  const durationMs = Date.now() - startedAt;
12010
- const message = sanitizeError2(err);
12266
+ const message = sanitizeError(err);
12011
12267
  logMcpToolInvocation({
12012
12268
  toolName: "check_pipeline_readiness",
12013
12269
  status: "error",
@@ -12168,7 +12424,7 @@ function registerPipelineTools(server2) {
12168
12424
  } catch (deductErr) {
12169
12425
  errors.push({
12170
12426
  stage: "planning",
12171
- message: `Credit deduction failed: ${sanitizeError2(deductErr)}`
12427
+ message: `Credit deduction failed: ${sanitizeError(deductErr)}`
12172
12428
  });
12173
12429
  }
12174
12430
  }
@@ -12339,7 +12595,7 @@ function registerPipelineTools(server2) {
12339
12595
  } catch (schedErr) {
12340
12596
  errors.push({
12341
12597
  stage: "schedule",
12342
- message: `Failed to schedule ${post.id}: ${sanitizeError2(schedErr)}`
12598
+ message: `Failed to schedule ${post.id}: ${sanitizeError(schedErr)}`
12343
12599
  });
12344
12600
  }
12345
12601
  }
@@ -12428,7 +12684,7 @@ function registerPipelineTools(server2) {
12428
12684
  return { content: [{ type: "text", text: lines.join("\n") }] };
12429
12685
  } catch (err) {
12430
12686
  const durationMs = Date.now() - startedAt;
12431
- const message = sanitizeError2(err);
12687
+ const message = sanitizeError(err);
12432
12688
  logMcpToolInvocation({
12433
12689
  toolName: "run_content_pipeline",
12434
12690
  status: "error",
@@ -12652,7 +12908,7 @@ function registerPipelineTools(server2) {
12652
12908
  return { content: [{ type: "text", text: lines.join("\n") }] };
12653
12909
  } catch (err) {
12654
12910
  const durationMs = Date.now() - startedAt;
12655
- const message = sanitizeError2(err);
12911
+ const message = sanitizeError(err);
12656
12912
  logMcpToolInvocation({
12657
12913
  toolName: "auto_approve_plan",
12658
12914
  status: "error",
@@ -12832,7 +13088,7 @@ ${i + 1}. ${s.topic}`);
12832
13088
  return { content: [{ type: "text", text: lines.join("\n") }] };
12833
13089
  } catch (err) {
12834
13090
  const durationMs = Date.now() - startedAt;
12835
- const message = sanitizeError2(err);
13091
+ const message = sanitizeError(err);
12836
13092
  logMcpToolInvocation({
12837
13093
  toolName: "suggest_next_content",
12838
13094
  status: "error",
@@ -13173,7 +13429,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
13173
13429
  return { content: [{ type: "text", text: lines.join("\n") }] };
13174
13430
  } catch (err) {
13175
13431
  const durationMs = Date.now() - startedAt;
13176
- const message = sanitizeError2(err);
13432
+ const message = sanitizeError(err);
13177
13433
  logMcpToolInvocation({
13178
13434
  toolName: "generate_performance_digest",
13179
13435
  status: "error",
@@ -13270,7 +13526,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
13270
13526
  return { content: [{ type: "text", text: lines.join("\n") }] };
13271
13527
  } catch (err) {
13272
13528
  const durationMs = Date.now() - startedAt;
13273
- const message = sanitizeError2(err);
13529
+ const message = sanitizeError(err);
13274
13530
  logMcpToolInvocation({
13275
13531
  toolName: "detect_anomalies",
13276
13532
  status: "error",
@@ -14303,7 +14559,7 @@ function registerCarouselTools(server2) {
14303
14559
  slideNumber: slide.slideNumber,
14304
14560
  jobId: null,
14305
14561
  model: image_model,
14306
- error: sanitizeError2(err)
14562
+ error: sanitizeError(err)
14307
14563
  };
14308
14564
  }
14309
14565
  })