@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/CHANGELOG.md +30 -0
- package/README.md +6 -4
- package/dist/http.js +449 -193
- package/dist/index.js +447 -191
- package/package.json +17 -4
- package/tools.lock.json +88 -0
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.
|
|
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
|
|
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.
|
|
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.
|
|
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: ${
|
|
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 =
|
|
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 =
|
|
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
|
|
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").
|
|
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
|
|
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
|
-
|
|
6995
|
-
|
|
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(
|
|
7403
|
+
const ct = content_type || inferContentType(src);
|
|
6999
7404
|
uploadBody = {
|
|
7000
|
-
url:
|
|
7405
|
+
url: src,
|
|
7001
7406
|
contentType: ct,
|
|
7002
|
-
fileName: basename(new URL(
|
|
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(
|
|
7009
|
-
} catch
|
|
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:
|
|
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: ${
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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: ${
|
|
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}: ${
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
14562
|
+
error: sanitizeError(err)
|
|
14307
14563
|
};
|
|
14308
14564
|
}
|
|
14309
14565
|
})
|