@socialneuron/mcp-server 1.3.2 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +13 -4
- package/dist/http.js +1234 -527
- package/dist/index.js +1074 -429
- package/package.json +4 -2
package/dist/http.js
CHANGED
|
@@ -10,16 +10,24 @@ var __export = (target, all) => {
|
|
|
10
10
|
|
|
11
11
|
// src/lib/posthog.ts
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
|
-
import { PostHog } from "posthog-node";
|
|
14
13
|
function hashUserId(userId) {
|
|
15
14
|
return createHash("sha256").update(`${POSTHOG_SALT}:${userId}`).digest("hex").substring(0, 32);
|
|
16
15
|
}
|
|
16
|
+
function isTelemetryOptedIn() {
|
|
17
|
+
if (process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true" || process.env.SOCIALNEURON_NO_TELEMETRY === "1") {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return process.env.SOCIALNEURON_TELEMETRY === "1";
|
|
21
|
+
}
|
|
17
22
|
function initPostHog() {
|
|
18
|
-
if (
|
|
23
|
+
if (!isTelemetryOptedIn()) return;
|
|
19
24
|
const key = process.env.POSTHOG_KEY || process.env.VITE_POSTHOG_KEY;
|
|
20
25
|
const host = process.env.POSTHOG_HOST || process.env.VITE_POSTHOG_HOST || "https://eu.i.posthog.com";
|
|
21
26
|
if (!key) return;
|
|
22
|
-
|
|
27
|
+
import("posthog-node").then(({ PostHog }) => {
|
|
28
|
+
client = new PostHog(key, { host, flushAt: 5, flushInterval: 1e4 });
|
|
29
|
+
}).catch(() => {
|
|
30
|
+
});
|
|
23
31
|
}
|
|
24
32
|
async function captureToolEvent(args) {
|
|
25
33
|
if (!client) return;
|
|
@@ -579,7 +587,7 @@ var TOOL_SCOPES = {
|
|
|
579
587
|
adapt_content: "mcp:write",
|
|
580
588
|
generate_video: "mcp:write",
|
|
581
589
|
generate_image: "mcp:write",
|
|
582
|
-
check_status: "mcp:
|
|
590
|
+
check_status: "mcp:read",
|
|
583
591
|
render_demo_video: "mcp:write",
|
|
584
592
|
save_brand_profile: "mcp:write",
|
|
585
593
|
update_platform_voice: "mcp:write",
|
|
@@ -767,7 +775,76 @@ async function callEdgeFunction(functionName, body, options) {
|
|
|
767
775
|
}
|
|
768
776
|
}
|
|
769
777
|
|
|
778
|
+
// src/lib/rate-limit.ts
|
|
779
|
+
var CATEGORY_CONFIGS = {
|
|
780
|
+
posting: { maxTokens: 30, refillRate: 30 / 60 },
|
|
781
|
+
// 30 req/min
|
|
782
|
+
screenshot: { maxTokens: 10, refillRate: 10 / 60 },
|
|
783
|
+
// 10 req/min
|
|
784
|
+
read: { maxTokens: 60, refillRate: 60 / 60 }
|
|
785
|
+
// 60 req/min
|
|
786
|
+
};
|
|
787
|
+
var RateLimiter = class {
|
|
788
|
+
tokens;
|
|
789
|
+
lastRefill;
|
|
790
|
+
maxTokens;
|
|
791
|
+
refillRate;
|
|
792
|
+
// tokens per second
|
|
793
|
+
constructor(config) {
|
|
794
|
+
this.maxTokens = config.maxTokens;
|
|
795
|
+
this.refillRate = config.refillRate;
|
|
796
|
+
this.tokens = config.maxTokens;
|
|
797
|
+
this.lastRefill = Date.now();
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Try to consume one token. Returns true if the request is allowed,
|
|
801
|
+
* false if rate-limited.
|
|
802
|
+
*/
|
|
803
|
+
consume() {
|
|
804
|
+
this.refill();
|
|
805
|
+
if (this.tokens >= 1) {
|
|
806
|
+
this.tokens -= 1;
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Seconds until at least one token is available.
|
|
813
|
+
*/
|
|
814
|
+
retryAfter() {
|
|
815
|
+
this.refill();
|
|
816
|
+
if (this.tokens >= 1) return 0;
|
|
817
|
+
return Math.ceil((1 - this.tokens) / this.refillRate);
|
|
818
|
+
}
|
|
819
|
+
refill() {
|
|
820
|
+
const now = Date.now();
|
|
821
|
+
const elapsed = (now - this.lastRefill) / 1e3;
|
|
822
|
+
this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
|
|
823
|
+
this.lastRefill = now;
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
var limiters = /* @__PURE__ */ new Map();
|
|
827
|
+
function getRateLimiter(category) {
|
|
828
|
+
let limiter = limiters.get(category);
|
|
829
|
+
if (!limiter) {
|
|
830
|
+
const config = CATEGORY_CONFIGS[category] ?? CATEGORY_CONFIGS.read;
|
|
831
|
+
limiter = new RateLimiter(config);
|
|
832
|
+
limiters.set(category, limiter);
|
|
833
|
+
}
|
|
834
|
+
return limiter;
|
|
835
|
+
}
|
|
836
|
+
function checkRateLimit(category, key) {
|
|
837
|
+
const bucketKey = key ? `${category}:${key}` : category;
|
|
838
|
+
const limiter = getRateLimiter(bucketKey);
|
|
839
|
+
const allowed = limiter.consume();
|
|
840
|
+
return {
|
|
841
|
+
allowed,
|
|
842
|
+
retryAfter: allowed ? 0 : limiter.retryAfter()
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
770
846
|
// src/tools/ideation.ts
|
|
847
|
+
init_supabase();
|
|
771
848
|
function registerIdeationTools(server) {
|
|
772
849
|
server.tool(
|
|
773
850
|
"generate_content",
|
|
@@ -788,7 +865,9 @@ function registerIdeationTools(server) {
|
|
|
788
865
|
"facebook",
|
|
789
866
|
"threads",
|
|
790
867
|
"bluesky"
|
|
791
|
-
]).optional().describe(
|
|
868
|
+
]).optional().describe(
|
|
869
|
+
"Target social media platform. Helps tailor tone, length, and format."
|
|
870
|
+
),
|
|
792
871
|
brand_voice: z.string().max(500).optional().describe(
|
|
793
872
|
'Brand voice guidelines to follow (e.g. "professional and empathetic", "playful and Gen-Z"). Leave blank to use a neutral tone.'
|
|
794
873
|
),
|
|
@@ -799,7 +878,30 @@ function registerIdeationTools(server) {
|
|
|
799
878
|
"Project ID to auto-load brand profile and performance context for prompt enrichment."
|
|
800
879
|
)
|
|
801
880
|
},
|
|
802
|
-
async ({
|
|
881
|
+
async ({
|
|
882
|
+
prompt,
|
|
883
|
+
content_type,
|
|
884
|
+
platform: platform2,
|
|
885
|
+
brand_voice,
|
|
886
|
+
model,
|
|
887
|
+
project_id
|
|
888
|
+
}) => {
|
|
889
|
+
try {
|
|
890
|
+
const userId = await getDefaultUserId();
|
|
891
|
+
const rl = checkRateLimit("posting", userId);
|
|
892
|
+
if (!rl.allowed) {
|
|
893
|
+
return {
|
|
894
|
+
content: [
|
|
895
|
+
{
|
|
896
|
+
type: "text",
|
|
897
|
+
text: `Rate limited. Retry after ${rl.retryAfter}s.`
|
|
898
|
+
}
|
|
899
|
+
],
|
|
900
|
+
isError: true
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
} catch {
|
|
904
|
+
}
|
|
803
905
|
let enrichedPrompt = prompt;
|
|
804
906
|
if (platform2) {
|
|
805
907
|
enrichedPrompt += `
|
|
@@ -931,8 +1033,12 @@ Content Type: ${content_type}`;
|
|
|
931
1033
|
category: z.string().optional().describe(
|
|
932
1034
|
"Category filter (for YouTube). Examples: general, entertainment, education, tech, music, gaming, sports, news."
|
|
933
1035
|
),
|
|
934
|
-
niche: z.string().optional().describe(
|
|
935
|
-
|
|
1036
|
+
niche: z.string().optional().describe(
|
|
1037
|
+
"Niche keyword filter. Only return trends matching these keywords."
|
|
1038
|
+
),
|
|
1039
|
+
url: z.string().optional().describe(
|
|
1040
|
+
'Required when source is "rss" or "url". The feed or page URL to fetch.'
|
|
1041
|
+
),
|
|
936
1042
|
force_refresh: z.boolean().optional().describe("Skip the server-side cache and fetch fresh data.")
|
|
937
1043
|
},
|
|
938
1044
|
async ({ source, category, niche, url, force_refresh }) => {
|
|
@@ -1007,7 +1113,9 @@ Content Type: ${content_type}`;
|
|
|
1007
1113
|
"adapt_content",
|
|
1008
1114
|
"Adapt existing content for a different social media platform. Rewrites content to match the target platform's norms including character limits, hashtag style, tone, and CTA conventions.",
|
|
1009
1115
|
{
|
|
1010
|
-
content: z.string().max(5e3).describe(
|
|
1116
|
+
content: z.string().max(5e3).describe(
|
|
1117
|
+
"The content to adapt. Can be a caption, script, blog excerpt, or any text."
|
|
1118
|
+
),
|
|
1011
1119
|
source_platform: z.enum([
|
|
1012
1120
|
"youtube",
|
|
1013
1121
|
"tiktok",
|
|
@@ -1017,7 +1125,9 @@ Content Type: ${content_type}`;
|
|
|
1017
1125
|
"facebook",
|
|
1018
1126
|
"threads",
|
|
1019
1127
|
"bluesky"
|
|
1020
|
-
]).optional().describe(
|
|
1128
|
+
]).optional().describe(
|
|
1129
|
+
"The platform the content was originally written for. Helps preserve intent."
|
|
1130
|
+
),
|
|
1021
1131
|
target_platform: z.enum([
|
|
1022
1132
|
"youtube",
|
|
1023
1133
|
"tiktok",
|
|
@@ -1031,9 +1141,33 @@ Content Type: ${content_type}`;
|
|
|
1031
1141
|
brand_voice: z.string().max(500).optional().describe(
|
|
1032
1142
|
'Brand voice guidelines to maintain during adaptation (e.g. "professional", "playful").'
|
|
1033
1143
|
),
|
|
1034
|
-
project_id: z.string().uuid().optional().describe(
|
|
1144
|
+
project_id: z.string().uuid().optional().describe(
|
|
1145
|
+
"Optional project ID to load platform voice overrides from brand profile."
|
|
1146
|
+
)
|
|
1035
1147
|
},
|
|
1036
|
-
async ({
|
|
1148
|
+
async ({
|
|
1149
|
+
content,
|
|
1150
|
+
source_platform,
|
|
1151
|
+
target_platform,
|
|
1152
|
+
brand_voice,
|
|
1153
|
+
project_id
|
|
1154
|
+
}) => {
|
|
1155
|
+
try {
|
|
1156
|
+
const userId = await getDefaultUserId();
|
|
1157
|
+
const rl = checkRateLimit("posting", userId);
|
|
1158
|
+
if (!rl.allowed) {
|
|
1159
|
+
return {
|
|
1160
|
+
content: [
|
|
1161
|
+
{
|
|
1162
|
+
type: "text",
|
|
1163
|
+
text: `Rate limited. Retry after ${rl.retryAfter}s.`
|
|
1164
|
+
}
|
|
1165
|
+
],
|
|
1166
|
+
isError: true
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
} catch {
|
|
1170
|
+
}
|
|
1037
1171
|
const platformGuidelines = {
|
|
1038
1172
|
twitter: "Max 280 characters. Concise, punchy. 1-3 hashtags max. Thread-friendly.",
|
|
1039
1173
|
threads: "Max 500 characters. Conversational, opinion-driven. Minimal hashtags.",
|
|
@@ -1115,76 +1249,6 @@ ${content}`,
|
|
|
1115
1249
|
|
|
1116
1250
|
// src/tools/content.ts
|
|
1117
1251
|
import { z as z2 } from "zod";
|
|
1118
|
-
|
|
1119
|
-
// src/lib/rate-limit.ts
|
|
1120
|
-
var CATEGORY_CONFIGS = {
|
|
1121
|
-
posting: { maxTokens: 30, refillRate: 30 / 60 },
|
|
1122
|
-
// 30 req/min
|
|
1123
|
-
screenshot: { maxTokens: 10, refillRate: 10 / 60 },
|
|
1124
|
-
// 10 req/min
|
|
1125
|
-
read: { maxTokens: 60, refillRate: 60 / 60 }
|
|
1126
|
-
// 60 req/min
|
|
1127
|
-
};
|
|
1128
|
-
var RateLimiter = class {
|
|
1129
|
-
tokens;
|
|
1130
|
-
lastRefill;
|
|
1131
|
-
maxTokens;
|
|
1132
|
-
refillRate;
|
|
1133
|
-
// tokens per second
|
|
1134
|
-
constructor(config) {
|
|
1135
|
-
this.maxTokens = config.maxTokens;
|
|
1136
|
-
this.refillRate = config.refillRate;
|
|
1137
|
-
this.tokens = config.maxTokens;
|
|
1138
|
-
this.lastRefill = Date.now();
|
|
1139
|
-
}
|
|
1140
|
-
/**
|
|
1141
|
-
* Try to consume one token. Returns true if the request is allowed,
|
|
1142
|
-
* false if rate-limited.
|
|
1143
|
-
*/
|
|
1144
|
-
consume() {
|
|
1145
|
-
this.refill();
|
|
1146
|
-
if (this.tokens >= 1) {
|
|
1147
|
-
this.tokens -= 1;
|
|
1148
|
-
return true;
|
|
1149
|
-
}
|
|
1150
|
-
return false;
|
|
1151
|
-
}
|
|
1152
|
-
/**
|
|
1153
|
-
* Seconds until at least one token is available.
|
|
1154
|
-
*/
|
|
1155
|
-
retryAfter() {
|
|
1156
|
-
this.refill();
|
|
1157
|
-
if (this.tokens >= 1) return 0;
|
|
1158
|
-
return Math.ceil((1 - this.tokens) / this.refillRate);
|
|
1159
|
-
}
|
|
1160
|
-
refill() {
|
|
1161
|
-
const now = Date.now();
|
|
1162
|
-
const elapsed = (now - this.lastRefill) / 1e3;
|
|
1163
|
-
this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
|
|
1164
|
-
this.lastRefill = now;
|
|
1165
|
-
}
|
|
1166
|
-
};
|
|
1167
|
-
var limiters = /* @__PURE__ */ new Map();
|
|
1168
|
-
function getRateLimiter(category) {
|
|
1169
|
-
let limiter = limiters.get(category);
|
|
1170
|
-
if (!limiter) {
|
|
1171
|
-
const config = CATEGORY_CONFIGS[category] ?? CATEGORY_CONFIGS.read;
|
|
1172
|
-
limiter = new RateLimiter(config);
|
|
1173
|
-
limiters.set(category, limiter);
|
|
1174
|
-
}
|
|
1175
|
-
return limiter;
|
|
1176
|
-
}
|
|
1177
|
-
function checkRateLimit(category, key) {
|
|
1178
|
-
const bucketKey = key ? `${category}:${key}` : category;
|
|
1179
|
-
const limiter = getRateLimiter(bucketKey);
|
|
1180
|
-
const allowed = limiter.consume();
|
|
1181
|
-
return {
|
|
1182
|
-
allowed,
|
|
1183
|
-
retryAfter: allowed ? 0 : limiter.retryAfter()
|
|
1184
|
-
};
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
// src/tools/content.ts
|
|
1188
1252
|
init_supabase();
|
|
1189
1253
|
|
|
1190
1254
|
// src/lib/sanitize-error.ts
|
|
@@ -1238,8 +1302,19 @@ function sanitizeDbError(error) {
|
|
|
1238
1302
|
|
|
1239
1303
|
// src/tools/content.ts
|
|
1240
1304
|
init_request_context();
|
|
1241
|
-
|
|
1242
|
-
|
|
1305
|
+
|
|
1306
|
+
// src/lib/version.ts
|
|
1307
|
+
var MCP_VERSION = "1.4.1";
|
|
1308
|
+
|
|
1309
|
+
// src/tools/content.ts
|
|
1310
|
+
var MAX_CREDITS_PER_RUN = Math.max(
|
|
1311
|
+
0,
|
|
1312
|
+
Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0)
|
|
1313
|
+
);
|
|
1314
|
+
var MAX_ASSETS_PER_RUN = Math.max(
|
|
1315
|
+
0,
|
|
1316
|
+
Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0)
|
|
1317
|
+
);
|
|
1243
1318
|
var _globalCreditsUsed = 0;
|
|
1244
1319
|
var _globalAssetsGenerated = 0;
|
|
1245
1320
|
function getCreditsUsed() {
|
|
@@ -1281,7 +1356,7 @@ function getCurrentBudgetStatus() {
|
|
|
1281
1356
|
function asEnvelope(data) {
|
|
1282
1357
|
return {
|
|
1283
1358
|
_meta: {
|
|
1284
|
-
version:
|
|
1359
|
+
version: MCP_VERSION,
|
|
1285
1360
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1286
1361
|
},
|
|
1287
1362
|
data
|
|
@@ -1363,8 +1438,12 @@ function registerContentTools(server) {
|
|
|
1363
1438
|
enable_audio: z2.boolean().optional().describe(
|
|
1364
1439
|
"Enable native audio generation. Kling 2.6: doubles cost. Kling 3.0: 50% more (std 30/sec, pro 40/sec). 5+ languages."
|
|
1365
1440
|
),
|
|
1366
|
-
image_url: z2.string().optional().describe(
|
|
1367
|
-
|
|
1441
|
+
image_url: z2.string().optional().describe(
|
|
1442
|
+
"Start frame image URL for image-to-video (Kling 3.0 frame control)."
|
|
1443
|
+
),
|
|
1444
|
+
end_frame_url: z2.string().optional().describe(
|
|
1445
|
+
"End frame image URL (Kling 3.0 only). Enables seamless loop transitions."
|
|
1446
|
+
),
|
|
1368
1447
|
response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
1369
1448
|
},
|
|
1370
1449
|
async ({
|
|
@@ -1802,10 +1881,13 @@ function registerContentTools(server) {
|
|
|
1802
1881
|
};
|
|
1803
1882
|
}
|
|
1804
1883
|
if (job.external_id && (job.status === "pending" || job.status === "processing")) {
|
|
1805
|
-
const { data: liveStatus } = await callEdgeFunction(
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1884
|
+
const { data: liveStatus } = await callEdgeFunction(
|
|
1885
|
+
"kie-task-status",
|
|
1886
|
+
{
|
|
1887
|
+
taskId: job.external_id,
|
|
1888
|
+
model: job.model
|
|
1889
|
+
}
|
|
1890
|
+
);
|
|
1809
1891
|
if (liveStatus) {
|
|
1810
1892
|
const lines2 = [
|
|
1811
1893
|
`Job: ${job.id}`,
|
|
@@ -1879,7 +1961,12 @@ function registerContentTools(server) {
|
|
|
1879
1961
|
});
|
|
1880
1962
|
if (format === "json") {
|
|
1881
1963
|
return {
|
|
1882
|
-
content: [
|
|
1964
|
+
content: [
|
|
1965
|
+
{
|
|
1966
|
+
type: "text",
|
|
1967
|
+
text: JSON.stringify(asEnvelope(job), null, 2)
|
|
1968
|
+
}
|
|
1969
|
+
]
|
|
1883
1970
|
};
|
|
1884
1971
|
}
|
|
1885
1972
|
return {
|
|
@@ -1897,7 +1984,15 @@ function registerContentTools(server) {
|
|
|
1897
1984
|
brand_context: z2.string().max(3e3).optional().describe(
|
|
1898
1985
|
"Brand context JSON from extract_brand. Include colors, voice tone, visual style keywords for consistent branding across frames."
|
|
1899
1986
|
),
|
|
1900
|
-
platform: z2.enum([
|
|
1987
|
+
platform: z2.enum([
|
|
1988
|
+
"tiktok",
|
|
1989
|
+
"instagram-reels",
|
|
1990
|
+
"youtube-shorts",
|
|
1991
|
+
"youtube",
|
|
1992
|
+
"general"
|
|
1993
|
+
]).describe(
|
|
1994
|
+
"Target platform. Determines aspect ratio, duration, and pacing."
|
|
1995
|
+
),
|
|
1901
1996
|
target_duration: z2.number().min(5).max(120).optional().describe(
|
|
1902
1997
|
"Target total duration in seconds. Defaults to 30s for short-form, 60s for YouTube."
|
|
1903
1998
|
),
|
|
@@ -1905,7 +2000,9 @@ function registerContentTools(server) {
|
|
|
1905
2000
|
style: z2.string().optional().describe(
|
|
1906
2001
|
'Visual style direction (e.g., "cinematic", "anime", "documentary", "motion graphics").'
|
|
1907
2002
|
),
|
|
1908
|
-
response_format: z2.enum(["text", "json"]).optional().describe(
|
|
2003
|
+
response_format: z2.enum(["text", "json"]).optional().describe(
|
|
2004
|
+
"Response format. Defaults to json for structured storyboard data."
|
|
2005
|
+
)
|
|
1909
2006
|
},
|
|
1910
2007
|
async ({
|
|
1911
2008
|
concept,
|
|
@@ -1918,7 +2015,11 @@ function registerContentTools(server) {
|
|
|
1918
2015
|
}) => {
|
|
1919
2016
|
const format = response_format ?? "json";
|
|
1920
2017
|
const startedAt = Date.now();
|
|
1921
|
-
const isShortForm = [
|
|
2018
|
+
const isShortForm = [
|
|
2019
|
+
"tiktok",
|
|
2020
|
+
"instagram-reels",
|
|
2021
|
+
"youtube-shorts"
|
|
2022
|
+
].includes(platform2);
|
|
1922
2023
|
const duration = target_duration ?? (isShortForm ? 30 : 60);
|
|
1923
2024
|
const scenes = num_scenes ?? (isShortForm ? 7 : 10);
|
|
1924
2025
|
const aspectRatio = isShortForm ? "9:16" : "16:9";
|
|
@@ -2011,7 +2112,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
2011
2112
|
details: { error }
|
|
2012
2113
|
});
|
|
2013
2114
|
return {
|
|
2014
|
-
content: [
|
|
2115
|
+
content: [
|
|
2116
|
+
{
|
|
2117
|
+
type: "text",
|
|
2118
|
+
text: `Storyboard generation failed: ${error}`
|
|
2119
|
+
}
|
|
2120
|
+
],
|
|
2015
2121
|
isError: true
|
|
2016
2122
|
};
|
|
2017
2123
|
}
|
|
@@ -2027,7 +2133,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
2027
2133
|
try {
|
|
2028
2134
|
const parsed = JSON.parse(rawContent);
|
|
2029
2135
|
return {
|
|
2030
|
-
content: [
|
|
2136
|
+
content: [
|
|
2137
|
+
{
|
|
2138
|
+
type: "text",
|
|
2139
|
+
text: JSON.stringify(asEnvelope(parsed), null, 2)
|
|
2140
|
+
}
|
|
2141
|
+
]
|
|
2031
2142
|
};
|
|
2032
2143
|
} catch {
|
|
2033
2144
|
return {
|
|
@@ -2091,7 +2202,10 @@ Return ONLY valid JSON in this exact format:
|
|
|
2091
2202
|
isError: true
|
|
2092
2203
|
};
|
|
2093
2204
|
}
|
|
2094
|
-
const rateLimit = checkRateLimit(
|
|
2205
|
+
const rateLimit = checkRateLimit(
|
|
2206
|
+
"posting",
|
|
2207
|
+
`generate_voiceover:${userId}`
|
|
2208
|
+
);
|
|
2095
2209
|
if (!rateLimit.allowed) {
|
|
2096
2210
|
await logMcpToolInvocation({
|
|
2097
2211
|
toolName: "generate_voiceover",
|
|
@@ -2126,7 +2240,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
2126
2240
|
details: { error }
|
|
2127
2241
|
});
|
|
2128
2242
|
return {
|
|
2129
|
-
content: [
|
|
2243
|
+
content: [
|
|
2244
|
+
{
|
|
2245
|
+
type: "text",
|
|
2246
|
+
text: `Voiceover generation failed: ${error}`
|
|
2247
|
+
}
|
|
2248
|
+
],
|
|
2130
2249
|
isError: true
|
|
2131
2250
|
};
|
|
2132
2251
|
}
|
|
@@ -2139,7 +2258,10 @@ Return ONLY valid JSON in this exact format:
|
|
|
2139
2258
|
});
|
|
2140
2259
|
return {
|
|
2141
2260
|
content: [
|
|
2142
|
-
{
|
|
2261
|
+
{
|
|
2262
|
+
type: "text",
|
|
2263
|
+
text: "Voiceover generation failed: no audio URL returned."
|
|
2264
|
+
}
|
|
2143
2265
|
],
|
|
2144
2266
|
isError: true
|
|
2145
2267
|
};
|
|
@@ -2211,7 +2333,9 @@ Return ONLY valid JSON in this exact format:
|
|
|
2211
2333
|
"Carousel template. hormozi-authority: bold typography, one idea per slide, dark backgrounds. educational-series: numbered tips. Default: hormozi-authority."
|
|
2212
2334
|
),
|
|
2213
2335
|
slide_count: z2.number().min(3).max(10).optional().describe("Number of slides (3-10). Default: 7."),
|
|
2214
|
-
aspect_ratio: z2.enum(["1:1", "4:5", "9:16"]).optional().describe(
|
|
2336
|
+
aspect_ratio: z2.enum(["1:1", "4:5", "9:16"]).optional().describe(
|
|
2337
|
+
"Aspect ratio. 1:1 square (default), 4:5 portrait, 9:16 story."
|
|
2338
|
+
),
|
|
2215
2339
|
style: z2.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
|
|
2216
2340
|
"Visual style. hormozi: black bg, bold white text, gold accents. Default: hormozi (when using hormozi-authority template)."
|
|
2217
2341
|
),
|
|
@@ -2248,7 +2372,10 @@ Return ONLY valid JSON in this exact format:
|
|
|
2248
2372
|
};
|
|
2249
2373
|
}
|
|
2250
2374
|
const userId = await getDefaultUserId();
|
|
2251
|
-
const rateLimit = checkRateLimit(
|
|
2375
|
+
const rateLimit = checkRateLimit(
|
|
2376
|
+
"posting",
|
|
2377
|
+
`generate_carousel:${userId}`
|
|
2378
|
+
);
|
|
2252
2379
|
if (!rateLimit.allowed) {
|
|
2253
2380
|
await logMcpToolInvocation({
|
|
2254
2381
|
toolName: "generate_carousel",
|
|
@@ -2286,7 +2413,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
2286
2413
|
details: { error }
|
|
2287
2414
|
});
|
|
2288
2415
|
return {
|
|
2289
|
-
content: [
|
|
2416
|
+
content: [
|
|
2417
|
+
{
|
|
2418
|
+
type: "text",
|
|
2419
|
+
text: `Carousel generation failed: ${error}`
|
|
2420
|
+
}
|
|
2421
|
+
],
|
|
2290
2422
|
isError: true
|
|
2291
2423
|
};
|
|
2292
2424
|
}
|
|
@@ -2298,7 +2430,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
2298
2430
|
details: { error: "No carousel data returned" }
|
|
2299
2431
|
});
|
|
2300
2432
|
return {
|
|
2301
|
-
content: [
|
|
2433
|
+
content: [
|
|
2434
|
+
{
|
|
2435
|
+
type: "text",
|
|
2436
|
+
text: "Carousel generation returned no data."
|
|
2437
|
+
}
|
|
2438
|
+
],
|
|
2302
2439
|
isError: true
|
|
2303
2440
|
};
|
|
2304
2441
|
}
|
|
@@ -2499,7 +2636,7 @@ var PLATFORM_CASE_MAP = {
|
|
|
2499
2636
|
function asEnvelope2(data) {
|
|
2500
2637
|
return {
|
|
2501
2638
|
_meta: {
|
|
2502
|
-
version:
|
|
2639
|
+
version: MCP_VERSION,
|
|
2503
2640
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2504
2641
|
},
|
|
2505
2642
|
data
|
|
@@ -2531,15 +2668,21 @@ function registerDistributionTools(server) {
|
|
|
2531
2668
|
"threads",
|
|
2532
2669
|
"bluesky"
|
|
2533
2670
|
])
|
|
2534
|
-
).min(1).describe(
|
|
2671
|
+
).min(1).describe(
|
|
2672
|
+
"Target platforms to post to. Each must have an active OAuth connection."
|
|
2673
|
+
),
|
|
2535
2674
|
title: z3.string().optional().describe("Post title (used by YouTube and some other platforms)."),
|
|
2536
|
-
hashtags: z3.array(z3.string()).optional().describe(
|
|
2675
|
+
hashtags: z3.array(z3.string()).optional().describe(
|
|
2676
|
+
'Hashtags to append to the caption. Include or omit the "#" prefix.'
|
|
2677
|
+
),
|
|
2537
2678
|
schedule_at: z3.string().optional().describe(
|
|
2538
2679
|
'ISO 8601 datetime for scheduled posting (e.g. "2026-03-15T14:00:00Z"). Omit for immediate posting.'
|
|
2539
2680
|
),
|
|
2540
2681
|
project_id: z3.string().optional().describe("Social Neuron project ID to associate this post with."),
|
|
2541
2682
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
|
|
2542
|
-
attribution: z3.boolean().optional().describe(
|
|
2683
|
+
attribution: z3.boolean().optional().describe(
|
|
2684
|
+
'If true, appends "Created with Social Neuron" to the caption. Default: false.'
|
|
2685
|
+
)
|
|
2543
2686
|
},
|
|
2544
2687
|
async ({
|
|
2545
2688
|
media_url,
|
|
@@ -2558,7 +2701,12 @@ function registerDistributionTools(server) {
|
|
|
2558
2701
|
const startedAt = Date.now();
|
|
2559
2702
|
if ((!caption || caption.trim().length === 0) && (!title || title.trim().length === 0)) {
|
|
2560
2703
|
return {
|
|
2561
|
-
content: [
|
|
2704
|
+
content: [
|
|
2705
|
+
{
|
|
2706
|
+
type: "text",
|
|
2707
|
+
text: "Either caption or title is required."
|
|
2708
|
+
}
|
|
2709
|
+
],
|
|
2562
2710
|
isError: true
|
|
2563
2711
|
};
|
|
2564
2712
|
}
|
|
@@ -2581,7 +2729,9 @@ function registerDistributionTools(server) {
|
|
|
2581
2729
|
isError: true
|
|
2582
2730
|
};
|
|
2583
2731
|
}
|
|
2584
|
-
const normalizedPlatforms = platforms.map(
|
|
2732
|
+
const normalizedPlatforms = platforms.map(
|
|
2733
|
+
(p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
|
|
2734
|
+
);
|
|
2585
2735
|
let finalCaption = caption;
|
|
2586
2736
|
if (attribution && finalCaption) {
|
|
2587
2737
|
finalCaption = `${finalCaption}
|
|
@@ -2645,7 +2795,9 @@ Created with Social Neuron`;
|
|
|
2645
2795
|
];
|
|
2646
2796
|
for (const [platform2, result] of Object.entries(data.results)) {
|
|
2647
2797
|
if (result.success) {
|
|
2648
|
-
lines.push(
|
|
2798
|
+
lines.push(
|
|
2799
|
+
` ${platform2}: OK (jobId=${result.jobId}, postId=${result.postId})`
|
|
2800
|
+
);
|
|
2649
2801
|
} else {
|
|
2650
2802
|
lines.push(` ${platform2}: FAILED - ${result.error}`);
|
|
2651
2803
|
}
|
|
@@ -2662,7 +2814,12 @@ Created with Social Neuron`;
|
|
|
2662
2814
|
});
|
|
2663
2815
|
if (format === "json") {
|
|
2664
2816
|
return {
|
|
2665
|
-
content: [
|
|
2817
|
+
content: [
|
|
2818
|
+
{
|
|
2819
|
+
type: "text",
|
|
2820
|
+
text: JSON.stringify(asEnvelope2(data), null, 2)
|
|
2821
|
+
}
|
|
2822
|
+
],
|
|
2666
2823
|
isError: !data.success
|
|
2667
2824
|
};
|
|
2668
2825
|
}
|
|
@@ -2718,12 +2875,17 @@ Created with Social Neuron`;
|
|
|
2718
2875
|
for (const account of accounts) {
|
|
2719
2876
|
const name = account.username || "(unnamed)";
|
|
2720
2877
|
const platformLower = account.platform.toLowerCase();
|
|
2721
|
-
lines.push(
|
|
2878
|
+
lines.push(
|
|
2879
|
+
` ${platformLower}: ${name} (connected ${account.created_at.split("T")[0]})`
|
|
2880
|
+
);
|
|
2722
2881
|
}
|
|
2723
2882
|
if (format === "json") {
|
|
2724
2883
|
return {
|
|
2725
2884
|
content: [
|
|
2726
|
-
{
|
|
2885
|
+
{
|
|
2886
|
+
type: "text",
|
|
2887
|
+
text: JSON.stringify(asEnvelope2({ accounts }), null, 2)
|
|
2888
|
+
}
|
|
2727
2889
|
]
|
|
2728
2890
|
};
|
|
2729
2891
|
}
|
|
@@ -2785,7 +2947,10 @@ Created with Social Neuron`;
|
|
|
2785
2947
|
if (format === "json") {
|
|
2786
2948
|
return {
|
|
2787
2949
|
content: [
|
|
2788
|
-
{
|
|
2950
|
+
{
|
|
2951
|
+
type: "text",
|
|
2952
|
+
text: JSON.stringify(asEnvelope2({ posts: [] }), null, 2)
|
|
2953
|
+
}
|
|
2789
2954
|
]
|
|
2790
2955
|
};
|
|
2791
2956
|
}
|
|
@@ -2802,7 +2967,10 @@ Created with Social Neuron`;
|
|
|
2802
2967
|
if (format === "json") {
|
|
2803
2968
|
return {
|
|
2804
2969
|
content: [
|
|
2805
|
-
{
|
|
2970
|
+
{
|
|
2971
|
+
type: "text",
|
|
2972
|
+
text: JSON.stringify(asEnvelope2({ posts }), null, 2)
|
|
2973
|
+
}
|
|
2806
2974
|
]
|
|
2807
2975
|
};
|
|
2808
2976
|
}
|
|
@@ -2863,7 +3031,13 @@ Created with Social Neuron`;
|
|
|
2863
3031
|
min_gap_hours: z3.number().min(1).max(24).default(4).describe("Minimum gap between posts on same platform"),
|
|
2864
3032
|
response_format: z3.enum(["text", "json"]).default("text")
|
|
2865
3033
|
},
|
|
2866
|
-
async ({
|
|
3034
|
+
async ({
|
|
3035
|
+
platforms,
|
|
3036
|
+
count,
|
|
3037
|
+
start_after,
|
|
3038
|
+
min_gap_hours,
|
|
3039
|
+
response_format
|
|
3040
|
+
}) => {
|
|
2867
3041
|
const startedAt = Date.now();
|
|
2868
3042
|
try {
|
|
2869
3043
|
const userId = await getDefaultUserId();
|
|
@@ -2874,7 +3048,9 @@ Created with Social Neuron`;
|
|
|
2874
3048
|
const gapMs = min_gap_hours * 60 * 60 * 1e3;
|
|
2875
3049
|
const candidates = [];
|
|
2876
3050
|
for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
|
|
2877
|
-
const date = new Date(
|
|
3051
|
+
const date = new Date(
|
|
3052
|
+
startDate.getTime() + dayOffset * 24 * 60 * 60 * 1e3
|
|
3053
|
+
);
|
|
2878
3054
|
const dayOfWeek = date.getUTCDay();
|
|
2879
3055
|
for (const platform2 of platforms) {
|
|
2880
3056
|
const hours = PREFERRED_HOURS[platform2] ?? [12, 16];
|
|
@@ -2883,8 +3059,11 @@ Created with Social Neuron`;
|
|
|
2883
3059
|
slotDate.setUTCHours(hours[hourIdx], 0, 0, 0);
|
|
2884
3060
|
if (slotDate <= startDate) continue;
|
|
2885
3061
|
const hasConflict = (existingPosts ?? []).some((post) => {
|
|
2886
|
-
if (String(post.platform).toLowerCase() !== platform2)
|
|
2887
|
-
|
|
3062
|
+
if (String(post.platform).toLowerCase() !== platform2)
|
|
3063
|
+
return false;
|
|
3064
|
+
const postTime = new Date(
|
|
3065
|
+
post.scheduled_at ?? post.published_at
|
|
3066
|
+
).getTime();
|
|
2888
3067
|
return Math.abs(postTime - slotDate.getTime()) < gapMs;
|
|
2889
3068
|
});
|
|
2890
3069
|
let engagementScore = hours.length - hourIdx;
|
|
@@ -2929,15 +3108,22 @@ Created with Social Neuron`;
|
|
|
2929
3108
|
};
|
|
2930
3109
|
}
|
|
2931
3110
|
const lines = [];
|
|
2932
|
-
lines.push(
|
|
3111
|
+
lines.push(
|
|
3112
|
+
`Found ${slots.length} optimal slots (${conflictsAvoided} conflicts avoided):`
|
|
3113
|
+
);
|
|
2933
3114
|
lines.push("");
|
|
2934
3115
|
lines.push("Datetime (UTC) | Platform | Score");
|
|
2935
3116
|
lines.push("-------------------------+------------+------");
|
|
2936
3117
|
for (const s of slots) {
|
|
2937
3118
|
const dt = s.datetime.replace("T", " ").slice(0, 19);
|
|
2938
|
-
lines.push(
|
|
3119
|
+
lines.push(
|
|
3120
|
+
`${dt.padEnd(25)}| ${s.platform.padEnd(11)}| ${s.engagement_score}`
|
|
3121
|
+
);
|
|
2939
3122
|
}
|
|
2940
|
-
return {
|
|
3123
|
+
return {
|
|
3124
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
3125
|
+
isError: false
|
|
3126
|
+
};
|
|
2941
3127
|
} catch (err) {
|
|
2942
3128
|
const durationMs = Date.now() - startedAt;
|
|
2943
3129
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2948,7 +3134,9 @@ Created with Social Neuron`;
|
|
|
2948
3134
|
details: { error: message }
|
|
2949
3135
|
});
|
|
2950
3136
|
return {
|
|
2951
|
-
content: [
|
|
3137
|
+
content: [
|
|
3138
|
+
{ type: "text", text: `Failed to find slots: ${message}` }
|
|
3139
|
+
],
|
|
2952
3140
|
isError: true
|
|
2953
3141
|
};
|
|
2954
3142
|
}
|
|
@@ -2975,8 +3163,12 @@ Created with Social Neuron`;
|
|
|
2975
3163
|
auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
|
|
2976
3164
|
dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
|
|
2977
3165
|
response_format: z3.enum(["text", "json"]).default("text"),
|
|
2978
|
-
enforce_quality: z3.boolean().default(true).describe(
|
|
2979
|
-
|
|
3166
|
+
enforce_quality: z3.boolean().default(true).describe(
|
|
3167
|
+
"When true, block scheduling for posts that fail quality checks."
|
|
3168
|
+
),
|
|
3169
|
+
quality_threshold: z3.number().int().min(0).max(35).optional().describe(
|
|
3170
|
+
"Optional quality threshold override. Defaults to project setting or 26."
|
|
3171
|
+
),
|
|
2980
3172
|
batch_size: z3.number().int().min(1).max(10).default(4).describe("Concurrent schedule calls per platform batch."),
|
|
2981
3173
|
idempotency_seed: z3.string().max(128).optional().describe("Optional stable seed used when building idempotency keys.")
|
|
2982
3174
|
},
|
|
@@ -3015,17 +3207,25 @@ Created with Social Neuron`;
|
|
|
3015
3207
|
if (!stored?.plan_payload) {
|
|
3016
3208
|
return {
|
|
3017
3209
|
content: [
|
|
3018
|
-
{
|
|
3210
|
+
{
|
|
3211
|
+
type: "text",
|
|
3212
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
3213
|
+
}
|
|
3019
3214
|
],
|
|
3020
3215
|
isError: true
|
|
3021
3216
|
};
|
|
3022
3217
|
}
|
|
3023
3218
|
const payload = stored.plan_payload;
|
|
3024
|
-
const postsFromPayload = Array.isArray(payload.posts) ? payload.posts : Array.isArray(
|
|
3219
|
+
const postsFromPayload = Array.isArray(payload.posts) ? payload.posts : Array.isArray(
|
|
3220
|
+
payload.data?.posts
|
|
3221
|
+
) ? payload.data.posts : null;
|
|
3025
3222
|
if (!postsFromPayload) {
|
|
3026
3223
|
return {
|
|
3027
3224
|
content: [
|
|
3028
|
-
{
|
|
3225
|
+
{
|
|
3226
|
+
type: "text",
|
|
3227
|
+
text: `Stored plan ${plan_id} has no posts array.`
|
|
3228
|
+
}
|
|
3029
3229
|
],
|
|
3030
3230
|
isError: true
|
|
3031
3231
|
};
|
|
@@ -3117,7 +3317,10 @@ Created with Social Neuron`;
|
|
|
3117
3317
|
approvalSummary = {
|
|
3118
3318
|
total: approvals.length,
|
|
3119
3319
|
eligible: approvedPosts.length,
|
|
3120
|
-
skipped: Math.max(
|
|
3320
|
+
skipped: Math.max(
|
|
3321
|
+
0,
|
|
3322
|
+
workingPlan.posts.length - approvedPosts.length
|
|
3323
|
+
)
|
|
3121
3324
|
};
|
|
3122
3325
|
workingPlan = {
|
|
3123
3326
|
...workingPlan,
|
|
@@ -3151,9 +3354,14 @@ Created with Social Neuron`;
|
|
|
3151
3354
|
try {
|
|
3152
3355
|
const { data: settingsData } = await supabase.from("system_settings").select("value").eq("key", "content_safety").maybeSingle();
|
|
3153
3356
|
if (settingsData?.value?.quality_threshold !== void 0) {
|
|
3154
|
-
const parsedThreshold = Number(
|
|
3357
|
+
const parsedThreshold = Number(
|
|
3358
|
+
settingsData.value.quality_threshold
|
|
3359
|
+
);
|
|
3155
3360
|
if (Number.isFinite(parsedThreshold)) {
|
|
3156
|
-
effectiveQualityThreshold = Math.max(
|
|
3361
|
+
effectiveQualityThreshold = Math.max(
|
|
3362
|
+
0,
|
|
3363
|
+
Math.min(35, Math.trunc(parsedThreshold))
|
|
3364
|
+
);
|
|
3157
3365
|
}
|
|
3158
3366
|
}
|
|
3159
3367
|
if (Array.isArray(settingsData?.value?.custom_banned_terms)) {
|
|
@@ -3189,13 +3397,18 @@ Created with Social Neuron`;
|
|
|
3189
3397
|
}
|
|
3190
3398
|
};
|
|
3191
3399
|
});
|
|
3192
|
-
const qualityPassed = postsWithResults.filter(
|
|
3400
|
+
const qualityPassed = postsWithResults.filter(
|
|
3401
|
+
(post) => post.quality.passed
|
|
3402
|
+
).length;
|
|
3193
3403
|
const qualitySummary = {
|
|
3194
3404
|
total_posts: postsWithResults.length,
|
|
3195
3405
|
passed: qualityPassed,
|
|
3196
3406
|
failed: postsWithResults.length - qualityPassed,
|
|
3197
3407
|
avg_score: postsWithResults.length > 0 ? Number(
|
|
3198
|
-
(postsWithResults.reduce(
|
|
3408
|
+
(postsWithResults.reduce(
|
|
3409
|
+
(sum, post) => sum + post.quality.score,
|
|
3410
|
+
0
|
|
3411
|
+
) / postsWithResults.length).toFixed(2)
|
|
3199
3412
|
) : 0
|
|
3200
3413
|
};
|
|
3201
3414
|
if (dry_run) {
|
|
@@ -3265,8 +3478,13 @@ Created with Social Neuron`;
|
|
|
3265
3478
|
}
|
|
3266
3479
|
}
|
|
3267
3480
|
lines2.push("");
|
|
3268
|
-
lines2.push(
|
|
3269
|
-
|
|
3481
|
+
lines2.push(
|
|
3482
|
+
`Summary: ${passed}/${workingPlan.posts.length} passed quality check`
|
|
3483
|
+
);
|
|
3484
|
+
return {
|
|
3485
|
+
content: [{ type: "text", text: lines2.join("\n") }],
|
|
3486
|
+
isError: false
|
|
3487
|
+
};
|
|
3270
3488
|
}
|
|
3271
3489
|
let scheduled = 0;
|
|
3272
3490
|
let failed = 0;
|
|
@@ -3358,7 +3576,8 @@ Created with Social Neuron`;
|
|
|
3358
3576
|
}
|
|
3359
3577
|
const chunk = (arr, size) => {
|
|
3360
3578
|
const out = [];
|
|
3361
|
-
for (let i = 0; i < arr.length; i += size)
|
|
3579
|
+
for (let i = 0; i < arr.length; i += size)
|
|
3580
|
+
out.push(arr.slice(i, i + size));
|
|
3362
3581
|
return out;
|
|
3363
3582
|
};
|
|
3364
3583
|
const platformBatches = Array.from(grouped.entries()).map(
|
|
@@ -3366,7 +3585,9 @@ Created with Social Neuron`;
|
|
|
3366
3585
|
const platformResults = [];
|
|
3367
3586
|
const batches = chunk(platformPosts, batch_size);
|
|
3368
3587
|
for (const batch of batches) {
|
|
3369
|
-
const settled = await Promise.allSettled(
|
|
3588
|
+
const settled = await Promise.allSettled(
|
|
3589
|
+
batch.map((post) => scheduleOne(post))
|
|
3590
|
+
);
|
|
3370
3591
|
for (const outcome of settled) {
|
|
3371
3592
|
if (outcome.status === "fulfilled") {
|
|
3372
3593
|
platformResults.push(outcome.value);
|
|
@@ -3432,7 +3653,11 @@ Created with Social Neuron`;
|
|
|
3432
3653
|
plan_id: effectivePlanId,
|
|
3433
3654
|
approvals: approvalSummary,
|
|
3434
3655
|
posts: results,
|
|
3435
|
-
summary: {
|
|
3656
|
+
summary: {
|
|
3657
|
+
total_posts: workingPlan.posts.length,
|
|
3658
|
+
scheduled,
|
|
3659
|
+
failed
|
|
3660
|
+
}
|
|
3436
3661
|
}),
|
|
3437
3662
|
null,
|
|
3438
3663
|
2
|
|
@@ -3470,7 +3695,12 @@ Created with Social Neuron`;
|
|
|
3470
3695
|
details: { error: message }
|
|
3471
3696
|
});
|
|
3472
3697
|
return {
|
|
3473
|
-
content: [
|
|
3698
|
+
content: [
|
|
3699
|
+
{
|
|
3700
|
+
type: "text",
|
|
3701
|
+
text: `Batch scheduling failed: ${message}`
|
|
3702
|
+
}
|
|
3703
|
+
],
|
|
3474
3704
|
isError: true
|
|
3475
3705
|
};
|
|
3476
3706
|
}
|
|
@@ -3484,7 +3714,7 @@ import { z as z4 } from "zod";
|
|
|
3484
3714
|
function asEnvelope3(data) {
|
|
3485
3715
|
return {
|
|
3486
3716
|
_meta: {
|
|
3487
|
-
version:
|
|
3717
|
+
version: MCP_VERSION,
|
|
3488
3718
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3489
3719
|
},
|
|
3490
3720
|
data
|
|
@@ -3597,7 +3827,9 @@ function registerAnalyticsTools(server) {
|
|
|
3597
3827
|
]
|
|
3598
3828
|
};
|
|
3599
3829
|
}
|
|
3600
|
-
const { data: simpleRows, error: simpleError } = await supabase.from("post_analytics").select(
|
|
3830
|
+
const { data: simpleRows, error: simpleError } = await supabase.from("post_analytics").select(
|
|
3831
|
+
"id, post_id, platform, views, likes, comments, shares, captured_at"
|
|
3832
|
+
).in("post_id", postIds).gte("captured_at", sinceIso).order("captured_at", { ascending: false }).limit(maxPosts);
|
|
3601
3833
|
if (simpleError) {
|
|
3602
3834
|
return {
|
|
3603
3835
|
content: [
|
|
@@ -3609,7 +3841,12 @@ function registerAnalyticsTools(server) {
|
|
|
3609
3841
|
isError: true
|
|
3610
3842
|
};
|
|
3611
3843
|
}
|
|
3612
|
-
return formatSimpleAnalytics(
|
|
3844
|
+
return formatSimpleAnalytics(
|
|
3845
|
+
simpleRows,
|
|
3846
|
+
platform2,
|
|
3847
|
+
lookbackDays,
|
|
3848
|
+
format
|
|
3849
|
+
);
|
|
3613
3850
|
}
|
|
3614
3851
|
if (!rows || rows.length === 0) {
|
|
3615
3852
|
if (format === "json") {
|
|
@@ -3686,7 +3923,10 @@ function registerAnalyticsTools(server) {
|
|
|
3686
3923
|
const format = response_format ?? "text";
|
|
3687
3924
|
const startedAt = Date.now();
|
|
3688
3925
|
const userId = await getDefaultUserId();
|
|
3689
|
-
const rateLimit = checkRateLimit(
|
|
3926
|
+
const rateLimit = checkRateLimit(
|
|
3927
|
+
"posting",
|
|
3928
|
+
`refresh_platform_analytics:${userId}`
|
|
3929
|
+
);
|
|
3690
3930
|
if (!rateLimit.allowed) {
|
|
3691
3931
|
await logMcpToolInvocation({
|
|
3692
3932
|
toolName: "refresh_platform_analytics",
|
|
@@ -3704,7 +3944,9 @@ function registerAnalyticsTools(server) {
|
|
|
3704
3944
|
isError: true
|
|
3705
3945
|
};
|
|
3706
3946
|
}
|
|
3707
|
-
const { data, error } = await callEdgeFunction("fetch-analytics", {
|
|
3947
|
+
const { data, error } = await callEdgeFunction("fetch-analytics", {
|
|
3948
|
+
userId
|
|
3949
|
+
});
|
|
3708
3950
|
if (error) {
|
|
3709
3951
|
await logMcpToolInvocation({
|
|
3710
3952
|
toolName: "refresh_platform_analytics",
|
|
@@ -3713,7 +3955,12 @@ function registerAnalyticsTools(server) {
|
|
|
3713
3955
|
details: { error }
|
|
3714
3956
|
});
|
|
3715
3957
|
return {
|
|
3716
|
-
content: [
|
|
3958
|
+
content: [
|
|
3959
|
+
{
|
|
3960
|
+
type: "text",
|
|
3961
|
+
text: `Error refreshing analytics: ${error}`
|
|
3962
|
+
}
|
|
3963
|
+
],
|
|
3717
3964
|
isError: true
|
|
3718
3965
|
};
|
|
3719
3966
|
}
|
|
@@ -3726,12 +3973,18 @@ function registerAnalyticsTools(server) {
|
|
|
3726
3973
|
details: { error: "Edge function returned success=false" }
|
|
3727
3974
|
});
|
|
3728
3975
|
return {
|
|
3729
|
-
content: [
|
|
3976
|
+
content: [
|
|
3977
|
+
{ type: "text", text: "Analytics refresh failed." }
|
|
3978
|
+
],
|
|
3730
3979
|
isError: true
|
|
3731
3980
|
};
|
|
3732
3981
|
}
|
|
3733
|
-
const queued = (result.results ?? []).filter(
|
|
3734
|
-
|
|
3982
|
+
const queued = (result.results ?? []).filter(
|
|
3983
|
+
(r) => r.status === "queued"
|
|
3984
|
+
).length;
|
|
3985
|
+
const errored = (result.results ?? []).filter(
|
|
3986
|
+
(r) => r.status === "error"
|
|
3987
|
+
).length;
|
|
3735
3988
|
const lines = [
|
|
3736
3989
|
`Analytics refresh triggered successfully.`,
|
|
3737
3990
|
` Posts processed: ${result.postsProcessed}`,
|
|
@@ -3777,7 +4030,10 @@ function formatAnalytics(summary, days, format) {
|
|
|
3777
4030
|
if (format === "json") {
|
|
3778
4031
|
return {
|
|
3779
4032
|
content: [
|
|
3780
|
-
{
|
|
4033
|
+
{
|
|
4034
|
+
type: "text",
|
|
4035
|
+
text: JSON.stringify(asEnvelope3({ ...summary, days }), null, 2)
|
|
4036
|
+
}
|
|
3781
4037
|
]
|
|
3782
4038
|
};
|
|
3783
4039
|
}
|
|
@@ -3894,10 +4150,159 @@ function formatSimpleAnalytics(rows, platform2, days, format) {
|
|
|
3894
4150
|
// src/tools/brand.ts
|
|
3895
4151
|
import { z as z5 } from "zod";
|
|
3896
4152
|
init_supabase();
|
|
4153
|
+
|
|
4154
|
+
// src/lib/ssrf.ts
|
|
4155
|
+
var BLOCKED_IP_PATTERNS = [
|
|
4156
|
+
// IPv4 localhost/loopback
|
|
4157
|
+
/^127\./,
|
|
4158
|
+
/^0\./,
|
|
4159
|
+
// IPv4 private ranges (RFC 1918)
|
|
4160
|
+
/^10\./,
|
|
4161
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
|
4162
|
+
/^192\.168\./,
|
|
4163
|
+
// IPv4 link-local
|
|
4164
|
+
/^169\.254\./,
|
|
4165
|
+
// Cloud metadata endpoint (AWS, GCP, Azure)
|
|
4166
|
+
/^169\.254\.169\.254$/,
|
|
4167
|
+
// IPv4 broadcast
|
|
4168
|
+
/^255\./,
|
|
4169
|
+
// Shared address space (RFC 6598)
|
|
4170
|
+
/^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./
|
|
4171
|
+
];
|
|
4172
|
+
var BLOCKED_IPV6_PATTERNS = [
|
|
4173
|
+
/^::1$/i,
|
|
4174
|
+
// loopback
|
|
4175
|
+
/^::$/i,
|
|
4176
|
+
// unspecified
|
|
4177
|
+
/^fe[89ab][0-9a-f]:/i,
|
|
4178
|
+
// link-local fe80::/10
|
|
4179
|
+
/^fc[0-9a-f]:/i,
|
|
4180
|
+
// unique local fc00::/7
|
|
4181
|
+
/^fd[0-9a-f]:/i,
|
|
4182
|
+
// unique local fc00::/7
|
|
4183
|
+
/^::ffff:127\./i,
|
|
4184
|
+
// IPv4-mapped localhost
|
|
4185
|
+
/^::ffff:(0|10|127|169\.254|172\.(1[6-9]|2[0-9]|3[0-1])|192\.168)\./i
|
|
4186
|
+
// IPv4-mapped private
|
|
4187
|
+
];
|
|
4188
|
+
var BLOCKED_HOSTNAMES = [
|
|
4189
|
+
"localhost",
|
|
4190
|
+
"localhost.localdomain",
|
|
4191
|
+
"local",
|
|
4192
|
+
"127.0.0.1",
|
|
4193
|
+
"0.0.0.0",
|
|
4194
|
+
"[::1]",
|
|
4195
|
+
"[::ffff:127.0.0.1]",
|
|
4196
|
+
// Cloud metadata endpoints
|
|
4197
|
+
"metadata.google.internal",
|
|
4198
|
+
"metadata.goog",
|
|
4199
|
+
"instance-data",
|
|
4200
|
+
"instance-data.ec2.internal"
|
|
4201
|
+
];
|
|
4202
|
+
var ALLOWED_PROTOCOLS = ["http:", "https:"];
|
|
4203
|
+
var BLOCKED_PORTS = [22, 23, 25, 110, 143, 445, 3306, 5432, 6379, 27017, 11211];
|
|
4204
|
+
function isBlockedIP(ip) {
|
|
4205
|
+
const normalized = ip.replace(/^\[/, "").replace(/\]$/, "");
|
|
4206
|
+
if (normalized.includes(":")) {
|
|
4207
|
+
return BLOCKED_IPV6_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
4208
|
+
}
|
|
4209
|
+
return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
4210
|
+
}
|
|
4211
|
+
function isBlockedHostname(hostname) {
|
|
4212
|
+
return BLOCKED_HOSTNAMES.includes(hostname.toLowerCase());
|
|
4213
|
+
}
|
|
4214
|
+
function isIPAddress(hostname) {
|
|
4215
|
+
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
4216
|
+
const ipv6Pattern = /^\[?[a-fA-F0-9:]+\]?$/;
|
|
4217
|
+
return ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname);
|
|
4218
|
+
}
|
|
4219
|
+
async function validateUrlForSSRF(urlString) {
|
|
4220
|
+
try {
|
|
4221
|
+
const url = new URL(urlString);
|
|
4222
|
+
if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
|
|
4223
|
+
return {
|
|
4224
|
+
isValid: false,
|
|
4225
|
+
error: `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are allowed.`
|
|
4226
|
+
};
|
|
4227
|
+
}
|
|
4228
|
+
if (url.username || url.password) {
|
|
4229
|
+
return {
|
|
4230
|
+
isValid: false,
|
|
4231
|
+
error: "URLs with embedded credentials are not allowed."
|
|
4232
|
+
};
|
|
4233
|
+
}
|
|
4234
|
+
const hostname = url.hostname.toLowerCase();
|
|
4235
|
+
if (isBlockedHostname(hostname)) {
|
|
4236
|
+
return {
|
|
4237
|
+
isValid: false,
|
|
4238
|
+
error: "Access to internal/localhost addresses is not allowed."
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
if (isIPAddress(hostname) && isBlockedIP(hostname)) {
|
|
4242
|
+
return {
|
|
4243
|
+
isValid: false,
|
|
4244
|
+
error: "Access to private/internal IP addresses is not allowed."
|
|
4245
|
+
};
|
|
4246
|
+
}
|
|
4247
|
+
const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80;
|
|
4248
|
+
if (BLOCKED_PORTS.includes(port)) {
|
|
4249
|
+
return {
|
|
4250
|
+
isValid: false,
|
|
4251
|
+
error: `Access to port ${port} is not allowed.`
|
|
4252
|
+
};
|
|
4253
|
+
}
|
|
4254
|
+
let resolvedIP;
|
|
4255
|
+
if (!isIPAddress(hostname)) {
|
|
4256
|
+
try {
|
|
4257
|
+
const dns = await import("node:dns");
|
|
4258
|
+
const resolver = new dns.promises.Resolver();
|
|
4259
|
+
const resolvedIPs = [];
|
|
4260
|
+
try {
|
|
4261
|
+
const aRecords = await resolver.resolve4(hostname);
|
|
4262
|
+
resolvedIPs.push(...aRecords);
|
|
4263
|
+
} catch {
|
|
4264
|
+
}
|
|
4265
|
+
try {
|
|
4266
|
+
const aaaaRecords = await resolver.resolve6(hostname);
|
|
4267
|
+
resolvedIPs.push(...aaaaRecords);
|
|
4268
|
+
} catch {
|
|
4269
|
+
}
|
|
4270
|
+
if (resolvedIPs.length === 0) {
|
|
4271
|
+
return {
|
|
4272
|
+
isValid: false,
|
|
4273
|
+
error: "DNS resolution failed: hostname did not resolve to any address."
|
|
4274
|
+
};
|
|
4275
|
+
}
|
|
4276
|
+
for (const ip of resolvedIPs) {
|
|
4277
|
+
if (isBlockedIP(ip)) {
|
|
4278
|
+
return {
|
|
4279
|
+
isValid: false,
|
|
4280
|
+
error: "Hostname resolves to a private/internal IP address."
|
|
4281
|
+
};
|
|
4282
|
+
}
|
|
4283
|
+
}
|
|
4284
|
+
resolvedIP = resolvedIPs[0];
|
|
4285
|
+
} catch {
|
|
4286
|
+
return {
|
|
4287
|
+
isValid: false,
|
|
4288
|
+
error: "DNS resolution failed. Cannot verify hostname safety."
|
|
4289
|
+
};
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
return { isValid: true, sanitizedUrl: url.toString(), resolvedIP };
|
|
4293
|
+
} catch (error) {
|
|
4294
|
+
return {
|
|
4295
|
+
isValid: false,
|
|
4296
|
+
error: `Invalid URL format: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
4297
|
+
};
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4301
|
+
// src/tools/brand.ts
|
|
3897
4302
|
function asEnvelope4(data) {
|
|
3898
4303
|
return {
|
|
3899
4304
|
_meta: {
|
|
3900
|
-
version:
|
|
4305
|
+
version: MCP_VERSION,
|
|
3901
4306
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3902
4307
|
},
|
|
3903
4308
|
data
|
|
@@ -3914,6 +4319,15 @@ function registerBrandTools(server) {
|
|
|
3914
4319
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
3915
4320
|
},
|
|
3916
4321
|
async ({ url, response_format }) => {
|
|
4322
|
+
const ssrfCheck = await validateUrlForSSRF(url);
|
|
4323
|
+
if (!ssrfCheck.isValid) {
|
|
4324
|
+
return {
|
|
4325
|
+
content: [
|
|
4326
|
+
{ type: "text", text: `URL blocked: ${ssrfCheck.error}` }
|
|
4327
|
+
],
|
|
4328
|
+
isError: true
|
|
4329
|
+
};
|
|
4330
|
+
}
|
|
3917
4331
|
const { data, error } = await callEdgeFunction(
|
|
3918
4332
|
"brand-extract",
|
|
3919
4333
|
{ url },
|
|
@@ -3943,7 +4357,12 @@ function registerBrandTools(server) {
|
|
|
3943
4357
|
}
|
|
3944
4358
|
if ((response_format || "text") === "json") {
|
|
3945
4359
|
return {
|
|
3946
|
-
content: [
|
|
4360
|
+
content: [
|
|
4361
|
+
{
|
|
4362
|
+
type: "text",
|
|
4363
|
+
text: JSON.stringify(asEnvelope4(data), null, 2)
|
|
4364
|
+
}
|
|
4365
|
+
]
|
|
3947
4366
|
};
|
|
3948
4367
|
}
|
|
3949
4368
|
const lines = [
|
|
@@ -4007,7 +4426,12 @@ function registerBrandTools(server) {
|
|
|
4007
4426
|
const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
|
|
4008
4427
|
if (!membership) {
|
|
4009
4428
|
return {
|
|
4010
|
-
content: [
|
|
4429
|
+
content: [
|
|
4430
|
+
{
|
|
4431
|
+
type: "text",
|
|
4432
|
+
text: "Project is not accessible to current user."
|
|
4433
|
+
}
|
|
4434
|
+
],
|
|
4011
4435
|
isError: true
|
|
4012
4436
|
};
|
|
4013
4437
|
}
|
|
@@ -4026,13 +4450,21 @@ function registerBrandTools(server) {
|
|
|
4026
4450
|
if (!data) {
|
|
4027
4451
|
return {
|
|
4028
4452
|
content: [
|
|
4029
|
-
{
|
|
4453
|
+
{
|
|
4454
|
+
type: "text",
|
|
4455
|
+
text: "No active brand profile found for this project."
|
|
4456
|
+
}
|
|
4030
4457
|
]
|
|
4031
4458
|
};
|
|
4032
4459
|
}
|
|
4033
4460
|
if ((response_format || "text") === "json") {
|
|
4034
4461
|
return {
|
|
4035
|
-
content: [
|
|
4462
|
+
content: [
|
|
4463
|
+
{
|
|
4464
|
+
type: "text",
|
|
4465
|
+
text: JSON.stringify(asEnvelope4(data), null, 2)
|
|
4466
|
+
}
|
|
4467
|
+
]
|
|
4036
4468
|
};
|
|
4037
4469
|
}
|
|
4038
4470
|
const lines = [
|
|
@@ -4053,11 +4485,18 @@ function registerBrandTools(server) {
|
|
|
4053
4485
|
"Persist a brand profile as the active profile for a project.",
|
|
4054
4486
|
{
|
|
4055
4487
|
project_id: z5.string().uuid().optional().describe("Project ID. Defaults to active project context."),
|
|
4056
|
-
brand_context: z5.record(z5.string(), z5.unknown()).describe(
|
|
4488
|
+
brand_context: z5.record(z5.string(), z5.unknown()).describe(
|
|
4489
|
+
"Brand context payload to save to brand_profiles.brand_context."
|
|
4490
|
+
),
|
|
4057
4491
|
change_summary: z5.string().max(500).optional().describe("Optional summary of changes."),
|
|
4058
4492
|
changed_paths: z5.array(z5.string()).optional().describe("Optional changed path list."),
|
|
4059
4493
|
source_url: z5.string().url().optional().describe("Optional source URL for provenance."),
|
|
4060
|
-
extraction_method: z5.enum([
|
|
4494
|
+
extraction_method: z5.enum([
|
|
4495
|
+
"manual",
|
|
4496
|
+
"url_extract",
|
|
4497
|
+
"business_profiler",
|
|
4498
|
+
"product_showcase"
|
|
4499
|
+
]).optional().describe("Extraction method metadata."),
|
|
4061
4500
|
overall_confidence: z5.number().min(0).max(1).optional().describe("Optional overall confidence score in range 0..1."),
|
|
4062
4501
|
extraction_metadata: z5.record(z5.string(), z5.unknown()).optional(),
|
|
4063
4502
|
response_format: z5.enum(["text", "json"]).optional()
|
|
@@ -4097,20 +4536,28 @@ function registerBrandTools(server) {
|
|
|
4097
4536
|
const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
|
|
4098
4537
|
if (!membership) {
|
|
4099
4538
|
return {
|
|
4100
|
-
content: [
|
|
4539
|
+
content: [
|
|
4540
|
+
{
|
|
4541
|
+
type: "text",
|
|
4542
|
+
text: "Project is not accessible to current user."
|
|
4543
|
+
}
|
|
4544
|
+
],
|
|
4101
4545
|
isError: true
|
|
4102
4546
|
};
|
|
4103
4547
|
}
|
|
4104
|
-
const { data: profileId, error } = await supabase.rpc(
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4548
|
+
const { data: profileId, error } = await supabase.rpc(
|
|
4549
|
+
"set_active_brand_profile",
|
|
4550
|
+
{
|
|
4551
|
+
p_project_id: projectId,
|
|
4552
|
+
p_brand_context: brand_context,
|
|
4553
|
+
p_change_summary: change_summary || null,
|
|
4554
|
+
p_changed_paths: changed_paths || [],
|
|
4555
|
+
p_source_url: source_url || null,
|
|
4556
|
+
p_extraction_method: extraction_method || "manual",
|
|
4557
|
+
p_overall_confidence: overall_confidence ?? null,
|
|
4558
|
+
p_extraction_metadata: extraction_metadata || null
|
|
4559
|
+
}
|
|
4560
|
+
);
|
|
4114
4561
|
if (error) {
|
|
4115
4562
|
return {
|
|
4116
4563
|
content: [
|
|
@@ -4131,7 +4578,12 @@ function registerBrandTools(server) {
|
|
|
4131
4578
|
};
|
|
4132
4579
|
if ((response_format || "text") === "json") {
|
|
4133
4580
|
return {
|
|
4134
|
-
content: [
|
|
4581
|
+
content: [
|
|
4582
|
+
{
|
|
4583
|
+
type: "text",
|
|
4584
|
+
text: JSON.stringify(asEnvelope4(payload), null, 2)
|
|
4585
|
+
}
|
|
4586
|
+
]
|
|
4135
4587
|
};
|
|
4136
4588
|
}
|
|
4137
4589
|
return {
|
|
@@ -4187,7 +4639,10 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
4187
4639
|
if (!projectId) {
|
|
4188
4640
|
return {
|
|
4189
4641
|
content: [
|
|
4190
|
-
{
|
|
4642
|
+
{
|
|
4643
|
+
type: "text",
|
|
4644
|
+
text: "No project_id provided and no default project found."
|
|
4645
|
+
}
|
|
4191
4646
|
],
|
|
4192
4647
|
isError: true
|
|
4193
4648
|
};
|
|
@@ -4202,7 +4657,12 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
4202
4657
|
const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
|
|
4203
4658
|
if (!membership) {
|
|
4204
4659
|
return {
|
|
4205
|
-
content: [
|
|
4660
|
+
content: [
|
|
4661
|
+
{
|
|
4662
|
+
type: "text",
|
|
4663
|
+
text: "Project is not accessible to current user."
|
|
4664
|
+
}
|
|
4665
|
+
],
|
|
4206
4666
|
isError: true
|
|
4207
4667
|
};
|
|
4208
4668
|
}
|
|
@@ -4218,7 +4678,9 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
4218
4678
|
isError: true
|
|
4219
4679
|
};
|
|
4220
4680
|
}
|
|
4221
|
-
const brandContext = {
|
|
4681
|
+
const brandContext = {
|
|
4682
|
+
...existingProfile.brand_context
|
|
4683
|
+
};
|
|
4222
4684
|
const voiceProfile = brandContext.voiceProfile ?? {};
|
|
4223
4685
|
const platformOverrides = voiceProfile.platformOverrides ?? {};
|
|
4224
4686
|
const existingOverride = platformOverrides[platform2] ?? {};
|
|
@@ -4242,16 +4704,19 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
4242
4704
|
...brandContext,
|
|
4243
4705
|
voiceProfile: updatedVoiceProfile
|
|
4244
4706
|
};
|
|
4245
|
-
const { data: profileId, error: saveError } = await supabase.rpc(
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4707
|
+
const { data: profileId, error: saveError } = await supabase.rpc(
|
|
4708
|
+
"set_active_brand_profile",
|
|
4709
|
+
{
|
|
4710
|
+
p_project_id: projectId,
|
|
4711
|
+
p_brand_context: updatedContext,
|
|
4712
|
+
p_change_summary: `Updated platform voice override for ${platform2}`,
|
|
4713
|
+
p_changed_paths: [`voiceProfile.platformOverrides.${platform2}`],
|
|
4714
|
+
p_source_url: null,
|
|
4715
|
+
p_extraction_method: "manual",
|
|
4716
|
+
p_overall_confidence: null,
|
|
4717
|
+
p_extraction_metadata: null
|
|
4718
|
+
}
|
|
4719
|
+
);
|
|
4255
4720
|
if (saveError) {
|
|
4256
4721
|
return {
|
|
4257
4722
|
content: [
|
|
@@ -4272,7 +4737,12 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
4272
4737
|
};
|
|
4273
4738
|
if ((response_format || "text") === "json") {
|
|
4274
4739
|
return {
|
|
4275
|
-
content: [
|
|
4740
|
+
content: [
|
|
4741
|
+
{
|
|
4742
|
+
type: "text",
|
|
4743
|
+
text: JSON.stringify(asEnvelope4(payload), null, 2)
|
|
4744
|
+
}
|
|
4745
|
+
],
|
|
4276
4746
|
isError: false
|
|
4277
4747
|
};
|
|
4278
4748
|
}
|
|
@@ -4370,155 +4840,6 @@ async function capturePageScreenshot(page, outputPath, selector) {
|
|
|
4370
4840
|
// src/tools/screenshot.ts
|
|
4371
4841
|
import { resolve, relative } from "node:path";
|
|
4372
4842
|
import { mkdir } from "node:fs/promises";
|
|
4373
|
-
|
|
4374
|
-
// src/lib/ssrf.ts
|
|
4375
|
-
var BLOCKED_IP_PATTERNS = [
|
|
4376
|
-
// IPv4 localhost/loopback
|
|
4377
|
-
/^127\./,
|
|
4378
|
-
/^0\./,
|
|
4379
|
-
// IPv4 private ranges (RFC 1918)
|
|
4380
|
-
/^10\./,
|
|
4381
|
-
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
|
4382
|
-
/^192\.168\./,
|
|
4383
|
-
// IPv4 link-local
|
|
4384
|
-
/^169\.254\./,
|
|
4385
|
-
// Cloud metadata endpoint (AWS, GCP, Azure)
|
|
4386
|
-
/^169\.254\.169\.254$/,
|
|
4387
|
-
// IPv4 broadcast
|
|
4388
|
-
/^255\./,
|
|
4389
|
-
// Shared address space (RFC 6598)
|
|
4390
|
-
/^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./
|
|
4391
|
-
];
|
|
4392
|
-
var BLOCKED_IPV6_PATTERNS = [
|
|
4393
|
-
/^::1$/i,
|
|
4394
|
-
// loopback
|
|
4395
|
-
/^::$/i,
|
|
4396
|
-
// unspecified
|
|
4397
|
-
/^fe[89ab][0-9a-f]:/i,
|
|
4398
|
-
// link-local fe80::/10
|
|
4399
|
-
/^fc[0-9a-f]:/i,
|
|
4400
|
-
// unique local fc00::/7
|
|
4401
|
-
/^fd[0-9a-f]:/i,
|
|
4402
|
-
// unique local fc00::/7
|
|
4403
|
-
/^::ffff:127\./i,
|
|
4404
|
-
// IPv4-mapped localhost
|
|
4405
|
-
/^::ffff:(0|10|127|169\.254|172\.(1[6-9]|2[0-9]|3[0-1])|192\.168)\./i
|
|
4406
|
-
// IPv4-mapped private
|
|
4407
|
-
];
|
|
4408
|
-
var BLOCKED_HOSTNAMES = [
|
|
4409
|
-
"localhost",
|
|
4410
|
-
"localhost.localdomain",
|
|
4411
|
-
"local",
|
|
4412
|
-
"127.0.0.1",
|
|
4413
|
-
"0.0.0.0",
|
|
4414
|
-
"[::1]",
|
|
4415
|
-
"[::ffff:127.0.0.1]",
|
|
4416
|
-
// Cloud metadata endpoints
|
|
4417
|
-
"metadata.google.internal",
|
|
4418
|
-
"metadata.goog",
|
|
4419
|
-
"instance-data",
|
|
4420
|
-
"instance-data.ec2.internal"
|
|
4421
|
-
];
|
|
4422
|
-
var ALLOWED_PROTOCOLS = ["http:", "https:"];
|
|
4423
|
-
var BLOCKED_PORTS = [22, 23, 25, 110, 143, 445, 3306, 5432, 6379, 27017, 11211];
|
|
4424
|
-
function isBlockedIP(ip) {
|
|
4425
|
-
const normalized = ip.replace(/^\[/, "").replace(/\]$/, "");
|
|
4426
|
-
if (normalized.includes(":")) {
|
|
4427
|
-
return BLOCKED_IPV6_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
4428
|
-
}
|
|
4429
|
-
return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
4430
|
-
}
|
|
4431
|
-
function isBlockedHostname(hostname) {
|
|
4432
|
-
return BLOCKED_HOSTNAMES.includes(hostname.toLowerCase());
|
|
4433
|
-
}
|
|
4434
|
-
function isIPAddress(hostname) {
|
|
4435
|
-
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
4436
|
-
const ipv6Pattern = /^\[?[a-fA-F0-9:]+\]?$/;
|
|
4437
|
-
return ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname);
|
|
4438
|
-
}
|
|
4439
|
-
async function validateUrlForSSRF(urlString) {
|
|
4440
|
-
try {
|
|
4441
|
-
const url = new URL(urlString);
|
|
4442
|
-
if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
|
|
4443
|
-
return {
|
|
4444
|
-
isValid: false,
|
|
4445
|
-
error: `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are allowed.`
|
|
4446
|
-
};
|
|
4447
|
-
}
|
|
4448
|
-
if (url.username || url.password) {
|
|
4449
|
-
return {
|
|
4450
|
-
isValid: false,
|
|
4451
|
-
error: "URLs with embedded credentials are not allowed."
|
|
4452
|
-
};
|
|
4453
|
-
}
|
|
4454
|
-
const hostname = url.hostname.toLowerCase();
|
|
4455
|
-
if (isBlockedHostname(hostname)) {
|
|
4456
|
-
return {
|
|
4457
|
-
isValid: false,
|
|
4458
|
-
error: "Access to internal/localhost addresses is not allowed."
|
|
4459
|
-
};
|
|
4460
|
-
}
|
|
4461
|
-
if (isIPAddress(hostname) && isBlockedIP(hostname)) {
|
|
4462
|
-
return {
|
|
4463
|
-
isValid: false,
|
|
4464
|
-
error: "Access to private/internal IP addresses is not allowed."
|
|
4465
|
-
};
|
|
4466
|
-
}
|
|
4467
|
-
const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80;
|
|
4468
|
-
if (BLOCKED_PORTS.includes(port)) {
|
|
4469
|
-
return {
|
|
4470
|
-
isValid: false,
|
|
4471
|
-
error: `Access to port ${port} is not allowed.`
|
|
4472
|
-
};
|
|
4473
|
-
}
|
|
4474
|
-
let resolvedIP;
|
|
4475
|
-
if (!isIPAddress(hostname)) {
|
|
4476
|
-
try {
|
|
4477
|
-
const dns = await import("node:dns");
|
|
4478
|
-
const resolver = new dns.promises.Resolver();
|
|
4479
|
-
const resolvedIPs = [];
|
|
4480
|
-
try {
|
|
4481
|
-
const aRecords = await resolver.resolve4(hostname);
|
|
4482
|
-
resolvedIPs.push(...aRecords);
|
|
4483
|
-
} catch {
|
|
4484
|
-
}
|
|
4485
|
-
try {
|
|
4486
|
-
const aaaaRecords = await resolver.resolve6(hostname);
|
|
4487
|
-
resolvedIPs.push(...aaaaRecords);
|
|
4488
|
-
} catch {
|
|
4489
|
-
}
|
|
4490
|
-
if (resolvedIPs.length === 0) {
|
|
4491
|
-
return {
|
|
4492
|
-
isValid: false,
|
|
4493
|
-
error: "DNS resolution failed: hostname did not resolve to any address."
|
|
4494
|
-
};
|
|
4495
|
-
}
|
|
4496
|
-
for (const ip of resolvedIPs) {
|
|
4497
|
-
if (isBlockedIP(ip)) {
|
|
4498
|
-
return {
|
|
4499
|
-
isValid: false,
|
|
4500
|
-
error: "Hostname resolves to a private/internal IP address."
|
|
4501
|
-
};
|
|
4502
|
-
}
|
|
4503
|
-
}
|
|
4504
|
-
resolvedIP = resolvedIPs[0];
|
|
4505
|
-
} catch {
|
|
4506
|
-
return {
|
|
4507
|
-
isValid: false,
|
|
4508
|
-
error: "DNS resolution failed. Cannot verify hostname safety."
|
|
4509
|
-
};
|
|
4510
|
-
}
|
|
4511
|
-
}
|
|
4512
|
-
return { isValid: true, sanitizedUrl: url.toString(), resolvedIP };
|
|
4513
|
-
} catch (error) {
|
|
4514
|
-
return {
|
|
4515
|
-
isValid: false,
|
|
4516
|
-
error: `Invalid URL format: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
4517
|
-
};
|
|
4518
|
-
}
|
|
4519
|
-
}
|
|
4520
|
-
|
|
4521
|
-
// src/tools/screenshot.ts
|
|
4522
4843
|
init_supabase();
|
|
4523
4844
|
function registerScreenshotTools(server) {
|
|
4524
4845
|
server.tool(
|
|
@@ -5094,6 +5415,7 @@ function registerRemotionTools(server) {
|
|
|
5094
5415
|
// src/tools/insights.ts
|
|
5095
5416
|
init_supabase();
|
|
5096
5417
|
import { z as z8 } from "zod";
|
|
5418
|
+
var MAX_INSIGHT_AGE_DAYS = 30;
|
|
5097
5419
|
var PLATFORM_ENUM = [
|
|
5098
5420
|
"youtube",
|
|
5099
5421
|
"tiktok",
|
|
@@ -5104,11 +5426,19 @@ var PLATFORM_ENUM = [
|
|
|
5104
5426
|
"threads",
|
|
5105
5427
|
"bluesky"
|
|
5106
5428
|
];
|
|
5107
|
-
var DAY_NAMES = [
|
|
5429
|
+
var DAY_NAMES = [
|
|
5430
|
+
"Sunday",
|
|
5431
|
+
"Monday",
|
|
5432
|
+
"Tuesday",
|
|
5433
|
+
"Wednesday",
|
|
5434
|
+
"Thursday",
|
|
5435
|
+
"Friday",
|
|
5436
|
+
"Saturday"
|
|
5437
|
+
];
|
|
5108
5438
|
function asEnvelope5(data) {
|
|
5109
5439
|
return {
|
|
5110
5440
|
_meta: {
|
|
5111
|
-
version:
|
|
5441
|
+
version: MCP_VERSION,
|
|
5112
5442
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5113
5443
|
},
|
|
5114
5444
|
data
|
|
@@ -5119,7 +5449,12 @@ function registerInsightsTools(server) {
|
|
|
5119
5449
|
"get_performance_insights",
|
|
5120
5450
|
"Query performance insights derived from post analytics. Returns metrics like engagement rate, view velocity, and click rate aggregated over time. Use this to understand what content is performing well.",
|
|
5121
5451
|
{
|
|
5122
|
-
insight_type: z8.enum([
|
|
5452
|
+
insight_type: z8.enum([
|
|
5453
|
+
"top_hooks",
|
|
5454
|
+
"optimal_timing",
|
|
5455
|
+
"best_models",
|
|
5456
|
+
"competitor_patterns"
|
|
5457
|
+
]).optional().describe("Filter to a specific insight type."),
|
|
5123
5458
|
days: z8.number().min(1).max(90).optional().describe("Number of days to look back. Defaults to 30. Max 90."),
|
|
5124
5459
|
limit: z8.number().min(1).max(50).optional().describe("Maximum number of insights to return. Defaults to 10."),
|
|
5125
5460
|
response_format: z8.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
@@ -5138,10 +5473,13 @@ function registerInsightsTools(server) {
|
|
|
5138
5473
|
projectIds.push(...projects.map((p) => p.id));
|
|
5139
5474
|
}
|
|
5140
5475
|
}
|
|
5476
|
+
const effectiveDays = Math.min(lookbackDays, MAX_INSIGHT_AGE_DAYS);
|
|
5141
5477
|
const since = /* @__PURE__ */ new Date();
|
|
5142
|
-
since.setDate(since.getDate() -
|
|
5478
|
+
since.setDate(since.getDate() - effectiveDays);
|
|
5143
5479
|
const sinceIso = since.toISOString();
|
|
5144
|
-
let query = supabase.from("performance_insights").select(
|
|
5480
|
+
let query = supabase.from("performance_insights").select(
|
|
5481
|
+
"id, project_id, insight_type, insight_data, confidence_score, generated_at"
|
|
5482
|
+
).gte("generated_at", sinceIso).order("generated_at", { ascending: false }).limit(maxRows);
|
|
5145
5483
|
if (projectIds.length > 0) {
|
|
5146
5484
|
query = query.in("project_id", projectIds);
|
|
5147
5485
|
} else {
|
|
@@ -5498,7 +5836,7 @@ init_supabase();
|
|
|
5498
5836
|
function asEnvelope6(data) {
|
|
5499
5837
|
return {
|
|
5500
5838
|
_meta: {
|
|
5501
|
-
version:
|
|
5839
|
+
version: MCP_VERSION,
|
|
5502
5840
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5503
5841
|
},
|
|
5504
5842
|
data
|
|
@@ -5509,7 +5847,9 @@ function registerCommentsTools(server) {
|
|
|
5509
5847
|
"list_comments",
|
|
5510
5848
|
"List YouTube comments. Without a video_id, returns recent comments across all channel videos. With a video_id, returns comments for that specific video.",
|
|
5511
5849
|
{
|
|
5512
|
-
video_id: z10.string().optional().describe(
|
|
5850
|
+
video_id: z10.string().optional().describe(
|
|
5851
|
+
"YouTube video ID. If omitted, returns comments across all channel videos."
|
|
5852
|
+
),
|
|
5513
5853
|
max_results: z10.number().min(1).max(100).optional().describe("Maximum number of comments to return. Defaults to 50."),
|
|
5514
5854
|
page_token: z10.string().optional().describe("Pagination token from a previous list_comments call."),
|
|
5515
5855
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
@@ -5524,7 +5864,9 @@ function registerCommentsTools(server) {
|
|
|
5524
5864
|
});
|
|
5525
5865
|
if (error) {
|
|
5526
5866
|
return {
|
|
5527
|
-
content: [
|
|
5867
|
+
content: [
|
|
5868
|
+
{ type: "text", text: `Error listing comments: ${error}` }
|
|
5869
|
+
],
|
|
5528
5870
|
isError: true
|
|
5529
5871
|
};
|
|
5530
5872
|
}
|
|
@@ -5536,7 +5878,10 @@ function registerCommentsTools(server) {
|
|
|
5536
5878
|
{
|
|
5537
5879
|
type: "text",
|
|
5538
5880
|
text: JSON.stringify(
|
|
5539
|
-
asEnvelope6({
|
|
5881
|
+
asEnvelope6({
|
|
5882
|
+
comments,
|
|
5883
|
+
nextPageToken: result.nextPageToken ?? null
|
|
5884
|
+
}),
|
|
5540
5885
|
null,
|
|
5541
5886
|
2
|
|
5542
5887
|
)
|
|
@@ -5573,7 +5918,9 @@ function registerCommentsTools(server) {
|
|
|
5573
5918
|
"reply_to_comment",
|
|
5574
5919
|
"Reply to a YouTube comment. Requires the parent comment ID and reply text.",
|
|
5575
5920
|
{
|
|
5576
|
-
parent_id: z10.string().describe(
|
|
5921
|
+
parent_id: z10.string().describe(
|
|
5922
|
+
"The ID of the parent comment to reply to (from list_comments)."
|
|
5923
|
+
),
|
|
5577
5924
|
text: z10.string().min(1).describe("The reply text."),
|
|
5578
5925
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
5579
5926
|
},
|
|
@@ -5612,7 +5959,12 @@ function registerCommentsTools(server) {
|
|
|
5612
5959
|
details: { error }
|
|
5613
5960
|
});
|
|
5614
5961
|
return {
|
|
5615
|
-
content: [
|
|
5962
|
+
content: [
|
|
5963
|
+
{
|
|
5964
|
+
type: "text",
|
|
5965
|
+
text: `Error replying to comment: ${error}`
|
|
5966
|
+
}
|
|
5967
|
+
],
|
|
5616
5968
|
isError: true
|
|
5617
5969
|
};
|
|
5618
5970
|
}
|
|
@@ -5625,7 +5977,12 @@ function registerCommentsTools(server) {
|
|
|
5625
5977
|
});
|
|
5626
5978
|
if (format === "json") {
|
|
5627
5979
|
return {
|
|
5628
|
-
content: [
|
|
5980
|
+
content: [
|
|
5981
|
+
{
|
|
5982
|
+
type: "text",
|
|
5983
|
+
text: JSON.stringify(asEnvelope6(result), null, 2)
|
|
5984
|
+
}
|
|
5985
|
+
]
|
|
5629
5986
|
};
|
|
5630
5987
|
}
|
|
5631
5988
|
return {
|
|
@@ -5683,7 +6040,9 @@ function registerCommentsTools(server) {
|
|
|
5683
6040
|
details: { error }
|
|
5684
6041
|
});
|
|
5685
6042
|
return {
|
|
5686
|
-
content: [
|
|
6043
|
+
content: [
|
|
6044
|
+
{ type: "text", text: `Error posting comment: ${error}` }
|
|
6045
|
+
],
|
|
5687
6046
|
isError: true
|
|
5688
6047
|
};
|
|
5689
6048
|
}
|
|
@@ -5696,7 +6055,12 @@ function registerCommentsTools(server) {
|
|
|
5696
6055
|
});
|
|
5697
6056
|
if (format === "json") {
|
|
5698
6057
|
return {
|
|
5699
|
-
content: [
|
|
6058
|
+
content: [
|
|
6059
|
+
{
|
|
6060
|
+
type: "text",
|
|
6061
|
+
text: JSON.stringify(asEnvelope6(result), null, 2)
|
|
6062
|
+
}
|
|
6063
|
+
]
|
|
5700
6064
|
};
|
|
5701
6065
|
}
|
|
5702
6066
|
return {
|
|
@@ -5754,7 +6118,12 @@ function registerCommentsTools(server) {
|
|
|
5754
6118
|
details: { error }
|
|
5755
6119
|
});
|
|
5756
6120
|
return {
|
|
5757
|
-
content: [
|
|
6121
|
+
content: [
|
|
6122
|
+
{
|
|
6123
|
+
type: "text",
|
|
6124
|
+
text: `Error moderating comment: ${error}`
|
|
6125
|
+
}
|
|
6126
|
+
],
|
|
5758
6127
|
isError: true
|
|
5759
6128
|
};
|
|
5760
6129
|
}
|
|
@@ -5833,7 +6202,9 @@ function registerCommentsTools(server) {
|
|
|
5833
6202
|
details: { error }
|
|
5834
6203
|
});
|
|
5835
6204
|
return {
|
|
5836
|
-
content: [
|
|
6205
|
+
content: [
|
|
6206
|
+
{ type: "text", text: `Error deleting comment: ${error}` }
|
|
6207
|
+
],
|
|
5837
6208
|
isError: true
|
|
5838
6209
|
};
|
|
5839
6210
|
}
|
|
@@ -5848,13 +6219,22 @@ function registerCommentsTools(server) {
|
|
|
5848
6219
|
content: [
|
|
5849
6220
|
{
|
|
5850
6221
|
type: "text",
|
|
5851
|
-
text: JSON.stringify(
|
|
6222
|
+
text: JSON.stringify(
|
|
6223
|
+
asEnvelope6({ success: true, commentId: comment_id }),
|
|
6224
|
+
null,
|
|
6225
|
+
2
|
|
6226
|
+
)
|
|
5852
6227
|
}
|
|
5853
6228
|
]
|
|
5854
6229
|
};
|
|
5855
6230
|
}
|
|
5856
6231
|
return {
|
|
5857
|
-
content: [
|
|
6232
|
+
content: [
|
|
6233
|
+
{
|
|
6234
|
+
type: "text",
|
|
6235
|
+
text: `Comment ${comment_id} deleted successfully.`
|
|
6236
|
+
}
|
|
6237
|
+
]
|
|
5858
6238
|
};
|
|
5859
6239
|
}
|
|
5860
6240
|
);
|
|
@@ -5882,8 +6262,12 @@ function transformInsightsToPerformanceContext(projectId, insights) {
|
|
|
5882
6262
|
};
|
|
5883
6263
|
}
|
|
5884
6264
|
const topHooksInsight = insights.find((i) => i.insight_type === "top_hooks");
|
|
5885
|
-
const optimalTimingInsight = insights.find(
|
|
5886
|
-
|
|
6265
|
+
const optimalTimingInsight = insights.find(
|
|
6266
|
+
(i) => i.insight_type === "optimal_timing"
|
|
6267
|
+
);
|
|
6268
|
+
const bestModelsInsight = insights.find(
|
|
6269
|
+
(i) => i.insight_type === "best_models"
|
|
6270
|
+
);
|
|
5887
6271
|
const topHooks = topHooksInsight?.insight_data?.hooks || [];
|
|
5888
6272
|
const hooksSummary = topHooksInsight?.insight_data?.summary || "";
|
|
5889
6273
|
const timingSummary = optimalTimingInsight?.insight_data?.summary || "";
|
|
@@ -5894,7 +6278,10 @@ function transformInsightsToPerformanceContext(projectId, insights) {
|
|
|
5894
6278
|
if (hooksSummary) promptParts.push(hooksSummary);
|
|
5895
6279
|
if (timingSummary) promptParts.push(timingSummary);
|
|
5896
6280
|
if (modelSummary) promptParts.push(modelSummary);
|
|
5897
|
-
if (topHooks.length)
|
|
6281
|
+
if (topHooks.length)
|
|
6282
|
+
promptParts.push(
|
|
6283
|
+
`Top performing hooks: ${topHooks.slice(0, 3).join(", ")}`
|
|
6284
|
+
);
|
|
5898
6285
|
return {
|
|
5899
6286
|
projectId,
|
|
5900
6287
|
hasHistoricalData: true,
|
|
@@ -5920,7 +6307,7 @@ function transformInsightsToPerformanceContext(projectId, insights) {
|
|
|
5920
6307
|
function asEnvelope7(data) {
|
|
5921
6308
|
return {
|
|
5922
6309
|
_meta: {
|
|
5923
|
-
version:
|
|
6310
|
+
version: MCP_VERSION,
|
|
5924
6311
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5925
6312
|
},
|
|
5926
6313
|
data
|
|
@@ -5943,7 +6330,12 @@ function registerIdeationContextTools(server) {
|
|
|
5943
6330
|
const { data: member } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).limit(1).single();
|
|
5944
6331
|
if (!member?.organization_id) {
|
|
5945
6332
|
return {
|
|
5946
|
-
content: [
|
|
6333
|
+
content: [
|
|
6334
|
+
{
|
|
6335
|
+
type: "text",
|
|
6336
|
+
text: "No organization found for current user."
|
|
6337
|
+
}
|
|
6338
|
+
],
|
|
5947
6339
|
isError: true
|
|
5948
6340
|
};
|
|
5949
6341
|
}
|
|
@@ -5951,7 +6343,10 @@ function registerIdeationContextTools(server) {
|
|
|
5951
6343
|
if (projectsError) {
|
|
5952
6344
|
return {
|
|
5953
6345
|
content: [
|
|
5954
|
-
{
|
|
6346
|
+
{
|
|
6347
|
+
type: "text",
|
|
6348
|
+
text: `Failed to resolve projects: ${projectsError.message}`
|
|
6349
|
+
}
|
|
5955
6350
|
],
|
|
5956
6351
|
isError: true
|
|
5957
6352
|
};
|
|
@@ -5959,7 +6354,12 @@ function registerIdeationContextTools(server) {
|
|
|
5959
6354
|
const projectIds = (projects || []).map((p) => p.id);
|
|
5960
6355
|
if (projectIds.length === 0) {
|
|
5961
6356
|
return {
|
|
5962
|
-
content: [
|
|
6357
|
+
content: [
|
|
6358
|
+
{
|
|
6359
|
+
type: "text",
|
|
6360
|
+
text: "No projects found for current user."
|
|
6361
|
+
}
|
|
6362
|
+
],
|
|
5963
6363
|
isError: true
|
|
5964
6364
|
};
|
|
5965
6365
|
}
|
|
@@ -5968,7 +6368,10 @@ function registerIdeationContextTools(server) {
|
|
|
5968
6368
|
if (!selectedProjectId) {
|
|
5969
6369
|
return {
|
|
5970
6370
|
content: [
|
|
5971
|
-
{
|
|
6371
|
+
{
|
|
6372
|
+
type: "text",
|
|
6373
|
+
text: "No accessible project found for current user."
|
|
6374
|
+
}
|
|
5972
6375
|
],
|
|
5973
6376
|
isError: true
|
|
5974
6377
|
};
|
|
@@ -5986,7 +6389,9 @@ function registerIdeationContextTools(server) {
|
|
|
5986
6389
|
}
|
|
5987
6390
|
const since = /* @__PURE__ */ new Date();
|
|
5988
6391
|
since.setDate(since.getDate() - lookbackDays);
|
|
5989
|
-
const { data: insights, error } = await supabase.from("performance_insights").select(
|
|
6392
|
+
const { data: insights, error } = await supabase.from("performance_insights").select(
|
|
6393
|
+
"id, project_id, insight_type, insight_data, generated_at, expires_at"
|
|
6394
|
+
).eq("project_id", selectedProjectId).gte("generated_at", since.toISOString()).gt("expires_at", (/* @__PURE__ */ new Date()).toISOString()).order("generated_at", { ascending: false }).limit(30);
|
|
5990
6395
|
if (error) {
|
|
5991
6396
|
return {
|
|
5992
6397
|
content: [
|
|
@@ -6004,7 +6409,12 @@ function registerIdeationContextTools(server) {
|
|
|
6004
6409
|
);
|
|
6005
6410
|
if (format === "json") {
|
|
6006
6411
|
return {
|
|
6007
|
-
content: [
|
|
6412
|
+
content: [
|
|
6413
|
+
{
|
|
6414
|
+
type: "text",
|
|
6415
|
+
text: JSON.stringify(asEnvelope7(context), null, 2)
|
|
6416
|
+
}
|
|
6417
|
+
]
|
|
6008
6418
|
};
|
|
6009
6419
|
}
|
|
6010
6420
|
const lines = [
|
|
@@ -6028,7 +6438,7 @@ import { z as z12 } from "zod";
|
|
|
6028
6438
|
function asEnvelope8(data) {
|
|
6029
6439
|
return {
|
|
6030
6440
|
_meta: {
|
|
6031
|
-
version:
|
|
6441
|
+
version: MCP_VERSION,
|
|
6032
6442
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6033
6443
|
},
|
|
6034
6444
|
data
|
|
@@ -6067,7 +6477,12 @@ function registerCreditsTools(server) {
|
|
|
6067
6477
|
};
|
|
6068
6478
|
if ((response_format || "text") === "json") {
|
|
6069
6479
|
return {
|
|
6070
|
-
content: [
|
|
6480
|
+
content: [
|
|
6481
|
+
{
|
|
6482
|
+
type: "text",
|
|
6483
|
+
text: JSON.stringify(asEnvelope8(payload), null, 2)
|
|
6484
|
+
}
|
|
6485
|
+
]
|
|
6071
6486
|
};
|
|
6072
6487
|
}
|
|
6073
6488
|
return {
|
|
@@ -6101,7 +6516,12 @@ Monthly used: ${payload.monthlyUsed}` + (payload.monthlyLimit ? ` / ${payload.mo
|
|
|
6101
6516
|
};
|
|
6102
6517
|
if ((response_format || "text") === "json") {
|
|
6103
6518
|
return {
|
|
6104
|
-
content: [
|
|
6519
|
+
content: [
|
|
6520
|
+
{
|
|
6521
|
+
type: "text",
|
|
6522
|
+
text: JSON.stringify(asEnvelope8(payload), null, 2)
|
|
6523
|
+
}
|
|
6524
|
+
]
|
|
6105
6525
|
};
|
|
6106
6526
|
}
|
|
6107
6527
|
return {
|
|
@@ -6128,7 +6548,7 @@ import { z as z13 } from "zod";
|
|
|
6128
6548
|
function asEnvelope9(data) {
|
|
6129
6549
|
return {
|
|
6130
6550
|
_meta: {
|
|
6131
|
-
version:
|
|
6551
|
+
version: MCP_VERSION,
|
|
6132
6552
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6133
6553
|
},
|
|
6134
6554
|
data
|
|
@@ -6167,7 +6587,12 @@ function registerLoopSummaryTools(server) {
|
|
|
6167
6587
|
const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
|
|
6168
6588
|
if (!membership) {
|
|
6169
6589
|
return {
|
|
6170
|
-
content: [
|
|
6590
|
+
content: [
|
|
6591
|
+
{
|
|
6592
|
+
type: "text",
|
|
6593
|
+
text: "Project is not accessible to current user."
|
|
6594
|
+
}
|
|
6595
|
+
],
|
|
6171
6596
|
isError: true
|
|
6172
6597
|
};
|
|
6173
6598
|
}
|
|
@@ -6190,7 +6615,12 @@ function registerLoopSummaryTools(server) {
|
|
|
6190
6615
|
};
|
|
6191
6616
|
if ((response_format || "text") === "json") {
|
|
6192
6617
|
return {
|
|
6193
|
-
content: [
|
|
6618
|
+
content: [
|
|
6619
|
+
{
|
|
6620
|
+
type: "text",
|
|
6621
|
+
text: JSON.stringify(asEnvelope9(payload), null, 2)
|
|
6622
|
+
}
|
|
6623
|
+
]
|
|
6194
6624
|
};
|
|
6195
6625
|
}
|
|
6196
6626
|
return {
|
|
@@ -6213,11 +6643,6 @@ Next Action: ${payload.recommendedNextAction}`
|
|
|
6213
6643
|
// src/tools/usage.ts
|
|
6214
6644
|
init_supabase();
|
|
6215
6645
|
import { z as z14 } from "zod";
|
|
6216
|
-
|
|
6217
|
-
// src/lib/version.ts
|
|
6218
|
-
var MCP_VERSION = "1.3.2";
|
|
6219
|
-
|
|
6220
|
-
// src/tools/usage.ts
|
|
6221
6646
|
function asEnvelope10(data) {
|
|
6222
6647
|
return {
|
|
6223
6648
|
_meta: {
|
|
@@ -6523,7 +6948,10 @@ ${"=".repeat(40)}
|
|
|
6523
6948
|
import { z as z16 } from "zod";
|
|
6524
6949
|
init_supabase();
|
|
6525
6950
|
function asEnvelope12(data) {
|
|
6526
|
-
return {
|
|
6951
|
+
return {
|
|
6952
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
6953
|
+
data
|
|
6954
|
+
};
|
|
6527
6955
|
}
|
|
6528
6956
|
function isYouTubeUrl(url) {
|
|
6529
6957
|
if (/youtube\.com\/watch|youtu\.be\//.test(url)) return "video";
|
|
@@ -6554,13 +6982,17 @@ Metadata:`);
|
|
|
6554
6982
|
if (m.tags?.length) lines.push(` Tags: ${m.tags.join(", ")}`);
|
|
6555
6983
|
}
|
|
6556
6984
|
if (content.features?.length)
|
|
6557
|
-
lines.push(
|
|
6985
|
+
lines.push(
|
|
6986
|
+
`
|
|
6558
6987
|
Features:
|
|
6559
|
-
${content.features.map((f) => ` - ${f}`).join("\n")}`
|
|
6988
|
+
${content.features.map((f) => ` - ${f}`).join("\n")}`
|
|
6989
|
+
);
|
|
6560
6990
|
if (content.benefits?.length)
|
|
6561
|
-
lines.push(
|
|
6991
|
+
lines.push(
|
|
6992
|
+
`
|
|
6562
6993
|
Benefits:
|
|
6563
|
-
${content.benefits.map((b) => ` - ${b}`).join("\n")}`
|
|
6994
|
+
${content.benefits.map((b) => ` - ${b}`).join("\n")}`
|
|
6995
|
+
);
|
|
6564
6996
|
if (content.usp) lines.push(`
|
|
6565
6997
|
USP: ${content.usp}`);
|
|
6566
6998
|
if (content.suggested_hooks?.length)
|
|
@@ -6582,12 +7014,20 @@ function registerExtractionTools(server) {
|
|
|
6582
7014
|
max_results: z16.number().min(1).max(100).default(10).describe("Max comments to include"),
|
|
6583
7015
|
response_format: z16.enum(["text", "json"]).default("text")
|
|
6584
7016
|
},
|
|
6585
|
-
async ({
|
|
7017
|
+
async ({
|
|
7018
|
+
url,
|
|
7019
|
+
extract_type,
|
|
7020
|
+
include_comments,
|
|
7021
|
+
max_results,
|
|
7022
|
+
response_format
|
|
7023
|
+
}) => {
|
|
6586
7024
|
const startedAt = Date.now();
|
|
6587
7025
|
const ssrfCheck = await validateUrlForSSRF(url);
|
|
6588
7026
|
if (!ssrfCheck.isValid) {
|
|
6589
7027
|
return {
|
|
6590
|
-
content: [
|
|
7028
|
+
content: [
|
|
7029
|
+
{ type: "text", text: `URL blocked: ${ssrfCheck.error}` }
|
|
7030
|
+
],
|
|
6591
7031
|
isError: true
|
|
6592
7032
|
};
|
|
6593
7033
|
}
|
|
@@ -6715,13 +7155,21 @@ function registerExtractionTools(server) {
|
|
|
6715
7155
|
if (response_format === "json") {
|
|
6716
7156
|
return {
|
|
6717
7157
|
content: [
|
|
6718
|
-
{
|
|
7158
|
+
{
|
|
7159
|
+
type: "text",
|
|
7160
|
+
text: JSON.stringify(asEnvelope12(extracted), null, 2)
|
|
7161
|
+
}
|
|
6719
7162
|
],
|
|
6720
7163
|
isError: false
|
|
6721
7164
|
};
|
|
6722
7165
|
}
|
|
6723
7166
|
return {
|
|
6724
|
-
content: [
|
|
7167
|
+
content: [
|
|
7168
|
+
{
|
|
7169
|
+
type: "text",
|
|
7170
|
+
text: formatExtractedContentAsText(extracted)
|
|
7171
|
+
}
|
|
7172
|
+
],
|
|
6725
7173
|
isError: false
|
|
6726
7174
|
};
|
|
6727
7175
|
} catch (err) {
|
|
@@ -6734,7 +7182,9 @@ function registerExtractionTools(server) {
|
|
|
6734
7182
|
details: { url, error: message }
|
|
6735
7183
|
});
|
|
6736
7184
|
return {
|
|
6737
|
-
content: [
|
|
7185
|
+
content: [
|
|
7186
|
+
{ type: "text", text: `Extraction failed: ${message}` }
|
|
7187
|
+
],
|
|
6738
7188
|
isError: true
|
|
6739
7189
|
};
|
|
6740
7190
|
}
|
|
@@ -6746,7 +7196,10 @@ function registerExtractionTools(server) {
|
|
|
6746
7196
|
import { z as z17 } from "zod";
|
|
6747
7197
|
init_supabase();
|
|
6748
7198
|
function asEnvelope13(data) {
|
|
6749
|
-
return {
|
|
7199
|
+
return {
|
|
7200
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
7201
|
+
data
|
|
7202
|
+
};
|
|
6750
7203
|
}
|
|
6751
7204
|
function registerQualityTools(server) {
|
|
6752
7205
|
server.tool(
|
|
@@ -6802,7 +7255,12 @@ function registerQualityTools(server) {
|
|
|
6802
7255
|
});
|
|
6803
7256
|
if (response_format === "json") {
|
|
6804
7257
|
return {
|
|
6805
|
-
content: [
|
|
7258
|
+
content: [
|
|
7259
|
+
{
|
|
7260
|
+
type: "text",
|
|
7261
|
+
text: JSON.stringify(asEnvelope13(result), null, 2)
|
|
7262
|
+
}
|
|
7263
|
+
],
|
|
6806
7264
|
isError: false
|
|
6807
7265
|
};
|
|
6808
7266
|
}
|
|
@@ -6812,7 +7270,9 @@ function registerQualityTools(server) {
|
|
|
6812
7270
|
);
|
|
6813
7271
|
lines.push("");
|
|
6814
7272
|
for (const cat of result.categories) {
|
|
6815
|
-
lines.push(
|
|
7273
|
+
lines.push(
|
|
7274
|
+
` ${cat.name}: ${cat.score}/${cat.maxScore} \u2014 ${cat.detail}`
|
|
7275
|
+
);
|
|
6816
7276
|
}
|
|
6817
7277
|
if (result.blockers.length > 0) {
|
|
6818
7278
|
lines.push("");
|
|
@@ -6823,7 +7283,10 @@ function registerQualityTools(server) {
|
|
|
6823
7283
|
}
|
|
6824
7284
|
lines.push("");
|
|
6825
7285
|
lines.push(`Threshold: ${result.threshold}/${result.maxTotal}`);
|
|
6826
|
-
return {
|
|
7286
|
+
return {
|
|
7287
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
7288
|
+
isError: false
|
|
7289
|
+
};
|
|
6827
7290
|
}
|
|
6828
7291
|
);
|
|
6829
7292
|
server.tool(
|
|
@@ -6864,7 +7327,9 @@ function registerQualityTools(server) {
|
|
|
6864
7327
|
});
|
|
6865
7328
|
const scores = postsWithQuality.map((p) => p.quality.score);
|
|
6866
7329
|
const passed = postsWithQuality.filter((p) => p.quality.passed).length;
|
|
6867
|
-
const avgScore = scores.length > 0 ? Math.round(
|
|
7330
|
+
const avgScore = scores.length > 0 ? Math.round(
|
|
7331
|
+
scores.reduce((a, b) => a + b, 0) / scores.length * 10
|
|
7332
|
+
) / 10 : 0;
|
|
6868
7333
|
const summary = {
|
|
6869
7334
|
total_posts: plan.posts.length,
|
|
6870
7335
|
passed,
|
|
@@ -6883,25 +7348,36 @@ function registerQualityTools(server) {
|
|
|
6883
7348
|
content: [
|
|
6884
7349
|
{
|
|
6885
7350
|
type: "text",
|
|
6886
|
-
text: JSON.stringify(
|
|
7351
|
+
text: JSON.stringify(
|
|
7352
|
+
asEnvelope13({ posts: postsWithQuality, summary }),
|
|
7353
|
+
null,
|
|
7354
|
+
2
|
|
7355
|
+
)
|
|
6887
7356
|
}
|
|
6888
7357
|
],
|
|
6889
7358
|
isError: false
|
|
6890
7359
|
};
|
|
6891
7360
|
}
|
|
6892
7361
|
const lines = [];
|
|
6893
|
-
lines.push(
|
|
7362
|
+
lines.push(
|
|
7363
|
+
`PLAN QUALITY: ${passed}/${plan.posts.length} passed (avg: ${avgScore}/35)`
|
|
7364
|
+
);
|
|
6894
7365
|
lines.push("");
|
|
6895
7366
|
for (const post of postsWithQuality) {
|
|
6896
7367
|
const icon = post.quality.passed ? "[PASS]" : "[FAIL]";
|
|
6897
|
-
lines.push(
|
|
7368
|
+
lines.push(
|
|
7369
|
+
`${icon} ${post.id} | ${post.platform} | ${post.quality.score}/35`
|
|
7370
|
+
);
|
|
6898
7371
|
if (post.quality.blockers.length > 0) {
|
|
6899
7372
|
for (const b of post.quality.blockers) {
|
|
6900
7373
|
lines.push(` - ${b}`);
|
|
6901
7374
|
}
|
|
6902
7375
|
}
|
|
6903
7376
|
}
|
|
6904
|
-
return {
|
|
7377
|
+
return {
|
|
7378
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
7379
|
+
isError: false
|
|
7380
|
+
};
|
|
6905
7381
|
}
|
|
6906
7382
|
);
|
|
6907
7383
|
}
|
|
@@ -6914,7 +7390,10 @@ function toRecord(value) {
|
|
|
6914
7390
|
return value && typeof value === "object" ? value : void 0;
|
|
6915
7391
|
}
|
|
6916
7392
|
function asEnvelope14(data) {
|
|
6917
|
-
return {
|
|
7393
|
+
return {
|
|
7394
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
7395
|
+
data
|
|
7396
|
+
};
|
|
6918
7397
|
}
|
|
6919
7398
|
function tomorrowIsoDate() {
|
|
6920
7399
|
const d = /* @__PURE__ */ new Date();
|
|
@@ -6952,7 +7431,9 @@ function formatPlanAsText(plan) {
|
|
|
6952
7431
|
lines.push(`WEEKLY CONTENT PLAN: "${plan.topic}"`);
|
|
6953
7432
|
lines.push(`Period: ${plan.start_date} to ${plan.end_date}`);
|
|
6954
7433
|
lines.push(`Platforms: ${plan.platforms.join(", ")}`);
|
|
6955
|
-
lines.push(
|
|
7434
|
+
lines.push(
|
|
7435
|
+
`Posts: ${plan.posts.length} | Estimated credits: ~${plan.estimated_credits}`
|
|
7436
|
+
);
|
|
6956
7437
|
if (plan.plan_id) lines.push(`Plan ID: ${plan.plan_id}`);
|
|
6957
7438
|
if (plan.insights_applied?.has_historical_data) {
|
|
6958
7439
|
lines.push("");
|
|
@@ -6967,7 +7448,9 @@ function formatPlanAsText(plan) {
|
|
|
6967
7448
|
`- Best posting time: ${days[timing.dayOfWeek] ?? timing.dayOfWeek} ${timing.hourOfDay}:00`
|
|
6968
7449
|
);
|
|
6969
7450
|
}
|
|
6970
|
-
lines.push(
|
|
7451
|
+
lines.push(
|
|
7452
|
+
`- Recommended model: ${plan.insights_applied.recommended_model ?? "N/A"}`
|
|
7453
|
+
);
|
|
6971
7454
|
lines.push(`- Insights count: ${plan.insights_applied.insights_count}`);
|
|
6972
7455
|
}
|
|
6973
7456
|
lines.push("");
|
|
@@ -6988,9 +7471,11 @@ function formatPlanAsText(plan) {
|
|
|
6988
7471
|
` Caption: ${post.caption.slice(0, 200)}${post.caption.length > 200 ? "..." : ""}`
|
|
6989
7472
|
);
|
|
6990
7473
|
if (post.title) lines.push(` Title: ${post.title}`);
|
|
6991
|
-
if (post.visual_direction)
|
|
7474
|
+
if (post.visual_direction)
|
|
7475
|
+
lines.push(` Visual: ${post.visual_direction}`);
|
|
6992
7476
|
if (post.media_type) lines.push(` Media: ${post.media_type}`);
|
|
6993
|
-
if (post.hashtags?.length)
|
|
7477
|
+
if (post.hashtags?.length)
|
|
7478
|
+
lines.push(` Hashtags: ${post.hashtags.join(" ")}`);
|
|
6994
7479
|
lines.push("");
|
|
6995
7480
|
}
|
|
6996
7481
|
}
|
|
@@ -7073,7 +7558,10 @@ function registerPlanningTools(server) {
|
|
|
7073
7558
|
"mcp-data",
|
|
7074
7559
|
{
|
|
7075
7560
|
action: "brand-profile",
|
|
7076
|
-
...resolvedProjectId ? {
|
|
7561
|
+
...resolvedProjectId ? {
|
|
7562
|
+
projectId: resolvedProjectId,
|
|
7563
|
+
project_id: resolvedProjectId
|
|
7564
|
+
} : {}
|
|
7077
7565
|
},
|
|
7078
7566
|
{ timeoutMs: 15e3 }
|
|
7079
7567
|
);
|
|
@@ -7277,7 +7765,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7277
7765
|
details: { topic, error: `plan persistence failed: ${message}` }
|
|
7278
7766
|
});
|
|
7279
7767
|
return {
|
|
7280
|
-
content: [
|
|
7768
|
+
content: [
|
|
7769
|
+
{
|
|
7770
|
+
type: "text",
|
|
7771
|
+
text: `Plan persistence failed: ${message}`
|
|
7772
|
+
}
|
|
7773
|
+
],
|
|
7281
7774
|
isError: true
|
|
7282
7775
|
};
|
|
7283
7776
|
}
|
|
@@ -7291,7 +7784,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7291
7784
|
});
|
|
7292
7785
|
if (response_format === "json") {
|
|
7293
7786
|
return {
|
|
7294
|
-
content: [
|
|
7787
|
+
content: [
|
|
7788
|
+
{
|
|
7789
|
+
type: "text",
|
|
7790
|
+
text: JSON.stringify(asEnvelope14(plan), null, 2)
|
|
7791
|
+
}
|
|
7792
|
+
],
|
|
7295
7793
|
isError: false
|
|
7296
7794
|
};
|
|
7297
7795
|
}
|
|
@@ -7309,7 +7807,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7309
7807
|
details: { topic, error: message }
|
|
7310
7808
|
});
|
|
7311
7809
|
return {
|
|
7312
|
-
content: [
|
|
7810
|
+
content: [
|
|
7811
|
+
{
|
|
7812
|
+
type: "text",
|
|
7813
|
+
text: `Plan generation failed: ${message}`
|
|
7814
|
+
}
|
|
7815
|
+
],
|
|
7313
7816
|
isError: true
|
|
7314
7817
|
};
|
|
7315
7818
|
}
|
|
@@ -7369,7 +7872,11 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7369
7872
|
toolName: "save_content_plan",
|
|
7370
7873
|
status: "success",
|
|
7371
7874
|
durationMs,
|
|
7372
|
-
details: {
|
|
7875
|
+
details: {
|
|
7876
|
+
plan_id: planId,
|
|
7877
|
+
project_id: resolvedProjectId,
|
|
7878
|
+
status: normalizedStatus
|
|
7879
|
+
}
|
|
7373
7880
|
});
|
|
7374
7881
|
const result = {
|
|
7375
7882
|
plan_id: planId,
|
|
@@ -7378,13 +7885,21 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7378
7885
|
};
|
|
7379
7886
|
if (response_format === "json") {
|
|
7380
7887
|
return {
|
|
7381
|
-
content: [
|
|
7888
|
+
content: [
|
|
7889
|
+
{
|
|
7890
|
+
type: "text",
|
|
7891
|
+
text: JSON.stringify(asEnvelope14(result), null, 2)
|
|
7892
|
+
}
|
|
7893
|
+
],
|
|
7382
7894
|
isError: false
|
|
7383
7895
|
};
|
|
7384
7896
|
}
|
|
7385
7897
|
return {
|
|
7386
7898
|
content: [
|
|
7387
|
-
{
|
|
7899
|
+
{
|
|
7900
|
+
type: "text",
|
|
7901
|
+
text: `Saved content plan ${planId} (${normalizedStatus}).`
|
|
7902
|
+
}
|
|
7388
7903
|
],
|
|
7389
7904
|
isError: false
|
|
7390
7905
|
};
|
|
@@ -7398,7 +7913,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7398
7913
|
details: { error: message }
|
|
7399
7914
|
});
|
|
7400
7915
|
return {
|
|
7401
|
-
content: [
|
|
7916
|
+
content: [
|
|
7917
|
+
{
|
|
7918
|
+
type: "text",
|
|
7919
|
+
text: `Failed to save content plan: ${message}`
|
|
7920
|
+
}
|
|
7921
|
+
],
|
|
7402
7922
|
isError: true
|
|
7403
7923
|
};
|
|
7404
7924
|
}
|
|
@@ -7414,7 +7934,9 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7414
7934
|
async ({ plan_id, response_format }) => {
|
|
7415
7935
|
const supabase = getSupabaseClient();
|
|
7416
7936
|
const userId = await getDefaultUserId();
|
|
7417
|
-
const { data, error } = await supabase.from("content_plans").select(
|
|
7937
|
+
const { data, error } = await supabase.from("content_plans").select(
|
|
7938
|
+
"id, topic, status, plan_payload, insights_applied, created_at, updated_at"
|
|
7939
|
+
).eq("id", plan_id).eq("user_id", userId).maybeSingle();
|
|
7418
7940
|
if (error) {
|
|
7419
7941
|
return {
|
|
7420
7942
|
content: [
|
|
@@ -7429,7 +7951,10 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7429
7951
|
if (!data) {
|
|
7430
7952
|
return {
|
|
7431
7953
|
content: [
|
|
7432
|
-
{
|
|
7954
|
+
{
|
|
7955
|
+
type: "text",
|
|
7956
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
7957
|
+
}
|
|
7433
7958
|
],
|
|
7434
7959
|
isError: true
|
|
7435
7960
|
};
|
|
@@ -7445,7 +7970,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7445
7970
|
};
|
|
7446
7971
|
if (response_format === "json") {
|
|
7447
7972
|
return {
|
|
7448
|
-
content: [
|
|
7973
|
+
content: [
|
|
7974
|
+
{
|
|
7975
|
+
type: "text",
|
|
7976
|
+
text: JSON.stringify(asEnvelope14(payload), null, 2)
|
|
7977
|
+
}
|
|
7978
|
+
],
|
|
7449
7979
|
isError: false
|
|
7450
7980
|
};
|
|
7451
7981
|
}
|
|
@@ -7456,7 +7986,10 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7456
7986
|
`Status: ${data.status}`,
|
|
7457
7987
|
`Posts: ${Array.isArray(plan?.posts) ? plan.posts.length : 0}`
|
|
7458
7988
|
];
|
|
7459
|
-
return {
|
|
7989
|
+
return {
|
|
7990
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
7991
|
+
isError: false
|
|
7992
|
+
};
|
|
7460
7993
|
}
|
|
7461
7994
|
);
|
|
7462
7995
|
server.tool(
|
|
@@ -7499,14 +8032,19 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7499
8032
|
if (!stored?.plan_payload) {
|
|
7500
8033
|
return {
|
|
7501
8034
|
content: [
|
|
7502
|
-
{
|
|
8035
|
+
{
|
|
8036
|
+
type: "text",
|
|
8037
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
8038
|
+
}
|
|
7503
8039
|
],
|
|
7504
8040
|
isError: true
|
|
7505
8041
|
};
|
|
7506
8042
|
}
|
|
7507
8043
|
const plan = stored.plan_payload;
|
|
7508
8044
|
const existingPosts = Array.isArray(plan.posts) ? plan.posts : [];
|
|
7509
|
-
const updatesById = new Map(
|
|
8045
|
+
const updatesById = new Map(
|
|
8046
|
+
post_updates.map((update) => [update.post_id, update])
|
|
8047
|
+
);
|
|
7510
8048
|
const updatedPosts = existingPosts.map((post) => {
|
|
7511
8049
|
const update = updatesById.get(post.id);
|
|
7512
8050
|
if (!update) return post;
|
|
@@ -7524,7 +8062,9 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7524
8062
|
...update.status !== void 0 ? { status: update.status } : {}
|
|
7525
8063
|
};
|
|
7526
8064
|
});
|
|
7527
|
-
const nextStatus = updatedPosts.length > 0 && updatedPosts.every(
|
|
8065
|
+
const nextStatus = updatedPosts.length > 0 && updatedPosts.every(
|
|
8066
|
+
(post) => post.status === "approved" || post.status === "edited"
|
|
8067
|
+
) ? "approved" : stored.status === "scheduled" || stored.status === "completed" ? stored.status : "draft";
|
|
7528
8068
|
const updatedPlan = {
|
|
7529
8069
|
...plan,
|
|
7530
8070
|
posts: updatedPosts
|
|
@@ -7551,7 +8091,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7551
8091
|
};
|
|
7552
8092
|
if (response_format === "json") {
|
|
7553
8093
|
return {
|
|
7554
|
-
content: [
|
|
8094
|
+
content: [
|
|
8095
|
+
{
|
|
8096
|
+
type: "text",
|
|
8097
|
+
text: JSON.stringify(asEnvelope14(payload), null, 2)
|
|
8098
|
+
}
|
|
8099
|
+
],
|
|
7555
8100
|
isError: false
|
|
7556
8101
|
};
|
|
7557
8102
|
}
|
|
@@ -7591,7 +8136,10 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7591
8136
|
if (!stored?.plan_payload || !stored.project_id) {
|
|
7592
8137
|
return {
|
|
7593
8138
|
content: [
|
|
7594
|
-
{
|
|
8139
|
+
{
|
|
8140
|
+
type: "text",
|
|
8141
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
8142
|
+
}
|
|
7595
8143
|
],
|
|
7596
8144
|
isError: true
|
|
7597
8145
|
};
|
|
@@ -7600,7 +8148,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7600
8148
|
const posts = Array.isArray(plan.posts) ? plan.posts : [];
|
|
7601
8149
|
if (posts.length === 0) {
|
|
7602
8150
|
return {
|
|
7603
|
-
content: [
|
|
8151
|
+
content: [
|
|
8152
|
+
{
|
|
8153
|
+
type: "text",
|
|
8154
|
+
text: `Plan ${plan_id} has no posts to submit.`
|
|
8155
|
+
}
|
|
8156
|
+
],
|
|
7604
8157
|
isError: true
|
|
7605
8158
|
};
|
|
7606
8159
|
}
|
|
@@ -7643,7 +8196,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7643
8196
|
};
|
|
7644
8197
|
if (response_format === "json") {
|
|
7645
8198
|
return {
|
|
7646
|
-
content: [
|
|
8199
|
+
content: [
|
|
8200
|
+
{
|
|
8201
|
+
type: "text",
|
|
8202
|
+
text: JSON.stringify(asEnvelope14(payload), null, 2)
|
|
8203
|
+
}
|
|
8204
|
+
],
|
|
7647
8205
|
isError: false
|
|
7648
8206
|
};
|
|
7649
8207
|
}
|
|
@@ -7666,7 +8224,7 @@ import { z as z19 } from "zod";
|
|
|
7666
8224
|
function asEnvelope15(data) {
|
|
7667
8225
|
return {
|
|
7668
8226
|
_meta: {
|
|
7669
|
-
version:
|
|
8227
|
+
version: MCP_VERSION,
|
|
7670
8228
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7671
8229
|
},
|
|
7672
8230
|
data
|
|
@@ -7705,14 +8263,24 @@ function registerPlanApprovalTools(server) {
|
|
|
7705
8263
|
if (!projectId) {
|
|
7706
8264
|
return {
|
|
7707
8265
|
content: [
|
|
7708
|
-
{
|
|
8266
|
+
{
|
|
8267
|
+
type: "text",
|
|
8268
|
+
text: "No project_id provided and no default project found."
|
|
8269
|
+
}
|
|
7709
8270
|
],
|
|
7710
8271
|
isError: true
|
|
7711
8272
|
};
|
|
7712
8273
|
}
|
|
7713
|
-
const accessError = await assertProjectAccess(
|
|
8274
|
+
const accessError = await assertProjectAccess(
|
|
8275
|
+
supabase,
|
|
8276
|
+
userId,
|
|
8277
|
+
projectId
|
|
8278
|
+
);
|
|
7714
8279
|
if (accessError) {
|
|
7715
|
-
return {
|
|
8280
|
+
return {
|
|
8281
|
+
content: [{ type: "text", text: accessError }],
|
|
8282
|
+
isError: true
|
|
8283
|
+
};
|
|
7716
8284
|
}
|
|
7717
8285
|
const rows = posts.map((post) => ({
|
|
7718
8286
|
plan_id,
|
|
@@ -7741,7 +8309,12 @@ function registerPlanApprovalTools(server) {
|
|
|
7741
8309
|
};
|
|
7742
8310
|
if ((response_format || "text") === "json") {
|
|
7743
8311
|
return {
|
|
7744
|
-
content: [
|
|
8312
|
+
content: [
|
|
8313
|
+
{
|
|
8314
|
+
type: "text",
|
|
8315
|
+
text: JSON.stringify(asEnvelope15(payload), null, 2)
|
|
8316
|
+
}
|
|
8317
|
+
],
|
|
7745
8318
|
isError: false
|
|
7746
8319
|
};
|
|
7747
8320
|
}
|
|
@@ -7790,14 +8363,22 @@ function registerPlanApprovalTools(server) {
|
|
|
7790
8363
|
};
|
|
7791
8364
|
if ((response_format || "text") === "json") {
|
|
7792
8365
|
return {
|
|
7793
|
-
content: [
|
|
8366
|
+
content: [
|
|
8367
|
+
{
|
|
8368
|
+
type: "text",
|
|
8369
|
+
text: JSON.stringify(asEnvelope15(payload), null, 2)
|
|
8370
|
+
}
|
|
8371
|
+
],
|
|
7794
8372
|
isError: false
|
|
7795
8373
|
};
|
|
7796
8374
|
}
|
|
7797
8375
|
if (!data || data.length === 0) {
|
|
7798
8376
|
return {
|
|
7799
8377
|
content: [
|
|
7800
|
-
{
|
|
8378
|
+
{
|
|
8379
|
+
type: "text",
|
|
8380
|
+
text: `No approval items found for plan ${plan_id}.`
|
|
8381
|
+
}
|
|
7801
8382
|
],
|
|
7802
8383
|
isError: false
|
|
7803
8384
|
};
|
|
@@ -7810,7 +8391,10 @@ function registerPlanApprovalTools(server) {
|
|
|
7810
8391
|
}
|
|
7811
8392
|
lines.push("");
|
|
7812
8393
|
lines.push(`Total: ${data.length}`);
|
|
7813
|
-
return {
|
|
8394
|
+
return {
|
|
8395
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
8396
|
+
isError: false
|
|
8397
|
+
};
|
|
7814
8398
|
}
|
|
7815
8399
|
);
|
|
7816
8400
|
server.tool(
|
|
@@ -7829,7 +8413,10 @@ function registerPlanApprovalTools(server) {
|
|
|
7829
8413
|
if (decision === "edited" && !edited_post) {
|
|
7830
8414
|
return {
|
|
7831
8415
|
content: [
|
|
7832
|
-
{
|
|
8416
|
+
{
|
|
8417
|
+
type: "text",
|
|
8418
|
+
text: 'edited_post is required when decision is "edited".'
|
|
8419
|
+
}
|
|
7833
8420
|
],
|
|
7834
8421
|
isError: true
|
|
7835
8422
|
};
|
|
@@ -7842,7 +8429,9 @@ function registerPlanApprovalTools(server) {
|
|
|
7842
8429
|
if (decision === "edited") {
|
|
7843
8430
|
updates.edited_post = edited_post;
|
|
7844
8431
|
}
|
|
7845
|
-
const { data, error } = await supabase.from("content_plan_approvals").update(updates).eq("id", approval_id).eq("user_id", userId).eq("status", "pending").select(
|
|
8432
|
+
const { data, error } = await supabase.from("content_plan_approvals").update(updates).eq("id", approval_id).eq("user_id", userId).eq("status", "pending").select(
|
|
8433
|
+
"id, plan_id, post_id, status, reason, decided_at, original_post, edited_post"
|
|
8434
|
+
).maybeSingle();
|
|
7846
8435
|
if (error) {
|
|
7847
8436
|
return {
|
|
7848
8437
|
content: [
|
|
@@ -7867,7 +8456,12 @@ function registerPlanApprovalTools(server) {
|
|
|
7867
8456
|
}
|
|
7868
8457
|
if ((response_format || "text") === "json") {
|
|
7869
8458
|
return {
|
|
7870
|
-
content: [
|
|
8459
|
+
content: [
|
|
8460
|
+
{
|
|
8461
|
+
type: "text",
|
|
8462
|
+
text: JSON.stringify(asEnvelope15(data), null, 2)
|
|
8463
|
+
}
|
|
8464
|
+
],
|
|
7871
8465
|
isError: false
|
|
7872
8466
|
};
|
|
7873
8467
|
}
|
|
@@ -7932,7 +8526,7 @@ var TOOL_CATALOG = [
|
|
|
7932
8526
|
name: "check_status",
|
|
7933
8527
|
description: "Check status of async content generation job",
|
|
7934
8528
|
module: "content",
|
|
7935
|
-
scope: "mcp:
|
|
8529
|
+
scope: "mcp:read"
|
|
7936
8530
|
},
|
|
7937
8531
|
{
|
|
7938
8532
|
name: "create_storyboard",
|
|
@@ -8365,12 +8959,34 @@ function getJWKS(supabaseUrl) {
|
|
|
8365
8959
|
}
|
|
8366
8960
|
return jwks;
|
|
8367
8961
|
}
|
|
8962
|
+
var apiKeyCache = /* @__PURE__ */ new Map();
|
|
8963
|
+
var API_KEY_CACHE_TTL_MS = 6e4;
|
|
8368
8964
|
function createTokenVerifier(options) {
|
|
8369
8965
|
const { supabaseUrl, supabaseAnonKey } = options;
|
|
8370
8966
|
return {
|
|
8371
8967
|
async verifyAccessToken(token) {
|
|
8372
8968
|
if (token.startsWith("snk_")) {
|
|
8373
|
-
|
|
8969
|
+
const cached = apiKeyCache.get(token);
|
|
8970
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
8971
|
+
return cached.authInfo;
|
|
8972
|
+
}
|
|
8973
|
+
if (cached) apiKeyCache.delete(token);
|
|
8974
|
+
const authInfo = await verifyApiKey(
|
|
8975
|
+
token,
|
|
8976
|
+
supabaseUrl,
|
|
8977
|
+
supabaseAnonKey
|
|
8978
|
+
);
|
|
8979
|
+
apiKeyCache.set(token, {
|
|
8980
|
+
authInfo,
|
|
8981
|
+
expiresAt: Date.now() + API_KEY_CACHE_TTL_MS
|
|
8982
|
+
});
|
|
8983
|
+
if (apiKeyCache.size > 100) {
|
|
8984
|
+
const now = Date.now();
|
|
8985
|
+
for (const [k, v] of apiKeyCache) {
|
|
8986
|
+
if (v.expiresAt <= now) apiKeyCache.delete(k);
|
|
8987
|
+
}
|
|
8988
|
+
}
|
|
8989
|
+
return authInfo;
|
|
8374
8990
|
}
|
|
8375
8991
|
return verifySupabaseJwt(token, supabaseUrl);
|
|
8376
8992
|
}
|
|
@@ -8450,6 +9066,12 @@ if (!SUPABASE_URL2 || !SUPABASE_ANON_KEY) {
|
|
|
8450
9066
|
console.error("[MCP HTTP] Missing SUPABASE_URL or SUPABASE_ANON_KEY");
|
|
8451
9067
|
process.exit(1);
|
|
8452
9068
|
}
|
|
9069
|
+
var SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY ?? "";
|
|
9070
|
+
if (SUPABASE_SERVICE_ROLE_KEY && SUPABASE_SERVICE_ROLE_KEY.length < 100) {
|
|
9071
|
+
console.error(
|
|
9072
|
+
`[MCP HTTP] SUPABASE_SERVICE_ROLE_KEY looks invalid (${SUPABASE_SERVICE_ROLE_KEY.length} chars, expected 200+). Edge function calls may fail. Check your environment variables.`
|
|
9073
|
+
);
|
|
9074
|
+
}
|
|
8453
9075
|
process.on("uncaughtException", (err) => {
|
|
8454
9076
|
console.error(`[MCP HTTP] Uncaught exception: ${err.message}`);
|
|
8455
9077
|
process.exit(1);
|
|
@@ -8490,14 +9112,63 @@ var cleanupInterval = setInterval(
|
|
|
8490
9112
|
5 * 60 * 1e3
|
|
8491
9113
|
);
|
|
8492
9114
|
var app = express();
|
|
9115
|
+
app.disable("x-powered-by");
|
|
8493
9116
|
app.use(express.json());
|
|
8494
9117
|
app.set("trust proxy", 1);
|
|
9118
|
+
var ipBuckets = /* @__PURE__ */ new Map();
|
|
9119
|
+
var IP_RATE_MAX = 60;
|
|
9120
|
+
var IP_RATE_REFILL = 60 / 60;
|
|
9121
|
+
var IP_RATE_CLEANUP_INTERVAL = 10 * 60 * 1e3;
|
|
9122
|
+
setInterval(() => {
|
|
9123
|
+
const cutoff = Date.now() - 5 * 60 * 1e3;
|
|
9124
|
+
for (const [ip, bucket] of ipBuckets) {
|
|
9125
|
+
if (bucket.lastRefill < cutoff) ipBuckets.delete(ip);
|
|
9126
|
+
}
|
|
9127
|
+
}, IP_RATE_CLEANUP_INTERVAL).unref();
|
|
9128
|
+
app.use((req, res, next) => {
|
|
9129
|
+
if (req.path === "/health") return next();
|
|
9130
|
+
const ip = req.ip ?? req.socket.remoteAddress ?? "unknown";
|
|
9131
|
+
const now = Date.now();
|
|
9132
|
+
let bucket = ipBuckets.get(ip);
|
|
9133
|
+
if (!bucket) {
|
|
9134
|
+
bucket = { tokens: IP_RATE_MAX, lastRefill: now };
|
|
9135
|
+
ipBuckets.set(ip, bucket);
|
|
9136
|
+
}
|
|
9137
|
+
const elapsed = (now - bucket.lastRefill) / 1e3;
|
|
9138
|
+
bucket.tokens = Math.min(
|
|
9139
|
+
IP_RATE_MAX,
|
|
9140
|
+
bucket.tokens + elapsed * IP_RATE_REFILL
|
|
9141
|
+
);
|
|
9142
|
+
bucket.lastRefill = now;
|
|
9143
|
+
if (bucket.tokens < 1) {
|
|
9144
|
+
const retryAfter = Math.ceil((1 - bucket.tokens) / IP_RATE_REFILL);
|
|
9145
|
+
res.setHeader("Retry-After", String(retryAfter));
|
|
9146
|
+
res.status(429).json({
|
|
9147
|
+
error: "rate_limited",
|
|
9148
|
+
error_description: "Too many requests from this IP. Please slow down.",
|
|
9149
|
+
retry_after: retryAfter
|
|
9150
|
+
});
|
|
9151
|
+
return;
|
|
9152
|
+
}
|
|
9153
|
+
bucket.tokens -= 1;
|
|
9154
|
+
next();
|
|
9155
|
+
});
|
|
9156
|
+
app.use((_req, res, next) => {
|
|
9157
|
+
res.setHeader(
|
|
9158
|
+
"Strict-Transport-Security",
|
|
9159
|
+
"max-age=63072000; includeSubDomains"
|
|
9160
|
+
);
|
|
9161
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
9162
|
+
next();
|
|
9163
|
+
});
|
|
8495
9164
|
app.use((_req, res, next) => {
|
|
8496
9165
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
8497
9166
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
8498
|
-
res.setHeader(
|
|
9167
|
+
res.setHeader(
|
|
9168
|
+
"Access-Control-Allow-Headers",
|
|
9169
|
+
"Content-Type, Authorization, Mcp-Session-Id"
|
|
9170
|
+
);
|
|
8499
9171
|
res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
|
|
8500
|
-
res.setHeader("Vary", "Origin");
|
|
8501
9172
|
if (_req.method === "OPTIONS") {
|
|
8502
9173
|
res.status(204).end();
|
|
8503
9174
|
return;
|
|
@@ -8532,9 +9203,16 @@ async function authenticateRequest(req, res, next) {
|
|
|
8532
9203
|
const token = authHeader.slice(7);
|
|
8533
9204
|
try {
|
|
8534
9205
|
const authInfo = await tokenVerifier.verifyAccessToken(token);
|
|
9206
|
+
let scopes = authInfo.scopes;
|
|
9207
|
+
const scopeParam = req.query.scope;
|
|
9208
|
+
if (scopeParam) {
|
|
9209
|
+
const requestedScopes = scopeParam.split(",").map((s) => s.trim()).filter(Boolean);
|
|
9210
|
+
scopes = requestedScopes.filter((s) => authInfo.scopes.includes(s));
|
|
9211
|
+
if (scopes.length === 0) scopes = authInfo.scopes;
|
|
9212
|
+
}
|
|
8535
9213
|
req.auth = {
|
|
8536
9214
|
userId: authInfo.extra?.userId ?? authInfo.clientId,
|
|
8537
|
-
scopes
|
|
9215
|
+
scopes,
|
|
8538
9216
|
clientId: authInfo.clientId,
|
|
8539
9217
|
token: authInfo.token
|
|
8540
9218
|
};
|
|
@@ -8550,97 +9228,115 @@ async function authenticateRequest(req, res, next) {
|
|
|
8550
9228
|
app.get("/health", (_req, res) => {
|
|
8551
9229
|
res.json({ status: "ok", version: MCP_VERSION });
|
|
8552
9230
|
});
|
|
8553
|
-
app.get(
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
const auth = req.auth;
|
|
8567
|
-
const existingSessionId = req.headers["mcp-session-id"];
|
|
8568
|
-
const rl = checkRateLimit("read", auth.userId);
|
|
8569
|
-
if (!rl.allowed) {
|
|
8570
|
-
res.setHeader("Retry-After", String(rl.retryAfter));
|
|
8571
|
-
res.status(429).json({
|
|
8572
|
-
error: "rate_limited",
|
|
8573
|
-
error_description: "Too many requests. Please slow down.",
|
|
8574
|
-
retry_after: rl.retryAfter
|
|
9231
|
+
app.get(
|
|
9232
|
+
"/health/details",
|
|
9233
|
+
authenticateRequest,
|
|
9234
|
+
(_req, res) => {
|
|
9235
|
+
res.json({
|
|
9236
|
+
status: "ok",
|
|
9237
|
+
version: MCP_VERSION,
|
|
9238
|
+
transport: "streamable-http",
|
|
9239
|
+
sessions: sessions.size,
|
|
9240
|
+
sessionCap: MAX_SESSIONS,
|
|
9241
|
+
uptime: Math.floor(process.uptime()),
|
|
9242
|
+
memory: Math.round(process.memoryUsage().rss / 1024 / 1024),
|
|
9243
|
+
env: NODE_ENV
|
|
8575
9244
|
});
|
|
8576
|
-
return;
|
|
8577
9245
|
}
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
entry.lastActivity = Date.now();
|
|
8589
|
-
await requestContext.run(
|
|
8590
|
-
{ userId: auth.userId, scopes: auth.scopes, creditsUsed: 0, assetsGenerated: 0 },
|
|
8591
|
-
() => entry.transport.handleRequest(req, res, req.body)
|
|
8592
|
-
);
|
|
8593
|
-
return;
|
|
8594
|
-
}
|
|
8595
|
-
if (sessions.size >= MAX_SESSIONS) {
|
|
8596
|
-
res.status(429).json({
|
|
8597
|
-
error: "too_many_sessions",
|
|
8598
|
-
error_description: `Server session limit reached (${MAX_SESSIONS}). Try again later.`
|
|
8599
|
-
});
|
|
8600
|
-
return;
|
|
8601
|
-
}
|
|
8602
|
-
if (countUserSessions(auth.userId) >= MAX_SESSIONS_PER_USER) {
|
|
9246
|
+
);
|
|
9247
|
+
app.post(
|
|
9248
|
+
"/mcp",
|
|
9249
|
+
authenticateRequest,
|
|
9250
|
+
async (req, res) => {
|
|
9251
|
+
const auth = req.auth;
|
|
9252
|
+
const existingSessionId = req.headers["mcp-session-id"];
|
|
9253
|
+
const rl = checkRateLimit("read", auth.userId);
|
|
9254
|
+
if (!rl.allowed) {
|
|
9255
|
+
res.setHeader("Retry-After", String(rl.retryAfter));
|
|
8603
9256
|
res.status(429).json({
|
|
8604
|
-
error: "
|
|
8605
|
-
error_description:
|
|
9257
|
+
error: "rate_limited",
|
|
9258
|
+
error_description: "Too many requests. Please slow down.",
|
|
9259
|
+
retry_after: rl.retryAfter
|
|
8606
9260
|
});
|
|
8607
9261
|
return;
|
|
8608
9262
|
}
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
9263
|
+
try {
|
|
9264
|
+
if (existingSessionId && sessions.has(existingSessionId)) {
|
|
9265
|
+
const entry = sessions.get(existingSessionId);
|
|
9266
|
+
if (entry.userId !== auth.userId) {
|
|
9267
|
+
res.status(403).json({
|
|
9268
|
+
error: "forbidden",
|
|
9269
|
+
error_description: "Session belongs to another user"
|
|
9270
|
+
});
|
|
9271
|
+
return;
|
|
9272
|
+
}
|
|
9273
|
+
entry.lastActivity = Date.now();
|
|
9274
|
+
await requestContext.run(
|
|
9275
|
+
{
|
|
9276
|
+
userId: auth.userId,
|
|
9277
|
+
scopes: auth.scopes,
|
|
9278
|
+
creditsUsed: 0,
|
|
9279
|
+
assetsGenerated: 0
|
|
9280
|
+
},
|
|
9281
|
+
() => entry.transport.handleRequest(req, res, req.body)
|
|
9282
|
+
);
|
|
9283
|
+
return;
|
|
9284
|
+
}
|
|
9285
|
+
if (sessions.size >= MAX_SESSIONS) {
|
|
9286
|
+
res.status(429).json({
|
|
9287
|
+
error: "too_many_sessions",
|
|
9288
|
+
error_description: `Server session limit reached (${MAX_SESSIONS}). Try again later.`
|
|
8623
9289
|
});
|
|
9290
|
+
return;
|
|
8624
9291
|
}
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
9292
|
+
if (countUserSessions(auth.userId) >= MAX_SESSIONS_PER_USER) {
|
|
9293
|
+
res.status(429).json({
|
|
9294
|
+
error: "too_many_sessions",
|
|
9295
|
+
error_description: `Per-user session limit reached (${MAX_SESSIONS_PER_USER}). Close existing sessions or wait for timeout.`
|
|
9296
|
+
});
|
|
9297
|
+
return;
|
|
9298
|
+
}
|
|
9299
|
+
const server = new McpServer({
|
|
9300
|
+
name: "socialneuron",
|
|
9301
|
+
version: MCP_VERSION
|
|
9302
|
+
});
|
|
9303
|
+
applyScopeEnforcement(server, () => getRequestScopes() ?? auth.scopes);
|
|
9304
|
+
registerAllTools(server, { skipScreenshots: true });
|
|
9305
|
+
const transport = new StreamableHTTPServerTransport({
|
|
9306
|
+
sessionIdGenerator: () => randomUUID3(),
|
|
9307
|
+
onsessioninitialized: (sessionId) => {
|
|
9308
|
+
sessions.set(sessionId, {
|
|
9309
|
+
transport,
|
|
9310
|
+
server,
|
|
9311
|
+
lastActivity: Date.now(),
|
|
9312
|
+
userId: auth.userId
|
|
9313
|
+
});
|
|
9314
|
+
}
|
|
9315
|
+
});
|
|
9316
|
+
transport.onclose = () => {
|
|
9317
|
+
if (transport.sessionId) {
|
|
9318
|
+
sessions.delete(transport.sessionId);
|
|
9319
|
+
}
|
|
9320
|
+
};
|
|
9321
|
+
await server.connect(transport);
|
|
9322
|
+
await requestContext.run(
|
|
9323
|
+
{
|
|
9324
|
+
userId: auth.userId,
|
|
9325
|
+
scopes: auth.scopes,
|
|
9326
|
+
creditsUsed: 0,
|
|
9327
|
+
assetsGenerated: 0
|
|
9328
|
+
},
|
|
9329
|
+
() => transport.handleRequest(req, res, req.body)
|
|
9330
|
+
);
|
|
9331
|
+
} catch (err) {
|
|
9332
|
+
const message = err instanceof Error ? err.message : "Internal server error";
|
|
9333
|
+
console.error(`[MCP HTTP] POST /mcp error: ${message}`);
|
|
9334
|
+
if (!res.headersSent) {
|
|
9335
|
+
res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message } });
|
|
8629
9336
|
}
|
|
8630
|
-
};
|
|
8631
|
-
await server.connect(transport);
|
|
8632
|
-
await requestContext.run(
|
|
8633
|
-
{ userId: auth.userId, scopes: auth.scopes, creditsUsed: 0, assetsGenerated: 0 },
|
|
8634
|
-
() => transport.handleRequest(req, res, req.body)
|
|
8635
|
-
);
|
|
8636
|
-
} catch (err) {
|
|
8637
|
-
const message = err instanceof Error ? err.message : "Internal server error";
|
|
8638
|
-
console.error(`[MCP HTTP] POST /mcp error: ${message}`);
|
|
8639
|
-
if (!res.headersSent) {
|
|
8640
|
-
res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message } });
|
|
8641
9337
|
}
|
|
8642
9338
|
}
|
|
8643
|
-
|
|
9339
|
+
);
|
|
8644
9340
|
app.get("/mcp", authenticateRequest, async (req, res) => {
|
|
8645
9341
|
const sessionId = req.headers["mcp-session-id"];
|
|
8646
9342
|
if (!sessionId || !sessions.has(sessionId)) {
|
|
@@ -8656,28 +9352,39 @@ app.get("/mcp", authenticateRequest, async (req, res) => {
|
|
|
8656
9352
|
res.setHeader("X-Accel-Buffering", "no");
|
|
8657
9353
|
res.setHeader("Cache-Control", "no-cache");
|
|
8658
9354
|
await requestContext.run(
|
|
8659
|
-
{
|
|
9355
|
+
{
|
|
9356
|
+
userId: req.auth.userId,
|
|
9357
|
+
scopes: req.auth.scopes,
|
|
9358
|
+
creditsUsed: 0,
|
|
9359
|
+
assetsGenerated: 0
|
|
9360
|
+
},
|
|
8660
9361
|
() => entry.transport.handleRequest(req, res)
|
|
8661
9362
|
);
|
|
8662
9363
|
});
|
|
8663
|
-
app.delete(
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
9364
|
+
app.delete(
|
|
9365
|
+
"/mcp",
|
|
9366
|
+
authenticateRequest,
|
|
9367
|
+
async (req, res) => {
|
|
9368
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
9369
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
9370
|
+
res.status(400).json({ error: "Invalid or missing session ID" });
|
|
9371
|
+
return;
|
|
9372
|
+
}
|
|
9373
|
+
const entry = sessions.get(sessionId);
|
|
9374
|
+
if (entry.userId !== req.auth.userId) {
|
|
9375
|
+
res.status(403).json({ error: "Session belongs to another user" });
|
|
9376
|
+
return;
|
|
9377
|
+
}
|
|
9378
|
+
await entry.transport.close();
|
|
9379
|
+
await entry.server.close();
|
|
9380
|
+
sessions.delete(sessionId);
|
|
9381
|
+
res.status(200).json({ status: "session_closed" });
|
|
8673
9382
|
}
|
|
8674
|
-
|
|
8675
|
-
await entry.server.close();
|
|
8676
|
-
sessions.delete(sessionId);
|
|
8677
|
-
res.status(200).json({ status: "session_closed" });
|
|
8678
|
-
});
|
|
9383
|
+
);
|
|
8679
9384
|
var httpServer = app.listen(PORT, "0.0.0.0", () => {
|
|
8680
|
-
console.log(
|
|
9385
|
+
console.log(
|
|
9386
|
+
`[MCP HTTP] Social Neuron MCP Server listening on 0.0.0.0:${PORT}`
|
|
9387
|
+
);
|
|
8681
9388
|
console.log(`[MCP HTTP] Health: http://localhost:${PORT}/health`);
|
|
8682
9389
|
console.log(`[MCP HTTP] MCP endpoint: ${MCP_SERVER_URL}`);
|
|
8683
9390
|
console.log(`[MCP HTTP] Environment: ${NODE_ENV}`);
|