@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/http.js
CHANGED
|
@@ -1383,7 +1383,7 @@ init_supabase();
|
|
|
1383
1383
|
init_request_context();
|
|
1384
1384
|
|
|
1385
1385
|
// src/lib/version.ts
|
|
1386
|
-
var MCP_VERSION = "1.7.
|
|
1386
|
+
var MCP_VERSION = "1.7.5";
|
|
1387
1387
|
|
|
1388
1388
|
// src/tools/content.ts
|
|
1389
1389
|
var MAX_CREDITS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
|
|
@@ -2551,7 +2551,7 @@ var ERROR_PATTERNS = [
|
|
|
2551
2551
|
// Generic sensitive patterns (API keys, URLs with secrets)
|
|
2552
2552
|
[/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i, "An internal error occurred. Please try again."]
|
|
2553
2553
|
];
|
|
2554
|
-
function
|
|
2554
|
+
function sanitizeError(error) {
|
|
2555
2555
|
const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
2556
2556
|
if (process.env.NODE_ENV !== "production") {
|
|
2557
2557
|
console.error("[Error]", msg);
|
|
@@ -2564,6 +2564,171 @@ function sanitizeError2(error) {
|
|
|
2564
2564
|
return "An unexpected error occurred. Please try again.";
|
|
2565
2565
|
}
|
|
2566
2566
|
|
|
2567
|
+
// src/lib/ssrf.ts
|
|
2568
|
+
var BLOCKED_IP_PATTERNS = [
|
|
2569
|
+
// IPv4 localhost/loopback
|
|
2570
|
+
/^127\./,
|
|
2571
|
+
/^0\./,
|
|
2572
|
+
// IPv4 private ranges (RFC 1918)
|
|
2573
|
+
/^10\./,
|
|
2574
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
|
2575
|
+
/^192\.168\./,
|
|
2576
|
+
// IPv4 link-local
|
|
2577
|
+
/^169\.254\./,
|
|
2578
|
+
// Cloud metadata endpoint (AWS, GCP, Azure)
|
|
2579
|
+
/^169\.254\.169\.254$/,
|
|
2580
|
+
// IPv4 broadcast
|
|
2581
|
+
/^255\./,
|
|
2582
|
+
// Shared address space (RFC 6598)
|
|
2583
|
+
/^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./
|
|
2584
|
+
];
|
|
2585
|
+
var BLOCKED_IPV6_PATTERNS = [
|
|
2586
|
+
/^::1$/i,
|
|
2587
|
+
// loopback
|
|
2588
|
+
/^::$/i,
|
|
2589
|
+
// unspecified
|
|
2590
|
+
/^fe[89ab][0-9a-f]:/i,
|
|
2591
|
+
// link-local fe80::/10
|
|
2592
|
+
/^fc[0-9a-f]:/i,
|
|
2593
|
+
// unique local fc00::/7
|
|
2594
|
+
/^fd[0-9a-f]:/i,
|
|
2595
|
+
// unique local fc00::/7
|
|
2596
|
+
/^::ffff:127\./i,
|
|
2597
|
+
// IPv4-mapped localhost
|
|
2598
|
+
/^::ffff:(0|10|127|169\.254|172\.(1[6-9]|2[0-9]|3[0-1])|192\.168)\./i
|
|
2599
|
+
// IPv4-mapped private
|
|
2600
|
+
];
|
|
2601
|
+
var BLOCKED_HOSTNAMES = [
|
|
2602
|
+
"localhost",
|
|
2603
|
+
"localhost.localdomain",
|
|
2604
|
+
"local",
|
|
2605
|
+
"127.0.0.1",
|
|
2606
|
+
"0.0.0.0",
|
|
2607
|
+
"[::1]",
|
|
2608
|
+
"[::ffff:127.0.0.1]",
|
|
2609
|
+
// Cloud metadata endpoints
|
|
2610
|
+
"metadata.google.internal",
|
|
2611
|
+
"metadata.goog",
|
|
2612
|
+
"instance-data",
|
|
2613
|
+
"instance-data.ec2.internal"
|
|
2614
|
+
];
|
|
2615
|
+
var ALLOWED_PROTOCOLS = ["http:", "https:"];
|
|
2616
|
+
var BLOCKED_PORTS = [22, 23, 25, 110, 143, 445, 3306, 5432, 6379, 27017, 11211];
|
|
2617
|
+
function isBlockedIP(ip) {
|
|
2618
|
+
const normalized = ip.replace(/^\[/, "").replace(/\]$/, "");
|
|
2619
|
+
if (normalized.includes(":")) {
|
|
2620
|
+
return BLOCKED_IPV6_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
2621
|
+
}
|
|
2622
|
+
return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
2623
|
+
}
|
|
2624
|
+
function isBlockedHostname(hostname) {
|
|
2625
|
+
return BLOCKED_HOSTNAMES.includes(hostname.toLowerCase());
|
|
2626
|
+
}
|
|
2627
|
+
function isIPAddress(hostname) {
|
|
2628
|
+
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
2629
|
+
const ipv6Pattern = /^\[?[a-fA-F0-9:]+\]?$/;
|
|
2630
|
+
return ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname);
|
|
2631
|
+
}
|
|
2632
|
+
async function validateUrlForSSRF(urlString) {
|
|
2633
|
+
try {
|
|
2634
|
+
const url = new URL(urlString);
|
|
2635
|
+
if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
|
|
2636
|
+
return {
|
|
2637
|
+
isValid: false,
|
|
2638
|
+
error: `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are allowed.`
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
if (url.username || url.password) {
|
|
2642
|
+
return {
|
|
2643
|
+
isValid: false,
|
|
2644
|
+
error: "URLs with embedded credentials are not allowed."
|
|
2645
|
+
};
|
|
2646
|
+
}
|
|
2647
|
+
const hostname = url.hostname.toLowerCase();
|
|
2648
|
+
if (isBlockedHostname(hostname)) {
|
|
2649
|
+
return {
|
|
2650
|
+
isValid: false,
|
|
2651
|
+
error: "Access to internal/localhost addresses is not allowed."
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
if (isIPAddress(hostname) && isBlockedIP(hostname)) {
|
|
2655
|
+
return {
|
|
2656
|
+
isValid: false,
|
|
2657
|
+
error: "Access to private/internal IP addresses is not allowed."
|
|
2658
|
+
};
|
|
2659
|
+
}
|
|
2660
|
+
const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80;
|
|
2661
|
+
if (BLOCKED_PORTS.includes(port)) {
|
|
2662
|
+
return {
|
|
2663
|
+
isValid: false,
|
|
2664
|
+
error: `Access to port ${port} is not allowed.`
|
|
2665
|
+
};
|
|
2666
|
+
}
|
|
2667
|
+
let resolvedIP;
|
|
2668
|
+
if (!isIPAddress(hostname)) {
|
|
2669
|
+
try {
|
|
2670
|
+
const dns = await import("node:dns");
|
|
2671
|
+
const resolver = new dns.promises.Resolver();
|
|
2672
|
+
const resolvedIPs = [];
|
|
2673
|
+
try {
|
|
2674
|
+
const aRecords = await resolver.resolve4(hostname);
|
|
2675
|
+
resolvedIPs.push(...aRecords);
|
|
2676
|
+
} catch {
|
|
2677
|
+
}
|
|
2678
|
+
try {
|
|
2679
|
+
const aaaaRecords = await resolver.resolve6(hostname);
|
|
2680
|
+
resolvedIPs.push(...aaaaRecords);
|
|
2681
|
+
} catch {
|
|
2682
|
+
}
|
|
2683
|
+
if (resolvedIPs.length === 0) {
|
|
2684
|
+
return {
|
|
2685
|
+
isValid: false,
|
|
2686
|
+
error: "DNS resolution failed: hostname did not resolve to any address."
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
for (const ip of resolvedIPs) {
|
|
2690
|
+
if (isBlockedIP(ip)) {
|
|
2691
|
+
return {
|
|
2692
|
+
isValid: false,
|
|
2693
|
+
error: "Hostname resolves to a private/internal IP address."
|
|
2694
|
+
};
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
resolvedIP = resolvedIPs[0];
|
|
2698
|
+
} catch {
|
|
2699
|
+
return {
|
|
2700
|
+
isValid: false,
|
|
2701
|
+
error: "DNS resolution failed. Cannot verify hostname safety."
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
return { isValid: true, sanitizedUrl: url.toString(), resolvedIP };
|
|
2706
|
+
} catch (error) {
|
|
2707
|
+
return {
|
|
2708
|
+
isValid: false,
|
|
2709
|
+
error: `Invalid URL format: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
function quickSSRFCheck(urlString) {
|
|
2714
|
+
try {
|
|
2715
|
+
const url = new URL(urlString);
|
|
2716
|
+
if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
|
|
2717
|
+
return { isValid: false, error: `Invalid protocol: ${url.protocol}` };
|
|
2718
|
+
}
|
|
2719
|
+
if (url.username || url.password) {
|
|
2720
|
+
return { isValid: false, error: "URLs with credentials not allowed" };
|
|
2721
|
+
}
|
|
2722
|
+
const hostname = url.hostname.toLowerCase();
|
|
2723
|
+
if (isBlockedHostname(hostname) || isIPAddress(hostname) && isBlockedIP(hostname)) {
|
|
2724
|
+
return { isValid: false, error: "Access to internal addresses not allowed" };
|
|
2725
|
+
}
|
|
2726
|
+
return { isValid: true, sanitizedUrl: url.toString() };
|
|
2727
|
+
} catch {
|
|
2728
|
+
return { isValid: false, error: "Invalid URL format" };
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2567
2732
|
// src/tools/distribution.ts
|
|
2568
2733
|
init_supabase();
|
|
2569
2734
|
|
|
@@ -2725,16 +2890,42 @@ function asEnvelope2(data) {
|
|
|
2725
2890
|
data
|
|
2726
2891
|
};
|
|
2727
2892
|
}
|
|
2893
|
+
function isAlreadyR2Signed(url) {
|
|
2894
|
+
try {
|
|
2895
|
+
const u = new URL(url);
|
|
2896
|
+
return u.searchParams.has("X-Amz-Signature");
|
|
2897
|
+
} catch {
|
|
2898
|
+
return false;
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
async function rehostExternalUrl(mediaUrl, projectId) {
|
|
2902
|
+
if (isAlreadyR2Signed(mediaUrl)) {
|
|
2903
|
+
return { signedUrl: mediaUrl, r2Key: "" };
|
|
2904
|
+
}
|
|
2905
|
+
const ssrf = quickSSRFCheck(mediaUrl);
|
|
2906
|
+
if (!ssrf.isValid) {
|
|
2907
|
+
return { error: ssrf.error ?? "URL rejected by SSRF check" };
|
|
2908
|
+
}
|
|
2909
|
+
const { data, error } = await callEdgeFunction(
|
|
2910
|
+
"upload-to-r2",
|
|
2911
|
+
{ url: ssrf.sanitizedUrl ?? mediaUrl, projectId },
|
|
2912
|
+
{ timeoutMs: 6e4 }
|
|
2913
|
+
);
|
|
2914
|
+
if (error || !data?.key || !data?.url) {
|
|
2915
|
+
return { error: error ?? "upload-to-r2 returned no key" };
|
|
2916
|
+
}
|
|
2917
|
+
return { signedUrl: data.url, r2Key: data.key };
|
|
2918
|
+
}
|
|
2728
2919
|
function registerDistributionTools(server) {
|
|
2729
2920
|
server.tool(
|
|
2730
2921
|
"schedule_post",
|
|
2731
2922
|
'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.',
|
|
2732
2923
|
{
|
|
2733
2924
|
media_url: z3.string().optional().describe(
|
|
2734
|
-
"URL of the media file to post.
|
|
2925
|
+
"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."
|
|
2735
2926
|
),
|
|
2736
2927
|
media_urls: z3.array(z3.string()).optional().describe(
|
|
2737
|
-
"Array of 2-10 image URLs for carousel posts.
|
|
2928
|
+
"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."
|
|
2738
2929
|
),
|
|
2739
2930
|
r2_key: z3.string().optional().describe(
|
|
2740
2931
|
"R2 object key from upload_media. Signed on demand at post time \u2014 survives scheduling delays. Alternative to media_url."
|
|
@@ -2823,7 +3014,10 @@ function registerDistributionTools(server) {
|
|
|
2823
3014
|
),
|
|
2824
3015
|
project_id: z3.string().optional().describe("Social Neuron project ID to associate this post with."),
|
|
2825
3016
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
|
|
2826
|
-
attribution: z3.boolean().optional().describe('If true, appends "Created with Social Neuron" to the caption. Default: false.')
|
|
3017
|
+
attribution: z3.boolean().optional().describe('If true, appends "Created with Social Neuron" to the caption. Default: false.'),
|
|
3018
|
+
auto_rehost: z3.boolean().optional().describe(
|
|
3019
|
+
"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."
|
|
3020
|
+
)
|
|
2827
3021
|
},
|
|
2828
3022
|
async ({
|
|
2829
3023
|
media_url,
|
|
@@ -2837,6 +3031,7 @@ function registerDistributionTools(server) {
|
|
|
2837
3031
|
project_id,
|
|
2838
3032
|
response_format,
|
|
2839
3033
|
attribution,
|
|
3034
|
+
auto_rehost,
|
|
2840
3035
|
r2_key,
|
|
2841
3036
|
r2_keys,
|
|
2842
3037
|
job_id,
|
|
@@ -2948,12 +3143,54 @@ function registerDistributionTools(server) {
|
|
|
2948
3143
|
}
|
|
2949
3144
|
resolvedMediaUrls = resolved;
|
|
2950
3145
|
}
|
|
3146
|
+
const shouldRehost = auto_rehost !== false;
|
|
3147
|
+
if (shouldRehost && resolvedMediaUrl && !isAlreadyR2Signed(resolvedMediaUrl)) {
|
|
3148
|
+
const rehost = await rehostExternalUrl(resolvedMediaUrl, project_id);
|
|
3149
|
+
if ("error" in rehost) {
|
|
3150
|
+
return {
|
|
3151
|
+
content: [
|
|
3152
|
+
{
|
|
3153
|
+
type: "text",
|
|
3154
|
+
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.`
|
|
3155
|
+
}
|
|
3156
|
+
],
|
|
3157
|
+
isError: true
|
|
3158
|
+
};
|
|
3159
|
+
}
|
|
3160
|
+
resolvedMediaUrl = rehost.signedUrl;
|
|
3161
|
+
}
|
|
3162
|
+
if (shouldRehost && resolvedMediaUrls && resolvedMediaUrls.length > 0) {
|
|
3163
|
+
const needsRehost = resolvedMediaUrls.map((u) => !isAlreadyR2Signed(u));
|
|
3164
|
+
if (needsRehost.some(Boolean)) {
|
|
3165
|
+
const rehosted = await Promise.all(
|
|
3166
|
+
resolvedMediaUrls.map(
|
|
3167
|
+
(u, i) => needsRehost[i] ? rehostExternalUrl(u, project_id) : Promise.resolve({ signedUrl: u, r2Key: "" })
|
|
3168
|
+
)
|
|
3169
|
+
);
|
|
3170
|
+
const failIdx = rehosted.findIndex((r) => "error" in r);
|
|
3171
|
+
if (failIdx !== -1) {
|
|
3172
|
+
const failed = rehosted[failIdx];
|
|
3173
|
+
return {
|
|
3174
|
+
content: [
|
|
3175
|
+
{
|
|
3176
|
+
type: "text",
|
|
3177
|
+
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.`
|
|
3178
|
+
}
|
|
3179
|
+
],
|
|
3180
|
+
isError: true
|
|
3181
|
+
};
|
|
3182
|
+
}
|
|
3183
|
+
resolvedMediaUrls = rehosted.map(
|
|
3184
|
+
(r) => r.signedUrl
|
|
3185
|
+
);
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
2951
3188
|
} catch (resolveErr) {
|
|
2952
3189
|
return {
|
|
2953
3190
|
content: [
|
|
2954
3191
|
{
|
|
2955
3192
|
type: "text",
|
|
2956
|
-
text: `Failed to resolve media: ${
|
|
3193
|
+
text: `Failed to resolve media: ${sanitizeError(resolveErr)}`
|
|
2957
3194
|
}
|
|
2958
3195
|
],
|
|
2959
3196
|
isError: true
|
|
@@ -3318,7 +3555,7 @@ Created with Social Neuron`;
|
|
|
3318
3555
|
return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
|
|
3319
3556
|
} catch (err) {
|
|
3320
3557
|
const durationMs = Date.now() - startedAt;
|
|
3321
|
-
const message =
|
|
3558
|
+
const message = sanitizeError(err);
|
|
3322
3559
|
logMcpToolInvocation({
|
|
3323
3560
|
toolName: "find_next_slots",
|
|
3324
3561
|
status: "error",
|
|
@@ -3838,7 +4075,7 @@ Created with Social Neuron`;
|
|
|
3838
4075
|
};
|
|
3839
4076
|
} catch (err) {
|
|
3840
4077
|
const durationMs = Date.now() - startedAt;
|
|
3841
|
-
const message =
|
|
4078
|
+
const message = sanitizeError(err);
|
|
3842
4079
|
logMcpToolInvocation({
|
|
3843
4080
|
toolName: "schedule_content_plan",
|
|
3844
4081
|
status: "error",
|
|
@@ -3860,6 +4097,19 @@ import { readFile } from "node:fs/promises";
|
|
|
3860
4097
|
import { basename, extname } from "node:path";
|
|
3861
4098
|
init_supabase();
|
|
3862
4099
|
var MAX_BASE64_SIZE = 10 * 1024 * 1024;
|
|
4100
|
+
var ALLOWED_UPLOAD_TYPES = /* @__PURE__ */ new Set([
|
|
4101
|
+
"image/png",
|
|
4102
|
+
"image/jpeg",
|
|
4103
|
+
"image/gif",
|
|
4104
|
+
"image/webp",
|
|
4105
|
+
"image/svg+xml",
|
|
4106
|
+
"video/mp4",
|
|
4107
|
+
"video/quicktime",
|
|
4108
|
+
"video/x-msvideo",
|
|
4109
|
+
"video/webm"
|
|
4110
|
+
]);
|
|
4111
|
+
var BASE64_CHARS = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
4112
|
+
var DATA_URI_PREFIX = /^data:([^;,]+);base64,/;
|
|
3863
4113
|
function maskR2Key(key) {
|
|
3864
4114
|
const segments = key.split("/");
|
|
3865
4115
|
return segments.length >= 3 ? `\u2026/${segments.slice(-2).join("/")}` : key;
|
|
@@ -3880,21 +4130,35 @@ function inferContentType(filePath) {
|
|
|
3880
4130
|
};
|
|
3881
4131
|
return map[ext] || "application/octet-stream";
|
|
3882
4132
|
}
|
|
4133
|
+
function approxBase64Size(raw) {
|
|
4134
|
+
const len = raw.length;
|
|
4135
|
+
if (len === 0) return 0;
|
|
4136
|
+
let padding = 0;
|
|
4137
|
+
if (raw.endsWith("==")) padding = 2;
|
|
4138
|
+
else if (raw.endsWith("=")) padding = 1;
|
|
4139
|
+
return Math.floor(len * 3 / 4) - padding;
|
|
4140
|
+
}
|
|
3883
4141
|
function registerMediaTools(server) {
|
|
3884
4142
|
server.tool(
|
|
3885
4143
|
"upload_media",
|
|
3886
|
-
"Upload
|
|
4144
|
+
"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.",
|
|
3887
4145
|
{
|
|
3888
|
-
source: z4.string().describe(
|
|
3889
|
-
'Local file path (e.g. "/Users/me/image.png") or public URL (e.g. "https://example.com/photo.jpg").
|
|
4146
|
+
source: z4.string().optional().describe(
|
|
4147
|
+
'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.'
|
|
4148
|
+
),
|
|
4149
|
+
file_data: z4.string().optional().describe(
|
|
4150
|
+
'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.'
|
|
4151
|
+
),
|
|
4152
|
+
file_name: z4.string().optional().describe(
|
|
4153
|
+
'Optional filename for the upload (e.g. "hero.png"). Path components are stripped \u2014 only the basename is used.'
|
|
3890
4154
|
),
|
|
3891
4155
|
content_type: z4.string().optional().describe(
|
|
3892
|
-
'MIME type (e.g. "image/png", "video/mp4"). Auto-detected from file extension
|
|
4156
|
+
'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.'
|
|
3893
4157
|
),
|
|
3894
4158
|
project_id: z4.string().optional().describe("Project ID for R2 path organization."),
|
|
3895
4159
|
response_format: z4.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
3896
4160
|
},
|
|
3897
|
-
async ({ source, content_type, project_id, response_format }) => {
|
|
4161
|
+
async ({ source, file_data, file_name, content_type, project_id, response_format }) => {
|
|
3898
4162
|
const format = response_format ?? "text";
|
|
3899
4163
|
const startedAt = Date.now();
|
|
3900
4164
|
const userId = await getDefaultUserId();
|
|
@@ -3916,46 +4180,187 @@ function registerMediaTools(server) {
|
|
|
3916
4180
|
isError: true
|
|
3917
4181
|
};
|
|
3918
4182
|
}
|
|
3919
|
-
|
|
3920
|
-
|
|
4183
|
+
if (!source && !file_data) {
|
|
4184
|
+
return {
|
|
4185
|
+
content: [
|
|
4186
|
+
{
|
|
4187
|
+
type: "text",
|
|
4188
|
+
text: "upload_media requires either `source` (path or URL) or `file_data` (base64)."
|
|
4189
|
+
}
|
|
4190
|
+
],
|
|
4191
|
+
isError: true
|
|
4192
|
+
};
|
|
4193
|
+
}
|
|
4194
|
+
if (file_data) {
|
|
4195
|
+
let raw = file_data;
|
|
4196
|
+
let detectedType;
|
|
4197
|
+
const prefixMatch = raw.match(DATA_URI_PREFIX);
|
|
4198
|
+
if (prefixMatch) {
|
|
4199
|
+
detectedType = prefixMatch[1].trim().toLowerCase();
|
|
4200
|
+
raw = raw.slice(prefixMatch[0].length);
|
|
4201
|
+
}
|
|
4202
|
+
const ct = (content_type ?? detectedType ?? "").trim().toLowerCase();
|
|
4203
|
+
if (!ct) {
|
|
4204
|
+
return {
|
|
4205
|
+
content: [
|
|
4206
|
+
{
|
|
4207
|
+
type: "text",
|
|
4208
|
+
text: "content_type is required when file_data has no data: prefix."
|
|
4209
|
+
}
|
|
4210
|
+
],
|
|
4211
|
+
isError: true
|
|
4212
|
+
};
|
|
4213
|
+
}
|
|
4214
|
+
if (!ALLOWED_UPLOAD_TYPES.has(ct)) {
|
|
4215
|
+
return {
|
|
4216
|
+
content: [
|
|
4217
|
+
{
|
|
4218
|
+
type: "text",
|
|
4219
|
+
text: `content_type "${ct}" is not supported. Allowed: ${[...ALLOWED_UPLOAD_TYPES].sort().join(", ")}.`
|
|
4220
|
+
}
|
|
4221
|
+
],
|
|
4222
|
+
isError: true
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
4225
|
+
const stripped = raw.replace(/\s+/g, "");
|
|
4226
|
+
if (!BASE64_CHARS.test(stripped)) {
|
|
4227
|
+
return {
|
|
4228
|
+
content: [
|
|
4229
|
+
{
|
|
4230
|
+
type: "text",
|
|
4231
|
+
text: "file_data is not valid base64 \u2014 only A-Z, a-z, 0-9, +, /, = are allowed."
|
|
4232
|
+
}
|
|
4233
|
+
],
|
|
4234
|
+
isError: true
|
|
4235
|
+
};
|
|
4236
|
+
}
|
|
4237
|
+
const approxSize = approxBase64Size(stripped);
|
|
4238
|
+
if (approxSize > MAX_BASE64_SIZE) {
|
|
4239
|
+
return {
|
|
4240
|
+
content: [
|
|
4241
|
+
{
|
|
4242
|
+
type: "text",
|
|
4243
|
+
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.`
|
|
4244
|
+
}
|
|
4245
|
+
],
|
|
4246
|
+
isError: true
|
|
4247
|
+
};
|
|
4248
|
+
}
|
|
4249
|
+
const safeName = basename(file_name ?? "upload");
|
|
4250
|
+
const uploadBody2 = {
|
|
4251
|
+
fileData: `data:${ct};base64,${stripped}`,
|
|
4252
|
+
contentType: ct,
|
|
4253
|
+
fileName: safeName,
|
|
4254
|
+
projectId: project_id
|
|
4255
|
+
};
|
|
4256
|
+
const { data: data2, error: error2 } = await callEdgeFunction("upload-to-r2", uploadBody2, { timeoutMs: 6e4 });
|
|
4257
|
+
if (error2) {
|
|
4258
|
+
await logMcpToolInvocation({
|
|
4259
|
+
toolName: "upload_media",
|
|
4260
|
+
status: "error",
|
|
4261
|
+
durationMs: Date.now() - startedAt,
|
|
4262
|
+
details: { error: error2, source: "base64", contentType: ct, size: approxSize }
|
|
4263
|
+
});
|
|
4264
|
+
return {
|
|
4265
|
+
content: [{ type: "text", text: `Upload failed: ${error2}` }],
|
|
4266
|
+
isError: true
|
|
4267
|
+
};
|
|
4268
|
+
}
|
|
4269
|
+
if (!data2?.key) {
|
|
4270
|
+
return {
|
|
4271
|
+
content: [{ type: "text", text: "Upload returned no R2 key." }],
|
|
4272
|
+
isError: true
|
|
4273
|
+
};
|
|
4274
|
+
}
|
|
4275
|
+
await logMcpToolInvocation({
|
|
4276
|
+
toolName: "upload_media",
|
|
4277
|
+
status: "success",
|
|
4278
|
+
durationMs: Date.now() - startedAt,
|
|
4279
|
+
details: {
|
|
4280
|
+
source: "base64",
|
|
4281
|
+
r2Key: data2.key,
|
|
4282
|
+
size: data2.size,
|
|
4283
|
+
contentType: data2.contentType
|
|
4284
|
+
}
|
|
4285
|
+
});
|
|
4286
|
+
if (format === "json") {
|
|
4287
|
+
return {
|
|
4288
|
+
content: [
|
|
4289
|
+
{
|
|
4290
|
+
type: "text",
|
|
4291
|
+
text: JSON.stringify(
|
|
4292
|
+
{
|
|
4293
|
+
r2_key: data2.key,
|
|
4294
|
+
signed_url: data2.url,
|
|
4295
|
+
size: data2.size,
|
|
4296
|
+
content_type: data2.contentType
|
|
4297
|
+
},
|
|
4298
|
+
null,
|
|
4299
|
+
2
|
|
4300
|
+
)
|
|
4301
|
+
}
|
|
4302
|
+
],
|
|
4303
|
+
isError: false
|
|
4304
|
+
};
|
|
4305
|
+
}
|
|
4306
|
+
return {
|
|
4307
|
+
content: [
|
|
4308
|
+
{
|
|
4309
|
+
type: "text",
|
|
4310
|
+
text: [
|
|
4311
|
+
"Media uploaded successfully.",
|
|
4312
|
+
`Media key: ${maskR2Key(data2.key)}`,
|
|
4313
|
+
`Signed URL: ${data2.url}`,
|
|
4314
|
+
`Size: ${(data2.size / 1024).toFixed(0)}KB`,
|
|
4315
|
+
`Type: ${data2.contentType}`,
|
|
4316
|
+
"",
|
|
4317
|
+
"Use job_id or response_format=json with schedule_post to post to any platform."
|
|
4318
|
+
].join("\n")
|
|
4319
|
+
}
|
|
4320
|
+
],
|
|
4321
|
+
isError: false
|
|
4322
|
+
};
|
|
4323
|
+
}
|
|
4324
|
+
const src = source;
|
|
4325
|
+
const isUrl = src.startsWith("http://") || src.startsWith("https://");
|
|
3921
4326
|
let uploadBody;
|
|
3922
4327
|
if (isUrl) {
|
|
3923
|
-
const ct = content_type || inferContentType(
|
|
4328
|
+
const ct = content_type || inferContentType(src);
|
|
3924
4329
|
uploadBody = {
|
|
3925
|
-
url:
|
|
4330
|
+
url: src,
|
|
3926
4331
|
contentType: ct,
|
|
3927
|
-
fileName: basename(new URL(
|
|
4332
|
+
fileName: basename(file_name ?? new URL(src).pathname) || "upload",
|
|
3928
4333
|
projectId: project_id
|
|
3929
4334
|
};
|
|
3930
4335
|
} else {
|
|
3931
4336
|
let fileBuffer;
|
|
3932
4337
|
try {
|
|
3933
|
-
fileBuffer = await readFile(
|
|
3934
|
-
} catch
|
|
4338
|
+
fileBuffer = await readFile(src);
|
|
4339
|
+
} catch {
|
|
3935
4340
|
await logMcpToolInvocation({
|
|
3936
4341
|
toolName: "upload_media",
|
|
3937
4342
|
status: "error",
|
|
3938
4343
|
durationMs: Date.now() - startedAt,
|
|
3939
|
-
details: { error:
|
|
4344
|
+
details: { error: "File not found", source: "local" }
|
|
3940
4345
|
});
|
|
3941
4346
|
return {
|
|
3942
4347
|
content: [
|
|
3943
4348
|
{
|
|
3944
4349
|
type: "text",
|
|
3945
|
-
text: `File not found or not readable: ${
|
|
4350
|
+
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.`
|
|
3946
4351
|
}
|
|
3947
4352
|
],
|
|
3948
4353
|
isError: true
|
|
3949
4354
|
};
|
|
3950
4355
|
}
|
|
3951
|
-
const ct = content_type || inferContentType(
|
|
4356
|
+
const ct = content_type || inferContentType(src);
|
|
3952
4357
|
if (fileBuffer.length > MAX_BASE64_SIZE) {
|
|
3953
4358
|
const { data: putData, error: putError } = await callEdgeFunction(
|
|
3954
4359
|
"get-signed-url",
|
|
3955
4360
|
{
|
|
3956
4361
|
operation: "put",
|
|
3957
4362
|
contentType: ct,
|
|
3958
|
-
filename: basename(
|
|
4363
|
+
filename: basename(file_name ?? src),
|
|
3959
4364
|
projectId: project_id
|
|
3960
4365
|
},
|
|
3961
4366
|
{ timeoutMs: 1e4 }
|
|
@@ -4057,7 +4462,7 @@ function registerMediaTools(server) {
|
|
|
4057
4462
|
uploadBody = {
|
|
4058
4463
|
fileData: base64,
|
|
4059
4464
|
contentType: ct,
|
|
4060
|
-
fileName: basename(
|
|
4465
|
+
fileName: basename(file_name ?? src),
|
|
4061
4466
|
projectId: project_id
|
|
4062
4467
|
};
|
|
4063
4468
|
}
|
|
@@ -4446,155 +4851,6 @@ function formatAnalytics(summary, days, format) {
|
|
|
4446
4851
|
// src/tools/brand.ts
|
|
4447
4852
|
import { z as z6 } from "zod";
|
|
4448
4853
|
init_supabase();
|
|
4449
|
-
|
|
4450
|
-
// src/lib/ssrf.ts
|
|
4451
|
-
var BLOCKED_IP_PATTERNS = [
|
|
4452
|
-
// IPv4 localhost/loopback
|
|
4453
|
-
/^127\./,
|
|
4454
|
-
/^0\./,
|
|
4455
|
-
// IPv4 private ranges (RFC 1918)
|
|
4456
|
-
/^10\./,
|
|
4457
|
-
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
|
4458
|
-
/^192\.168\./,
|
|
4459
|
-
// IPv4 link-local
|
|
4460
|
-
/^169\.254\./,
|
|
4461
|
-
// Cloud metadata endpoint (AWS, GCP, Azure)
|
|
4462
|
-
/^169\.254\.169\.254$/,
|
|
4463
|
-
// IPv4 broadcast
|
|
4464
|
-
/^255\./,
|
|
4465
|
-
// Shared address space (RFC 6598)
|
|
4466
|
-
/^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./
|
|
4467
|
-
];
|
|
4468
|
-
var BLOCKED_IPV6_PATTERNS = [
|
|
4469
|
-
/^::1$/i,
|
|
4470
|
-
// loopback
|
|
4471
|
-
/^::$/i,
|
|
4472
|
-
// unspecified
|
|
4473
|
-
/^fe[89ab][0-9a-f]:/i,
|
|
4474
|
-
// link-local fe80::/10
|
|
4475
|
-
/^fc[0-9a-f]:/i,
|
|
4476
|
-
// unique local fc00::/7
|
|
4477
|
-
/^fd[0-9a-f]:/i,
|
|
4478
|
-
// unique local fc00::/7
|
|
4479
|
-
/^::ffff:127\./i,
|
|
4480
|
-
// IPv4-mapped localhost
|
|
4481
|
-
/^::ffff:(0|10|127|169\.254|172\.(1[6-9]|2[0-9]|3[0-1])|192\.168)\./i
|
|
4482
|
-
// IPv4-mapped private
|
|
4483
|
-
];
|
|
4484
|
-
var BLOCKED_HOSTNAMES = [
|
|
4485
|
-
"localhost",
|
|
4486
|
-
"localhost.localdomain",
|
|
4487
|
-
"local",
|
|
4488
|
-
"127.0.0.1",
|
|
4489
|
-
"0.0.0.0",
|
|
4490
|
-
"[::1]",
|
|
4491
|
-
"[::ffff:127.0.0.1]",
|
|
4492
|
-
// Cloud metadata endpoints
|
|
4493
|
-
"metadata.google.internal",
|
|
4494
|
-
"metadata.goog",
|
|
4495
|
-
"instance-data",
|
|
4496
|
-
"instance-data.ec2.internal"
|
|
4497
|
-
];
|
|
4498
|
-
var ALLOWED_PROTOCOLS = ["http:", "https:"];
|
|
4499
|
-
var BLOCKED_PORTS = [22, 23, 25, 110, 143, 445, 3306, 5432, 6379, 27017, 11211];
|
|
4500
|
-
function isBlockedIP(ip) {
|
|
4501
|
-
const normalized = ip.replace(/^\[/, "").replace(/\]$/, "");
|
|
4502
|
-
if (normalized.includes(":")) {
|
|
4503
|
-
return BLOCKED_IPV6_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
4504
|
-
}
|
|
4505
|
-
return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
4506
|
-
}
|
|
4507
|
-
function isBlockedHostname(hostname) {
|
|
4508
|
-
return BLOCKED_HOSTNAMES.includes(hostname.toLowerCase());
|
|
4509
|
-
}
|
|
4510
|
-
function isIPAddress(hostname) {
|
|
4511
|
-
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
4512
|
-
const ipv6Pattern = /^\[?[a-fA-F0-9:]+\]?$/;
|
|
4513
|
-
return ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname);
|
|
4514
|
-
}
|
|
4515
|
-
async function validateUrlForSSRF(urlString) {
|
|
4516
|
-
try {
|
|
4517
|
-
const url = new URL(urlString);
|
|
4518
|
-
if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
|
|
4519
|
-
return {
|
|
4520
|
-
isValid: false,
|
|
4521
|
-
error: `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are allowed.`
|
|
4522
|
-
};
|
|
4523
|
-
}
|
|
4524
|
-
if (url.username || url.password) {
|
|
4525
|
-
return {
|
|
4526
|
-
isValid: false,
|
|
4527
|
-
error: "URLs with embedded credentials are not allowed."
|
|
4528
|
-
};
|
|
4529
|
-
}
|
|
4530
|
-
const hostname = url.hostname.toLowerCase();
|
|
4531
|
-
if (isBlockedHostname(hostname)) {
|
|
4532
|
-
return {
|
|
4533
|
-
isValid: false,
|
|
4534
|
-
error: "Access to internal/localhost addresses is not allowed."
|
|
4535
|
-
};
|
|
4536
|
-
}
|
|
4537
|
-
if (isIPAddress(hostname) && isBlockedIP(hostname)) {
|
|
4538
|
-
return {
|
|
4539
|
-
isValid: false,
|
|
4540
|
-
error: "Access to private/internal IP addresses is not allowed."
|
|
4541
|
-
};
|
|
4542
|
-
}
|
|
4543
|
-
const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80;
|
|
4544
|
-
if (BLOCKED_PORTS.includes(port)) {
|
|
4545
|
-
return {
|
|
4546
|
-
isValid: false,
|
|
4547
|
-
error: `Access to port ${port} is not allowed.`
|
|
4548
|
-
};
|
|
4549
|
-
}
|
|
4550
|
-
let resolvedIP;
|
|
4551
|
-
if (!isIPAddress(hostname)) {
|
|
4552
|
-
try {
|
|
4553
|
-
const dns = await import("node:dns");
|
|
4554
|
-
const resolver = new dns.promises.Resolver();
|
|
4555
|
-
const resolvedIPs = [];
|
|
4556
|
-
try {
|
|
4557
|
-
const aRecords = await resolver.resolve4(hostname);
|
|
4558
|
-
resolvedIPs.push(...aRecords);
|
|
4559
|
-
} catch {
|
|
4560
|
-
}
|
|
4561
|
-
try {
|
|
4562
|
-
const aaaaRecords = await resolver.resolve6(hostname);
|
|
4563
|
-
resolvedIPs.push(...aaaaRecords);
|
|
4564
|
-
} catch {
|
|
4565
|
-
}
|
|
4566
|
-
if (resolvedIPs.length === 0) {
|
|
4567
|
-
return {
|
|
4568
|
-
isValid: false,
|
|
4569
|
-
error: "DNS resolution failed: hostname did not resolve to any address."
|
|
4570
|
-
};
|
|
4571
|
-
}
|
|
4572
|
-
for (const ip of resolvedIPs) {
|
|
4573
|
-
if (isBlockedIP(ip)) {
|
|
4574
|
-
return {
|
|
4575
|
-
isValid: false,
|
|
4576
|
-
error: "Hostname resolves to a private/internal IP address."
|
|
4577
|
-
};
|
|
4578
|
-
}
|
|
4579
|
-
}
|
|
4580
|
-
resolvedIP = resolvedIPs[0];
|
|
4581
|
-
} catch {
|
|
4582
|
-
return {
|
|
4583
|
-
isValid: false,
|
|
4584
|
-
error: "DNS resolution failed. Cannot verify hostname safety."
|
|
4585
|
-
};
|
|
4586
|
-
}
|
|
4587
|
-
}
|
|
4588
|
-
return { isValid: true, sanitizedUrl: url.toString(), resolvedIP };
|
|
4589
|
-
} catch (error) {
|
|
4590
|
-
return {
|
|
4591
|
-
isValid: false,
|
|
4592
|
-
error: `Invalid URL format: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
4593
|
-
};
|
|
4594
|
-
}
|
|
4595
|
-
}
|
|
4596
|
-
|
|
4597
|
-
// src/tools/brand.ts
|
|
4598
4854
|
function asEnvelope4(data) {
|
|
4599
4855
|
return {
|
|
4600
4856
|
_meta: {
|
|
@@ -5117,7 +5373,7 @@ function registerScreenshotTools(server) {
|
|
|
5117
5373
|
};
|
|
5118
5374
|
} catch (err) {
|
|
5119
5375
|
await closeBrowser();
|
|
5120
|
-
const message =
|
|
5376
|
+
const message = sanitizeError(err);
|
|
5121
5377
|
await logMcpToolInvocation({
|
|
5122
5378
|
toolName: "capture_app_page",
|
|
5123
5379
|
status: "error",
|
|
@@ -5273,7 +5529,7 @@ function registerScreenshotTools(server) {
|
|
|
5273
5529
|
};
|
|
5274
5530
|
} catch (err) {
|
|
5275
5531
|
await closeBrowser();
|
|
5276
|
-
const message =
|
|
5532
|
+
const message = sanitizeError(err);
|
|
5277
5533
|
await logMcpToolInvocation({
|
|
5278
5534
|
toolName: "capture_screenshot",
|
|
5279
5535
|
status: "error",
|
|
@@ -5566,7 +5822,7 @@ function registerRemotionTools(server) {
|
|
|
5566
5822
|
]
|
|
5567
5823
|
};
|
|
5568
5824
|
} catch (err) {
|
|
5569
|
-
const message =
|
|
5825
|
+
const message = sanitizeError(err);
|
|
5570
5826
|
await logMcpToolInvocation({
|
|
5571
5827
|
toolName: "render_demo_video",
|
|
5572
5828
|
status: "error",
|
|
@@ -5694,7 +5950,7 @@ function registerRemotionTools(server) {
|
|
|
5694
5950
|
]
|
|
5695
5951
|
};
|
|
5696
5952
|
} catch (err) {
|
|
5697
|
-
const message =
|
|
5953
|
+
const message = sanitizeError(err);
|
|
5698
5954
|
await logMcpToolInvocation({
|
|
5699
5955
|
toolName: "render_template_video",
|
|
5700
5956
|
status: "error",
|
|
@@ -7611,7 +7867,7 @@ function registerExtractionTools(server) {
|
|
|
7611
7867
|
};
|
|
7612
7868
|
} catch (err) {
|
|
7613
7869
|
const durationMs = Date.now() - startedAt;
|
|
7614
|
-
const message =
|
|
7870
|
+
const message = sanitizeError(err);
|
|
7615
7871
|
logMcpToolInvocation({
|
|
7616
7872
|
toolName: "extract_url_content",
|
|
7617
7873
|
status: "error",
|
|
@@ -8167,7 +8423,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
8167
8423
|
}
|
|
8168
8424
|
} catch (persistErr) {
|
|
8169
8425
|
const durationMs2 = Date.now() - startedAt;
|
|
8170
|
-
const message =
|
|
8426
|
+
const message = sanitizeError(persistErr);
|
|
8171
8427
|
logMcpToolInvocation({
|
|
8172
8428
|
toolName: "plan_content_week",
|
|
8173
8429
|
status: "error",
|
|
@@ -8199,7 +8455,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
8199
8455
|
};
|
|
8200
8456
|
} catch (err) {
|
|
8201
8457
|
const durationMs = Date.now() - startedAt;
|
|
8202
|
-
const message =
|
|
8458
|
+
const message = sanitizeError(err);
|
|
8203
8459
|
logMcpToolInvocation({
|
|
8204
8460
|
toolName: "plan_content_week",
|
|
8205
8461
|
status: "error",
|
|
@@ -8291,7 +8547,7 @@ ${rawText.slice(0, 1e3)}`
|
|
|
8291
8547
|
};
|
|
8292
8548
|
} catch (err) {
|
|
8293
8549
|
const durationMs = Date.now() - startedAt;
|
|
8294
|
-
const message =
|
|
8550
|
+
const message = sanitizeError(err);
|
|
8295
8551
|
logMcpToolInvocation({
|
|
8296
8552
|
toolName: "save_content_plan",
|
|
8297
8553
|
status: "error",
|
|
@@ -9381,7 +9637,7 @@ function registerPipelineTools(server) {
|
|
|
9381
9637
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
9382
9638
|
} catch (err) {
|
|
9383
9639
|
const durationMs = Date.now() - startedAt;
|
|
9384
|
-
const message =
|
|
9640
|
+
const message = sanitizeError(err);
|
|
9385
9641
|
logMcpToolInvocation({
|
|
9386
9642
|
toolName: "check_pipeline_readiness",
|
|
9387
9643
|
status: "error",
|
|
@@ -9542,7 +9798,7 @@ function registerPipelineTools(server) {
|
|
|
9542
9798
|
} catch (deductErr) {
|
|
9543
9799
|
errors.push({
|
|
9544
9800
|
stage: "planning",
|
|
9545
|
-
message: `Credit deduction failed: ${
|
|
9801
|
+
message: `Credit deduction failed: ${sanitizeError(deductErr)}`
|
|
9546
9802
|
});
|
|
9547
9803
|
}
|
|
9548
9804
|
}
|
|
@@ -9713,7 +9969,7 @@ function registerPipelineTools(server) {
|
|
|
9713
9969
|
} catch (schedErr) {
|
|
9714
9970
|
errors.push({
|
|
9715
9971
|
stage: "schedule",
|
|
9716
|
-
message: `Failed to schedule ${post.id}: ${
|
|
9972
|
+
message: `Failed to schedule ${post.id}: ${sanitizeError(schedErr)}`
|
|
9717
9973
|
});
|
|
9718
9974
|
}
|
|
9719
9975
|
}
|
|
@@ -9802,7 +10058,7 @@ function registerPipelineTools(server) {
|
|
|
9802
10058
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
9803
10059
|
} catch (err) {
|
|
9804
10060
|
const durationMs = Date.now() - startedAt;
|
|
9805
|
-
const message =
|
|
10061
|
+
const message = sanitizeError(err);
|
|
9806
10062
|
logMcpToolInvocation({
|
|
9807
10063
|
toolName: "run_content_pipeline",
|
|
9808
10064
|
status: "error",
|
|
@@ -10026,7 +10282,7 @@ function registerPipelineTools(server) {
|
|
|
10026
10282
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
10027
10283
|
} catch (err) {
|
|
10028
10284
|
const durationMs = Date.now() - startedAt;
|
|
10029
|
-
const message =
|
|
10285
|
+
const message = sanitizeError(err);
|
|
10030
10286
|
logMcpToolInvocation({
|
|
10031
10287
|
toolName: "auto_approve_plan",
|
|
10032
10288
|
status: "error",
|
|
@@ -10204,7 +10460,7 @@ ${i + 1}. ${s.topic}`);
|
|
|
10204
10460
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
10205
10461
|
} catch (err) {
|
|
10206
10462
|
const durationMs = Date.now() - startedAt;
|
|
10207
|
-
const message =
|
|
10463
|
+
const message = sanitizeError(err);
|
|
10208
10464
|
logMcpToolInvocation({
|
|
10209
10465
|
toolName: "suggest_next_content",
|
|
10210
10466
|
status: "error",
|
|
@@ -10543,7 +10799,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
|
|
|
10543
10799
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
10544
10800
|
} catch (err) {
|
|
10545
10801
|
const durationMs = Date.now() - startedAt;
|
|
10546
|
-
const message =
|
|
10802
|
+
const message = sanitizeError(err);
|
|
10547
10803
|
logMcpToolInvocation({
|
|
10548
10804
|
toolName: "generate_performance_digest",
|
|
10549
10805
|
status: "error",
|
|
@@ -10640,7 +10896,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
|
|
|
10640
10896
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
10641
10897
|
} catch (err) {
|
|
10642
10898
|
const durationMs = Date.now() - startedAt;
|
|
10643
|
-
const message =
|
|
10899
|
+
const message = sanitizeError(err);
|
|
10644
10900
|
logMcpToolInvocation({
|
|
10645
10901
|
toolName: "detect_anomalies",
|
|
10646
10902
|
status: "error",
|
|
@@ -11669,7 +11925,7 @@ function registerCarouselTools(server) {
|
|
|
11669
11925
|
slideNumber: slide.slideNumber,
|
|
11670
11926
|
jobId: null,
|
|
11671
11927
|
model: image_model,
|
|
11672
|
-
error:
|
|
11928
|
+
error: sanitizeError(err)
|
|
11673
11929
|
};
|
|
11674
11930
|
}
|
|
11675
11931
|
})
|
|
@@ -13086,7 +13342,7 @@ app.post("/mcp", authenticateRequest, async (req, res) => {
|
|
|
13086
13342
|
const rawMessage = err instanceof Error ? err.message : "Internal server error";
|
|
13087
13343
|
console.error(`[MCP HTTP] POST /mcp error: ${rawMessage}`);
|
|
13088
13344
|
if (!res.headersSent) {
|
|
13089
|
-
res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message:
|
|
13345
|
+
res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: sanitizeError(err) } });
|
|
13090
13346
|
}
|
|
13091
13347
|
}
|
|
13092
13348
|
});
|
|
@@ -13128,7 +13384,7 @@ app.delete("/mcp", authenticateRequest, async (req, res) => {
|
|
|
13128
13384
|
app.use((err, _req, res, _next) => {
|
|
13129
13385
|
console.error("[MCP HTTP] Unhandled Express error:", err.stack || err.message || err);
|
|
13130
13386
|
if (!res.headersSent) {
|
|
13131
|
-
res.status(500).json({ error: "internal_error", error_description:
|
|
13387
|
+
res.status(500).json({ error: "internal_error", error_description: sanitizeError(err) });
|
|
13132
13388
|
}
|
|
13133
13389
|
});
|
|
13134
13390
|
var httpServer = app.listen(PORT, "0.0.0.0", () => {
|