@socialneuron/mcp-server 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/http.js +1223 -524
- package/dist/index.js +1073 -440
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -587,7 +587,7 @@ var TOOL_SCOPES = {
|
|
|
587
587
|
adapt_content: "mcp:write",
|
|
588
588
|
generate_video: "mcp:write",
|
|
589
589
|
generate_image: "mcp:write",
|
|
590
|
-
check_status: "mcp:
|
|
590
|
+
check_status: "mcp:read",
|
|
591
591
|
render_demo_video: "mcp:write",
|
|
592
592
|
save_brand_profile: "mcp:write",
|
|
593
593
|
update_platform_voice: "mcp:write",
|
|
@@ -775,7 +775,76 @@ async function callEdgeFunction(functionName, body, options) {
|
|
|
775
775
|
}
|
|
776
776
|
}
|
|
777
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
|
+
|
|
778
846
|
// src/tools/ideation.ts
|
|
847
|
+
init_supabase();
|
|
779
848
|
function registerIdeationTools(server) {
|
|
780
849
|
server.tool(
|
|
781
850
|
"generate_content",
|
|
@@ -796,7 +865,9 @@ function registerIdeationTools(server) {
|
|
|
796
865
|
"facebook",
|
|
797
866
|
"threads",
|
|
798
867
|
"bluesky"
|
|
799
|
-
]).optional().describe(
|
|
868
|
+
]).optional().describe(
|
|
869
|
+
"Target social media platform. Helps tailor tone, length, and format."
|
|
870
|
+
),
|
|
800
871
|
brand_voice: z.string().max(500).optional().describe(
|
|
801
872
|
'Brand voice guidelines to follow (e.g. "professional and empathetic", "playful and Gen-Z"). Leave blank to use a neutral tone.'
|
|
802
873
|
),
|
|
@@ -807,7 +878,30 @@ function registerIdeationTools(server) {
|
|
|
807
878
|
"Project ID to auto-load brand profile and performance context for prompt enrichment."
|
|
808
879
|
)
|
|
809
880
|
},
|
|
810
|
-
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
|
+
}
|
|
811
905
|
let enrichedPrompt = prompt;
|
|
812
906
|
if (platform2) {
|
|
813
907
|
enrichedPrompt += `
|
|
@@ -939,8 +1033,12 @@ Content Type: ${content_type}`;
|
|
|
939
1033
|
category: z.string().optional().describe(
|
|
940
1034
|
"Category filter (for YouTube). Examples: general, entertainment, education, tech, music, gaming, sports, news."
|
|
941
1035
|
),
|
|
942
|
-
niche: z.string().optional().describe(
|
|
943
|
-
|
|
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
|
+
),
|
|
944
1042
|
force_refresh: z.boolean().optional().describe("Skip the server-side cache and fetch fresh data.")
|
|
945
1043
|
},
|
|
946
1044
|
async ({ source, category, niche, url, force_refresh }) => {
|
|
@@ -1015,7 +1113,9 @@ Content Type: ${content_type}`;
|
|
|
1015
1113
|
"adapt_content",
|
|
1016
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.",
|
|
1017
1115
|
{
|
|
1018
|
-
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
|
+
),
|
|
1019
1119
|
source_platform: z.enum([
|
|
1020
1120
|
"youtube",
|
|
1021
1121
|
"tiktok",
|
|
@@ -1025,7 +1125,9 @@ Content Type: ${content_type}`;
|
|
|
1025
1125
|
"facebook",
|
|
1026
1126
|
"threads",
|
|
1027
1127
|
"bluesky"
|
|
1028
|
-
]).optional().describe(
|
|
1128
|
+
]).optional().describe(
|
|
1129
|
+
"The platform the content was originally written for. Helps preserve intent."
|
|
1130
|
+
),
|
|
1029
1131
|
target_platform: z.enum([
|
|
1030
1132
|
"youtube",
|
|
1031
1133
|
"tiktok",
|
|
@@ -1039,9 +1141,33 @@ Content Type: ${content_type}`;
|
|
|
1039
1141
|
brand_voice: z.string().max(500).optional().describe(
|
|
1040
1142
|
'Brand voice guidelines to maintain during adaptation (e.g. "professional", "playful").'
|
|
1041
1143
|
),
|
|
1042
|
-
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
|
+
)
|
|
1043
1147
|
},
|
|
1044
|
-
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
|
+
}
|
|
1045
1171
|
const platformGuidelines = {
|
|
1046
1172
|
twitter: "Max 280 characters. Concise, punchy. 1-3 hashtags max. Thread-friendly.",
|
|
1047
1173
|
threads: "Max 500 characters. Conversational, opinion-driven. Minimal hashtags.",
|
|
@@ -1123,76 +1249,6 @@ ${content}`,
|
|
|
1123
1249
|
|
|
1124
1250
|
// src/tools/content.ts
|
|
1125
1251
|
import { z as z2 } from "zod";
|
|
1126
|
-
|
|
1127
|
-
// src/lib/rate-limit.ts
|
|
1128
|
-
var CATEGORY_CONFIGS = {
|
|
1129
|
-
posting: { maxTokens: 30, refillRate: 30 / 60 },
|
|
1130
|
-
// 30 req/min
|
|
1131
|
-
screenshot: { maxTokens: 10, refillRate: 10 / 60 },
|
|
1132
|
-
// 10 req/min
|
|
1133
|
-
read: { maxTokens: 60, refillRate: 60 / 60 }
|
|
1134
|
-
// 60 req/min
|
|
1135
|
-
};
|
|
1136
|
-
var RateLimiter = class {
|
|
1137
|
-
tokens;
|
|
1138
|
-
lastRefill;
|
|
1139
|
-
maxTokens;
|
|
1140
|
-
refillRate;
|
|
1141
|
-
// tokens per second
|
|
1142
|
-
constructor(config) {
|
|
1143
|
-
this.maxTokens = config.maxTokens;
|
|
1144
|
-
this.refillRate = config.refillRate;
|
|
1145
|
-
this.tokens = config.maxTokens;
|
|
1146
|
-
this.lastRefill = Date.now();
|
|
1147
|
-
}
|
|
1148
|
-
/**
|
|
1149
|
-
* Try to consume one token. Returns true if the request is allowed,
|
|
1150
|
-
* false if rate-limited.
|
|
1151
|
-
*/
|
|
1152
|
-
consume() {
|
|
1153
|
-
this.refill();
|
|
1154
|
-
if (this.tokens >= 1) {
|
|
1155
|
-
this.tokens -= 1;
|
|
1156
|
-
return true;
|
|
1157
|
-
}
|
|
1158
|
-
return false;
|
|
1159
|
-
}
|
|
1160
|
-
/**
|
|
1161
|
-
* Seconds until at least one token is available.
|
|
1162
|
-
*/
|
|
1163
|
-
retryAfter() {
|
|
1164
|
-
this.refill();
|
|
1165
|
-
if (this.tokens >= 1) return 0;
|
|
1166
|
-
return Math.ceil((1 - this.tokens) / this.refillRate);
|
|
1167
|
-
}
|
|
1168
|
-
refill() {
|
|
1169
|
-
const now = Date.now();
|
|
1170
|
-
const elapsed = (now - this.lastRefill) / 1e3;
|
|
1171
|
-
this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
|
|
1172
|
-
this.lastRefill = now;
|
|
1173
|
-
}
|
|
1174
|
-
};
|
|
1175
|
-
var limiters = /* @__PURE__ */ new Map();
|
|
1176
|
-
function getRateLimiter(category) {
|
|
1177
|
-
let limiter = limiters.get(category);
|
|
1178
|
-
if (!limiter) {
|
|
1179
|
-
const config = CATEGORY_CONFIGS[category] ?? CATEGORY_CONFIGS.read;
|
|
1180
|
-
limiter = new RateLimiter(config);
|
|
1181
|
-
limiters.set(category, limiter);
|
|
1182
|
-
}
|
|
1183
|
-
return limiter;
|
|
1184
|
-
}
|
|
1185
|
-
function checkRateLimit(category, key) {
|
|
1186
|
-
const bucketKey = key ? `${category}:${key}` : category;
|
|
1187
|
-
const limiter = getRateLimiter(bucketKey);
|
|
1188
|
-
const allowed = limiter.consume();
|
|
1189
|
-
return {
|
|
1190
|
-
allowed,
|
|
1191
|
-
retryAfter: allowed ? 0 : limiter.retryAfter()
|
|
1192
|
-
};
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
// src/tools/content.ts
|
|
1196
1252
|
init_supabase();
|
|
1197
1253
|
|
|
1198
1254
|
// src/lib/sanitize-error.ts
|
|
@@ -1246,8 +1302,19 @@ function sanitizeDbError(error) {
|
|
|
1246
1302
|
|
|
1247
1303
|
// src/tools/content.ts
|
|
1248
1304
|
init_request_context();
|
|
1249
|
-
|
|
1250
|
-
|
|
1305
|
+
|
|
1306
|
+
// src/lib/version.ts
|
|
1307
|
+
var MCP_VERSION = "1.4.2";
|
|
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
|
+
);
|
|
1251
1318
|
var _globalCreditsUsed = 0;
|
|
1252
1319
|
var _globalAssetsGenerated = 0;
|
|
1253
1320
|
function getCreditsUsed() {
|
|
@@ -1289,7 +1356,7 @@ function getCurrentBudgetStatus() {
|
|
|
1289
1356
|
function asEnvelope(data) {
|
|
1290
1357
|
return {
|
|
1291
1358
|
_meta: {
|
|
1292
|
-
version:
|
|
1359
|
+
version: MCP_VERSION,
|
|
1293
1360
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1294
1361
|
},
|
|
1295
1362
|
data
|
|
@@ -1371,8 +1438,12 @@ function registerContentTools(server) {
|
|
|
1371
1438
|
enable_audio: z2.boolean().optional().describe(
|
|
1372
1439
|
"Enable native audio generation. Kling 2.6: doubles cost. Kling 3.0: 50% more (std 30/sec, pro 40/sec). 5+ languages."
|
|
1373
1440
|
),
|
|
1374
|
-
image_url: z2.string().optional().describe(
|
|
1375
|
-
|
|
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
|
+
),
|
|
1376
1447
|
response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
1377
1448
|
},
|
|
1378
1449
|
async ({
|
|
@@ -1810,10 +1881,13 @@ function registerContentTools(server) {
|
|
|
1810
1881
|
};
|
|
1811
1882
|
}
|
|
1812
1883
|
if (job.external_id && (job.status === "pending" || job.status === "processing")) {
|
|
1813
|
-
const { data: liveStatus } = await callEdgeFunction(
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1884
|
+
const { data: liveStatus } = await callEdgeFunction(
|
|
1885
|
+
"kie-task-status",
|
|
1886
|
+
{
|
|
1887
|
+
taskId: job.external_id,
|
|
1888
|
+
model: job.model
|
|
1889
|
+
}
|
|
1890
|
+
);
|
|
1817
1891
|
if (liveStatus) {
|
|
1818
1892
|
const lines2 = [
|
|
1819
1893
|
`Job: ${job.id}`,
|
|
@@ -1887,7 +1961,12 @@ function registerContentTools(server) {
|
|
|
1887
1961
|
});
|
|
1888
1962
|
if (format === "json") {
|
|
1889
1963
|
return {
|
|
1890
|
-
content: [
|
|
1964
|
+
content: [
|
|
1965
|
+
{
|
|
1966
|
+
type: "text",
|
|
1967
|
+
text: JSON.stringify(asEnvelope(job), null, 2)
|
|
1968
|
+
}
|
|
1969
|
+
]
|
|
1891
1970
|
};
|
|
1892
1971
|
}
|
|
1893
1972
|
return {
|
|
@@ -1905,7 +1984,15 @@ function registerContentTools(server) {
|
|
|
1905
1984
|
brand_context: z2.string().max(3e3).optional().describe(
|
|
1906
1985
|
"Brand context JSON from extract_brand. Include colors, voice tone, visual style keywords for consistent branding across frames."
|
|
1907
1986
|
),
|
|
1908
|
-
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
|
+
),
|
|
1909
1996
|
target_duration: z2.number().min(5).max(120).optional().describe(
|
|
1910
1997
|
"Target total duration in seconds. Defaults to 30s for short-form, 60s for YouTube."
|
|
1911
1998
|
),
|
|
@@ -1913,7 +2000,9 @@ function registerContentTools(server) {
|
|
|
1913
2000
|
style: z2.string().optional().describe(
|
|
1914
2001
|
'Visual style direction (e.g., "cinematic", "anime", "documentary", "motion graphics").'
|
|
1915
2002
|
),
|
|
1916
|
-
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
|
+
)
|
|
1917
2006
|
},
|
|
1918
2007
|
async ({
|
|
1919
2008
|
concept,
|
|
@@ -1926,7 +2015,11 @@ function registerContentTools(server) {
|
|
|
1926
2015
|
}) => {
|
|
1927
2016
|
const format = response_format ?? "json";
|
|
1928
2017
|
const startedAt = Date.now();
|
|
1929
|
-
const isShortForm = [
|
|
2018
|
+
const isShortForm = [
|
|
2019
|
+
"tiktok",
|
|
2020
|
+
"instagram-reels",
|
|
2021
|
+
"youtube-shorts"
|
|
2022
|
+
].includes(platform2);
|
|
1930
2023
|
const duration = target_duration ?? (isShortForm ? 30 : 60);
|
|
1931
2024
|
const scenes = num_scenes ?? (isShortForm ? 7 : 10);
|
|
1932
2025
|
const aspectRatio = isShortForm ? "9:16" : "16:9";
|
|
@@ -2019,7 +2112,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
2019
2112
|
details: { error }
|
|
2020
2113
|
});
|
|
2021
2114
|
return {
|
|
2022
|
-
content: [
|
|
2115
|
+
content: [
|
|
2116
|
+
{
|
|
2117
|
+
type: "text",
|
|
2118
|
+
text: `Storyboard generation failed: ${error}`
|
|
2119
|
+
}
|
|
2120
|
+
],
|
|
2023
2121
|
isError: true
|
|
2024
2122
|
};
|
|
2025
2123
|
}
|
|
@@ -2035,7 +2133,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
2035
2133
|
try {
|
|
2036
2134
|
const parsed = JSON.parse(rawContent);
|
|
2037
2135
|
return {
|
|
2038
|
-
content: [
|
|
2136
|
+
content: [
|
|
2137
|
+
{
|
|
2138
|
+
type: "text",
|
|
2139
|
+
text: JSON.stringify(asEnvelope(parsed), null, 2)
|
|
2140
|
+
}
|
|
2141
|
+
]
|
|
2039
2142
|
};
|
|
2040
2143
|
} catch {
|
|
2041
2144
|
return {
|
|
@@ -2099,7 +2202,10 @@ Return ONLY valid JSON in this exact format:
|
|
|
2099
2202
|
isError: true
|
|
2100
2203
|
};
|
|
2101
2204
|
}
|
|
2102
|
-
const rateLimit = checkRateLimit(
|
|
2205
|
+
const rateLimit = checkRateLimit(
|
|
2206
|
+
"posting",
|
|
2207
|
+
`generate_voiceover:${userId}`
|
|
2208
|
+
);
|
|
2103
2209
|
if (!rateLimit.allowed) {
|
|
2104
2210
|
await logMcpToolInvocation({
|
|
2105
2211
|
toolName: "generate_voiceover",
|
|
@@ -2134,7 +2240,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
2134
2240
|
details: { error }
|
|
2135
2241
|
});
|
|
2136
2242
|
return {
|
|
2137
|
-
content: [
|
|
2243
|
+
content: [
|
|
2244
|
+
{
|
|
2245
|
+
type: "text",
|
|
2246
|
+
text: `Voiceover generation failed: ${error}`
|
|
2247
|
+
}
|
|
2248
|
+
],
|
|
2138
2249
|
isError: true
|
|
2139
2250
|
};
|
|
2140
2251
|
}
|
|
@@ -2147,7 +2258,10 @@ Return ONLY valid JSON in this exact format:
|
|
|
2147
2258
|
});
|
|
2148
2259
|
return {
|
|
2149
2260
|
content: [
|
|
2150
|
-
{
|
|
2261
|
+
{
|
|
2262
|
+
type: "text",
|
|
2263
|
+
text: "Voiceover generation failed: no audio URL returned."
|
|
2264
|
+
}
|
|
2151
2265
|
],
|
|
2152
2266
|
isError: true
|
|
2153
2267
|
};
|
|
@@ -2219,7 +2333,9 @@ Return ONLY valid JSON in this exact format:
|
|
|
2219
2333
|
"Carousel template. hormozi-authority: bold typography, one idea per slide, dark backgrounds. educational-series: numbered tips. Default: hormozi-authority."
|
|
2220
2334
|
),
|
|
2221
2335
|
slide_count: z2.number().min(3).max(10).optional().describe("Number of slides (3-10). Default: 7."),
|
|
2222
|
-
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
|
+
),
|
|
2223
2339
|
style: z2.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
|
|
2224
2340
|
"Visual style. hormozi: black bg, bold white text, gold accents. Default: hormozi (when using hormozi-authority template)."
|
|
2225
2341
|
),
|
|
@@ -2256,7 +2372,10 @@ Return ONLY valid JSON in this exact format:
|
|
|
2256
2372
|
};
|
|
2257
2373
|
}
|
|
2258
2374
|
const userId = await getDefaultUserId();
|
|
2259
|
-
const rateLimit = checkRateLimit(
|
|
2375
|
+
const rateLimit = checkRateLimit(
|
|
2376
|
+
"posting",
|
|
2377
|
+
`generate_carousel:${userId}`
|
|
2378
|
+
);
|
|
2260
2379
|
if (!rateLimit.allowed) {
|
|
2261
2380
|
await logMcpToolInvocation({
|
|
2262
2381
|
toolName: "generate_carousel",
|
|
@@ -2294,7 +2413,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
2294
2413
|
details: { error }
|
|
2295
2414
|
});
|
|
2296
2415
|
return {
|
|
2297
|
-
content: [
|
|
2416
|
+
content: [
|
|
2417
|
+
{
|
|
2418
|
+
type: "text",
|
|
2419
|
+
text: `Carousel generation failed: ${error}`
|
|
2420
|
+
}
|
|
2421
|
+
],
|
|
2298
2422
|
isError: true
|
|
2299
2423
|
};
|
|
2300
2424
|
}
|
|
@@ -2306,7 +2430,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
2306
2430
|
details: { error: "No carousel data returned" }
|
|
2307
2431
|
});
|
|
2308
2432
|
return {
|
|
2309
|
-
content: [
|
|
2433
|
+
content: [
|
|
2434
|
+
{
|
|
2435
|
+
type: "text",
|
|
2436
|
+
text: "Carousel generation returned no data."
|
|
2437
|
+
}
|
|
2438
|
+
],
|
|
2310
2439
|
isError: true
|
|
2311
2440
|
};
|
|
2312
2441
|
}
|
|
@@ -2507,7 +2636,7 @@ var PLATFORM_CASE_MAP = {
|
|
|
2507
2636
|
function asEnvelope2(data) {
|
|
2508
2637
|
return {
|
|
2509
2638
|
_meta: {
|
|
2510
|
-
version:
|
|
2639
|
+
version: MCP_VERSION,
|
|
2511
2640
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2512
2641
|
},
|
|
2513
2642
|
data
|
|
@@ -2539,15 +2668,21 @@ function registerDistributionTools(server) {
|
|
|
2539
2668
|
"threads",
|
|
2540
2669
|
"bluesky"
|
|
2541
2670
|
])
|
|
2542
|
-
).min(1).describe(
|
|
2671
|
+
).min(1).describe(
|
|
2672
|
+
"Target platforms to post to. Each must have an active OAuth connection."
|
|
2673
|
+
),
|
|
2543
2674
|
title: z3.string().optional().describe("Post title (used by YouTube and some other platforms)."),
|
|
2544
|
-
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
|
+
),
|
|
2545
2678
|
schedule_at: z3.string().optional().describe(
|
|
2546
2679
|
'ISO 8601 datetime for scheduled posting (e.g. "2026-03-15T14:00:00Z"). Omit for immediate posting.'
|
|
2547
2680
|
),
|
|
2548
2681
|
project_id: z3.string().optional().describe("Social Neuron project ID to associate this post with."),
|
|
2549
2682
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
|
|
2550
|
-
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
|
+
)
|
|
2551
2686
|
},
|
|
2552
2687
|
async ({
|
|
2553
2688
|
media_url,
|
|
@@ -2566,7 +2701,12 @@ function registerDistributionTools(server) {
|
|
|
2566
2701
|
const startedAt = Date.now();
|
|
2567
2702
|
if ((!caption || caption.trim().length === 0) && (!title || title.trim().length === 0)) {
|
|
2568
2703
|
return {
|
|
2569
|
-
content: [
|
|
2704
|
+
content: [
|
|
2705
|
+
{
|
|
2706
|
+
type: "text",
|
|
2707
|
+
text: "Either caption or title is required."
|
|
2708
|
+
}
|
|
2709
|
+
],
|
|
2570
2710
|
isError: true
|
|
2571
2711
|
};
|
|
2572
2712
|
}
|
|
@@ -2589,7 +2729,9 @@ function registerDistributionTools(server) {
|
|
|
2589
2729
|
isError: true
|
|
2590
2730
|
};
|
|
2591
2731
|
}
|
|
2592
|
-
const normalizedPlatforms = platforms.map(
|
|
2732
|
+
const normalizedPlatforms = platforms.map(
|
|
2733
|
+
(p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
|
|
2734
|
+
);
|
|
2593
2735
|
let finalCaption = caption;
|
|
2594
2736
|
if (attribution && finalCaption) {
|
|
2595
2737
|
finalCaption = `${finalCaption}
|
|
@@ -2653,7 +2795,9 @@ Created with Social Neuron`;
|
|
|
2653
2795
|
];
|
|
2654
2796
|
for (const [platform2, result] of Object.entries(data.results)) {
|
|
2655
2797
|
if (result.success) {
|
|
2656
|
-
lines.push(
|
|
2798
|
+
lines.push(
|
|
2799
|
+
` ${platform2}: OK (jobId=${result.jobId}, postId=${result.postId})`
|
|
2800
|
+
);
|
|
2657
2801
|
} else {
|
|
2658
2802
|
lines.push(` ${platform2}: FAILED - ${result.error}`);
|
|
2659
2803
|
}
|
|
@@ -2670,7 +2814,12 @@ Created with Social Neuron`;
|
|
|
2670
2814
|
});
|
|
2671
2815
|
if (format === "json") {
|
|
2672
2816
|
return {
|
|
2673
|
-
content: [
|
|
2817
|
+
content: [
|
|
2818
|
+
{
|
|
2819
|
+
type: "text",
|
|
2820
|
+
text: JSON.stringify(asEnvelope2(data), null, 2)
|
|
2821
|
+
}
|
|
2822
|
+
],
|
|
2674
2823
|
isError: !data.success
|
|
2675
2824
|
};
|
|
2676
2825
|
}
|
|
@@ -2726,12 +2875,17 @@ Created with Social Neuron`;
|
|
|
2726
2875
|
for (const account of accounts) {
|
|
2727
2876
|
const name = account.username || "(unnamed)";
|
|
2728
2877
|
const platformLower = account.platform.toLowerCase();
|
|
2729
|
-
lines.push(
|
|
2878
|
+
lines.push(
|
|
2879
|
+
` ${platformLower}: ${name} (connected ${account.created_at.split("T")[0]})`
|
|
2880
|
+
);
|
|
2730
2881
|
}
|
|
2731
2882
|
if (format === "json") {
|
|
2732
2883
|
return {
|
|
2733
2884
|
content: [
|
|
2734
|
-
{
|
|
2885
|
+
{
|
|
2886
|
+
type: "text",
|
|
2887
|
+
text: JSON.stringify(asEnvelope2({ accounts }), null, 2)
|
|
2888
|
+
}
|
|
2735
2889
|
]
|
|
2736
2890
|
};
|
|
2737
2891
|
}
|
|
@@ -2793,7 +2947,10 @@ Created with Social Neuron`;
|
|
|
2793
2947
|
if (format === "json") {
|
|
2794
2948
|
return {
|
|
2795
2949
|
content: [
|
|
2796
|
-
{
|
|
2950
|
+
{
|
|
2951
|
+
type: "text",
|
|
2952
|
+
text: JSON.stringify(asEnvelope2({ posts: [] }), null, 2)
|
|
2953
|
+
}
|
|
2797
2954
|
]
|
|
2798
2955
|
};
|
|
2799
2956
|
}
|
|
@@ -2810,7 +2967,10 @@ Created with Social Neuron`;
|
|
|
2810
2967
|
if (format === "json") {
|
|
2811
2968
|
return {
|
|
2812
2969
|
content: [
|
|
2813
|
-
{
|
|
2970
|
+
{
|
|
2971
|
+
type: "text",
|
|
2972
|
+
text: JSON.stringify(asEnvelope2({ posts }), null, 2)
|
|
2973
|
+
}
|
|
2814
2974
|
]
|
|
2815
2975
|
};
|
|
2816
2976
|
}
|
|
@@ -2871,7 +3031,13 @@ Created with Social Neuron`;
|
|
|
2871
3031
|
min_gap_hours: z3.number().min(1).max(24).default(4).describe("Minimum gap between posts on same platform"),
|
|
2872
3032
|
response_format: z3.enum(["text", "json"]).default("text")
|
|
2873
3033
|
},
|
|
2874
|
-
async ({
|
|
3034
|
+
async ({
|
|
3035
|
+
platforms,
|
|
3036
|
+
count,
|
|
3037
|
+
start_after,
|
|
3038
|
+
min_gap_hours,
|
|
3039
|
+
response_format
|
|
3040
|
+
}) => {
|
|
2875
3041
|
const startedAt = Date.now();
|
|
2876
3042
|
try {
|
|
2877
3043
|
const userId = await getDefaultUserId();
|
|
@@ -2882,7 +3048,9 @@ Created with Social Neuron`;
|
|
|
2882
3048
|
const gapMs = min_gap_hours * 60 * 60 * 1e3;
|
|
2883
3049
|
const candidates = [];
|
|
2884
3050
|
for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
|
|
2885
|
-
const date = new Date(
|
|
3051
|
+
const date = new Date(
|
|
3052
|
+
startDate.getTime() + dayOffset * 24 * 60 * 60 * 1e3
|
|
3053
|
+
);
|
|
2886
3054
|
const dayOfWeek = date.getUTCDay();
|
|
2887
3055
|
for (const platform2 of platforms) {
|
|
2888
3056
|
const hours = PREFERRED_HOURS[platform2] ?? [12, 16];
|
|
@@ -2891,8 +3059,11 @@ Created with Social Neuron`;
|
|
|
2891
3059
|
slotDate.setUTCHours(hours[hourIdx], 0, 0, 0);
|
|
2892
3060
|
if (slotDate <= startDate) continue;
|
|
2893
3061
|
const hasConflict = (existingPosts ?? []).some((post) => {
|
|
2894
|
-
if (String(post.platform).toLowerCase() !== platform2)
|
|
2895
|
-
|
|
3062
|
+
if (String(post.platform).toLowerCase() !== platform2)
|
|
3063
|
+
return false;
|
|
3064
|
+
const postTime = new Date(
|
|
3065
|
+
post.scheduled_at ?? post.published_at
|
|
3066
|
+
).getTime();
|
|
2896
3067
|
return Math.abs(postTime - slotDate.getTime()) < gapMs;
|
|
2897
3068
|
});
|
|
2898
3069
|
let engagementScore = hours.length - hourIdx;
|
|
@@ -2937,15 +3108,22 @@ Created with Social Neuron`;
|
|
|
2937
3108
|
};
|
|
2938
3109
|
}
|
|
2939
3110
|
const lines = [];
|
|
2940
|
-
lines.push(
|
|
3111
|
+
lines.push(
|
|
3112
|
+
`Found ${slots.length} optimal slots (${conflictsAvoided} conflicts avoided):`
|
|
3113
|
+
);
|
|
2941
3114
|
lines.push("");
|
|
2942
3115
|
lines.push("Datetime (UTC) | Platform | Score");
|
|
2943
3116
|
lines.push("-------------------------+------------+------");
|
|
2944
3117
|
for (const s of slots) {
|
|
2945
3118
|
const dt = s.datetime.replace("T", " ").slice(0, 19);
|
|
2946
|
-
lines.push(
|
|
3119
|
+
lines.push(
|
|
3120
|
+
`${dt.padEnd(25)}| ${s.platform.padEnd(11)}| ${s.engagement_score}`
|
|
3121
|
+
);
|
|
2947
3122
|
}
|
|
2948
|
-
return {
|
|
3123
|
+
return {
|
|
3124
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
3125
|
+
isError: false
|
|
3126
|
+
};
|
|
2949
3127
|
} catch (err) {
|
|
2950
3128
|
const durationMs = Date.now() - startedAt;
|
|
2951
3129
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2956,7 +3134,9 @@ Created with Social Neuron`;
|
|
|
2956
3134
|
details: { error: message }
|
|
2957
3135
|
});
|
|
2958
3136
|
return {
|
|
2959
|
-
content: [
|
|
3137
|
+
content: [
|
|
3138
|
+
{ type: "text", text: `Failed to find slots: ${message}` }
|
|
3139
|
+
],
|
|
2960
3140
|
isError: true
|
|
2961
3141
|
};
|
|
2962
3142
|
}
|
|
@@ -2983,8 +3163,12 @@ Created with Social Neuron`;
|
|
|
2983
3163
|
auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
|
|
2984
3164
|
dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
|
|
2985
3165
|
response_format: z3.enum(["text", "json"]).default("text"),
|
|
2986
|
-
enforce_quality: z3.boolean().default(true).describe(
|
|
2987
|
-
|
|
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
|
+
),
|
|
2988
3172
|
batch_size: z3.number().int().min(1).max(10).default(4).describe("Concurrent schedule calls per platform batch."),
|
|
2989
3173
|
idempotency_seed: z3.string().max(128).optional().describe("Optional stable seed used when building idempotency keys.")
|
|
2990
3174
|
},
|
|
@@ -3023,17 +3207,25 @@ Created with Social Neuron`;
|
|
|
3023
3207
|
if (!stored?.plan_payload) {
|
|
3024
3208
|
return {
|
|
3025
3209
|
content: [
|
|
3026
|
-
{
|
|
3210
|
+
{
|
|
3211
|
+
type: "text",
|
|
3212
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
3213
|
+
}
|
|
3027
3214
|
],
|
|
3028
3215
|
isError: true
|
|
3029
3216
|
};
|
|
3030
3217
|
}
|
|
3031
3218
|
const payload = stored.plan_payload;
|
|
3032
|
-
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;
|
|
3033
3222
|
if (!postsFromPayload) {
|
|
3034
3223
|
return {
|
|
3035
3224
|
content: [
|
|
3036
|
-
{
|
|
3225
|
+
{
|
|
3226
|
+
type: "text",
|
|
3227
|
+
text: `Stored plan ${plan_id} has no posts array.`
|
|
3228
|
+
}
|
|
3037
3229
|
],
|
|
3038
3230
|
isError: true
|
|
3039
3231
|
};
|
|
@@ -3125,7 +3317,10 @@ Created with Social Neuron`;
|
|
|
3125
3317
|
approvalSummary = {
|
|
3126
3318
|
total: approvals.length,
|
|
3127
3319
|
eligible: approvedPosts.length,
|
|
3128
|
-
skipped: Math.max(
|
|
3320
|
+
skipped: Math.max(
|
|
3321
|
+
0,
|
|
3322
|
+
workingPlan.posts.length - approvedPosts.length
|
|
3323
|
+
)
|
|
3129
3324
|
};
|
|
3130
3325
|
workingPlan = {
|
|
3131
3326
|
...workingPlan,
|
|
@@ -3159,9 +3354,14 @@ Created with Social Neuron`;
|
|
|
3159
3354
|
try {
|
|
3160
3355
|
const { data: settingsData } = await supabase.from("system_settings").select("value").eq("key", "content_safety").maybeSingle();
|
|
3161
3356
|
if (settingsData?.value?.quality_threshold !== void 0) {
|
|
3162
|
-
const parsedThreshold = Number(
|
|
3357
|
+
const parsedThreshold = Number(
|
|
3358
|
+
settingsData.value.quality_threshold
|
|
3359
|
+
);
|
|
3163
3360
|
if (Number.isFinite(parsedThreshold)) {
|
|
3164
|
-
effectiveQualityThreshold = Math.max(
|
|
3361
|
+
effectiveQualityThreshold = Math.max(
|
|
3362
|
+
0,
|
|
3363
|
+
Math.min(35, Math.trunc(parsedThreshold))
|
|
3364
|
+
);
|
|
3165
3365
|
}
|
|
3166
3366
|
}
|
|
3167
3367
|
if (Array.isArray(settingsData?.value?.custom_banned_terms)) {
|
|
@@ -3197,13 +3397,18 @@ Created with Social Neuron`;
|
|
|
3197
3397
|
}
|
|
3198
3398
|
};
|
|
3199
3399
|
});
|
|
3200
|
-
const qualityPassed = postsWithResults.filter(
|
|
3400
|
+
const qualityPassed = postsWithResults.filter(
|
|
3401
|
+
(post) => post.quality.passed
|
|
3402
|
+
).length;
|
|
3201
3403
|
const qualitySummary = {
|
|
3202
3404
|
total_posts: postsWithResults.length,
|
|
3203
3405
|
passed: qualityPassed,
|
|
3204
3406
|
failed: postsWithResults.length - qualityPassed,
|
|
3205
3407
|
avg_score: postsWithResults.length > 0 ? Number(
|
|
3206
|
-
(postsWithResults.reduce(
|
|
3408
|
+
(postsWithResults.reduce(
|
|
3409
|
+
(sum, post) => sum + post.quality.score,
|
|
3410
|
+
0
|
|
3411
|
+
) / postsWithResults.length).toFixed(2)
|
|
3207
3412
|
) : 0
|
|
3208
3413
|
};
|
|
3209
3414
|
if (dry_run) {
|
|
@@ -3273,8 +3478,13 @@ Created with Social Neuron`;
|
|
|
3273
3478
|
}
|
|
3274
3479
|
}
|
|
3275
3480
|
lines2.push("");
|
|
3276
|
-
lines2.push(
|
|
3277
|
-
|
|
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
|
+
};
|
|
3278
3488
|
}
|
|
3279
3489
|
let scheduled = 0;
|
|
3280
3490
|
let failed = 0;
|
|
@@ -3366,7 +3576,8 @@ Created with Social Neuron`;
|
|
|
3366
3576
|
}
|
|
3367
3577
|
const chunk = (arr, size) => {
|
|
3368
3578
|
const out = [];
|
|
3369
|
-
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));
|
|
3370
3581
|
return out;
|
|
3371
3582
|
};
|
|
3372
3583
|
const platformBatches = Array.from(grouped.entries()).map(
|
|
@@ -3374,7 +3585,9 @@ Created with Social Neuron`;
|
|
|
3374
3585
|
const platformResults = [];
|
|
3375
3586
|
const batches = chunk(platformPosts, batch_size);
|
|
3376
3587
|
for (const batch of batches) {
|
|
3377
|
-
const settled = await Promise.allSettled(
|
|
3588
|
+
const settled = await Promise.allSettled(
|
|
3589
|
+
batch.map((post) => scheduleOne(post))
|
|
3590
|
+
);
|
|
3378
3591
|
for (const outcome of settled) {
|
|
3379
3592
|
if (outcome.status === "fulfilled") {
|
|
3380
3593
|
platformResults.push(outcome.value);
|
|
@@ -3440,7 +3653,11 @@ Created with Social Neuron`;
|
|
|
3440
3653
|
plan_id: effectivePlanId,
|
|
3441
3654
|
approvals: approvalSummary,
|
|
3442
3655
|
posts: results,
|
|
3443
|
-
summary: {
|
|
3656
|
+
summary: {
|
|
3657
|
+
total_posts: workingPlan.posts.length,
|
|
3658
|
+
scheduled,
|
|
3659
|
+
failed
|
|
3660
|
+
}
|
|
3444
3661
|
}),
|
|
3445
3662
|
null,
|
|
3446
3663
|
2
|
|
@@ -3478,7 +3695,12 @@ Created with Social Neuron`;
|
|
|
3478
3695
|
details: { error: message }
|
|
3479
3696
|
});
|
|
3480
3697
|
return {
|
|
3481
|
-
content: [
|
|
3698
|
+
content: [
|
|
3699
|
+
{
|
|
3700
|
+
type: "text",
|
|
3701
|
+
text: `Batch scheduling failed: ${message}`
|
|
3702
|
+
}
|
|
3703
|
+
],
|
|
3482
3704
|
isError: true
|
|
3483
3705
|
};
|
|
3484
3706
|
}
|
|
@@ -3492,7 +3714,7 @@ import { z as z4 } from "zod";
|
|
|
3492
3714
|
function asEnvelope3(data) {
|
|
3493
3715
|
return {
|
|
3494
3716
|
_meta: {
|
|
3495
|
-
version:
|
|
3717
|
+
version: MCP_VERSION,
|
|
3496
3718
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3497
3719
|
},
|
|
3498
3720
|
data
|
|
@@ -3605,7 +3827,9 @@ function registerAnalyticsTools(server) {
|
|
|
3605
3827
|
]
|
|
3606
3828
|
};
|
|
3607
3829
|
}
|
|
3608
|
-
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);
|
|
3609
3833
|
if (simpleError) {
|
|
3610
3834
|
return {
|
|
3611
3835
|
content: [
|
|
@@ -3617,7 +3841,12 @@ function registerAnalyticsTools(server) {
|
|
|
3617
3841
|
isError: true
|
|
3618
3842
|
};
|
|
3619
3843
|
}
|
|
3620
|
-
return formatSimpleAnalytics(
|
|
3844
|
+
return formatSimpleAnalytics(
|
|
3845
|
+
simpleRows,
|
|
3846
|
+
platform2,
|
|
3847
|
+
lookbackDays,
|
|
3848
|
+
format
|
|
3849
|
+
);
|
|
3621
3850
|
}
|
|
3622
3851
|
if (!rows || rows.length === 0) {
|
|
3623
3852
|
if (format === "json") {
|
|
@@ -3694,7 +3923,10 @@ function registerAnalyticsTools(server) {
|
|
|
3694
3923
|
const format = response_format ?? "text";
|
|
3695
3924
|
const startedAt = Date.now();
|
|
3696
3925
|
const userId = await getDefaultUserId();
|
|
3697
|
-
const rateLimit = checkRateLimit(
|
|
3926
|
+
const rateLimit = checkRateLimit(
|
|
3927
|
+
"posting",
|
|
3928
|
+
`refresh_platform_analytics:${userId}`
|
|
3929
|
+
);
|
|
3698
3930
|
if (!rateLimit.allowed) {
|
|
3699
3931
|
await logMcpToolInvocation({
|
|
3700
3932
|
toolName: "refresh_platform_analytics",
|
|
@@ -3712,7 +3944,9 @@ function registerAnalyticsTools(server) {
|
|
|
3712
3944
|
isError: true
|
|
3713
3945
|
};
|
|
3714
3946
|
}
|
|
3715
|
-
const { data, error } = await callEdgeFunction("fetch-analytics", {
|
|
3947
|
+
const { data, error } = await callEdgeFunction("fetch-analytics", {
|
|
3948
|
+
userId
|
|
3949
|
+
});
|
|
3716
3950
|
if (error) {
|
|
3717
3951
|
await logMcpToolInvocation({
|
|
3718
3952
|
toolName: "refresh_platform_analytics",
|
|
@@ -3721,7 +3955,12 @@ function registerAnalyticsTools(server) {
|
|
|
3721
3955
|
details: { error }
|
|
3722
3956
|
});
|
|
3723
3957
|
return {
|
|
3724
|
-
content: [
|
|
3958
|
+
content: [
|
|
3959
|
+
{
|
|
3960
|
+
type: "text",
|
|
3961
|
+
text: `Error refreshing analytics: ${error}`
|
|
3962
|
+
}
|
|
3963
|
+
],
|
|
3725
3964
|
isError: true
|
|
3726
3965
|
};
|
|
3727
3966
|
}
|
|
@@ -3734,12 +3973,18 @@ function registerAnalyticsTools(server) {
|
|
|
3734
3973
|
details: { error: "Edge function returned success=false" }
|
|
3735
3974
|
});
|
|
3736
3975
|
return {
|
|
3737
|
-
content: [
|
|
3976
|
+
content: [
|
|
3977
|
+
{ type: "text", text: "Analytics refresh failed." }
|
|
3978
|
+
],
|
|
3738
3979
|
isError: true
|
|
3739
3980
|
};
|
|
3740
3981
|
}
|
|
3741
|
-
const queued = (result.results ?? []).filter(
|
|
3742
|
-
|
|
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;
|
|
3743
3988
|
const lines = [
|
|
3744
3989
|
`Analytics refresh triggered successfully.`,
|
|
3745
3990
|
` Posts processed: ${result.postsProcessed}`,
|
|
@@ -3785,7 +4030,10 @@ function formatAnalytics(summary, days, format) {
|
|
|
3785
4030
|
if (format === "json") {
|
|
3786
4031
|
return {
|
|
3787
4032
|
content: [
|
|
3788
|
-
{
|
|
4033
|
+
{
|
|
4034
|
+
type: "text",
|
|
4035
|
+
text: JSON.stringify(asEnvelope3({ ...summary, days }), null, 2)
|
|
4036
|
+
}
|
|
3789
4037
|
]
|
|
3790
4038
|
};
|
|
3791
4039
|
}
|
|
@@ -3902,10 +4150,159 @@ function formatSimpleAnalytics(rows, platform2, days, format) {
|
|
|
3902
4150
|
// src/tools/brand.ts
|
|
3903
4151
|
import { z as z5 } from "zod";
|
|
3904
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
|
|
3905
4302
|
function asEnvelope4(data) {
|
|
3906
4303
|
return {
|
|
3907
4304
|
_meta: {
|
|
3908
|
-
version:
|
|
4305
|
+
version: MCP_VERSION,
|
|
3909
4306
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3910
4307
|
},
|
|
3911
4308
|
data
|
|
@@ -3922,6 +4319,15 @@ function registerBrandTools(server) {
|
|
|
3922
4319
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
3923
4320
|
},
|
|
3924
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
|
+
}
|
|
3925
4331
|
const { data, error } = await callEdgeFunction(
|
|
3926
4332
|
"brand-extract",
|
|
3927
4333
|
{ url },
|
|
@@ -3951,7 +4357,12 @@ function registerBrandTools(server) {
|
|
|
3951
4357
|
}
|
|
3952
4358
|
if ((response_format || "text") === "json") {
|
|
3953
4359
|
return {
|
|
3954
|
-
content: [
|
|
4360
|
+
content: [
|
|
4361
|
+
{
|
|
4362
|
+
type: "text",
|
|
4363
|
+
text: JSON.stringify(asEnvelope4(data), null, 2)
|
|
4364
|
+
}
|
|
4365
|
+
]
|
|
3955
4366
|
};
|
|
3956
4367
|
}
|
|
3957
4368
|
const lines = [
|
|
@@ -4015,7 +4426,12 @@ function registerBrandTools(server) {
|
|
|
4015
4426
|
const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
|
|
4016
4427
|
if (!membership) {
|
|
4017
4428
|
return {
|
|
4018
|
-
content: [
|
|
4429
|
+
content: [
|
|
4430
|
+
{
|
|
4431
|
+
type: "text",
|
|
4432
|
+
text: "Project is not accessible to current user."
|
|
4433
|
+
}
|
|
4434
|
+
],
|
|
4019
4435
|
isError: true
|
|
4020
4436
|
};
|
|
4021
4437
|
}
|
|
@@ -4034,13 +4450,21 @@ function registerBrandTools(server) {
|
|
|
4034
4450
|
if (!data) {
|
|
4035
4451
|
return {
|
|
4036
4452
|
content: [
|
|
4037
|
-
{
|
|
4453
|
+
{
|
|
4454
|
+
type: "text",
|
|
4455
|
+
text: "No active brand profile found for this project."
|
|
4456
|
+
}
|
|
4038
4457
|
]
|
|
4039
4458
|
};
|
|
4040
4459
|
}
|
|
4041
4460
|
if ((response_format || "text") === "json") {
|
|
4042
4461
|
return {
|
|
4043
|
-
content: [
|
|
4462
|
+
content: [
|
|
4463
|
+
{
|
|
4464
|
+
type: "text",
|
|
4465
|
+
text: JSON.stringify(asEnvelope4(data), null, 2)
|
|
4466
|
+
}
|
|
4467
|
+
]
|
|
4044
4468
|
};
|
|
4045
4469
|
}
|
|
4046
4470
|
const lines = [
|
|
@@ -4061,11 +4485,18 @@ function registerBrandTools(server) {
|
|
|
4061
4485
|
"Persist a brand profile as the active profile for a project.",
|
|
4062
4486
|
{
|
|
4063
4487
|
project_id: z5.string().uuid().optional().describe("Project ID. Defaults to active project context."),
|
|
4064
|
-
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
|
+
),
|
|
4065
4491
|
change_summary: z5.string().max(500).optional().describe("Optional summary of changes."),
|
|
4066
4492
|
changed_paths: z5.array(z5.string()).optional().describe("Optional changed path list."),
|
|
4067
4493
|
source_url: z5.string().url().optional().describe("Optional source URL for provenance."),
|
|
4068
|
-
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."),
|
|
4069
4500
|
overall_confidence: z5.number().min(0).max(1).optional().describe("Optional overall confidence score in range 0..1."),
|
|
4070
4501
|
extraction_metadata: z5.record(z5.string(), z5.unknown()).optional(),
|
|
4071
4502
|
response_format: z5.enum(["text", "json"]).optional()
|
|
@@ -4105,20 +4536,28 @@ function registerBrandTools(server) {
|
|
|
4105
4536
|
const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
|
|
4106
4537
|
if (!membership) {
|
|
4107
4538
|
return {
|
|
4108
|
-
content: [
|
|
4539
|
+
content: [
|
|
4540
|
+
{
|
|
4541
|
+
type: "text",
|
|
4542
|
+
text: "Project is not accessible to current user."
|
|
4543
|
+
}
|
|
4544
|
+
],
|
|
4109
4545
|
isError: true
|
|
4110
4546
|
};
|
|
4111
4547
|
}
|
|
4112
|
-
const { data: profileId, error } = await supabase.rpc(
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
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
|
+
);
|
|
4122
4561
|
if (error) {
|
|
4123
4562
|
return {
|
|
4124
4563
|
content: [
|
|
@@ -4139,7 +4578,12 @@ function registerBrandTools(server) {
|
|
|
4139
4578
|
};
|
|
4140
4579
|
if ((response_format || "text") === "json") {
|
|
4141
4580
|
return {
|
|
4142
|
-
content: [
|
|
4581
|
+
content: [
|
|
4582
|
+
{
|
|
4583
|
+
type: "text",
|
|
4584
|
+
text: JSON.stringify(asEnvelope4(payload), null, 2)
|
|
4585
|
+
}
|
|
4586
|
+
]
|
|
4143
4587
|
};
|
|
4144
4588
|
}
|
|
4145
4589
|
return {
|
|
@@ -4195,7 +4639,10 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
4195
4639
|
if (!projectId) {
|
|
4196
4640
|
return {
|
|
4197
4641
|
content: [
|
|
4198
|
-
{
|
|
4642
|
+
{
|
|
4643
|
+
type: "text",
|
|
4644
|
+
text: "No project_id provided and no default project found."
|
|
4645
|
+
}
|
|
4199
4646
|
],
|
|
4200
4647
|
isError: true
|
|
4201
4648
|
};
|
|
@@ -4210,7 +4657,12 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
4210
4657
|
const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
|
|
4211
4658
|
if (!membership) {
|
|
4212
4659
|
return {
|
|
4213
|
-
content: [
|
|
4660
|
+
content: [
|
|
4661
|
+
{
|
|
4662
|
+
type: "text",
|
|
4663
|
+
text: "Project is not accessible to current user."
|
|
4664
|
+
}
|
|
4665
|
+
],
|
|
4214
4666
|
isError: true
|
|
4215
4667
|
};
|
|
4216
4668
|
}
|
|
@@ -4226,7 +4678,9 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
4226
4678
|
isError: true
|
|
4227
4679
|
};
|
|
4228
4680
|
}
|
|
4229
|
-
const brandContext = {
|
|
4681
|
+
const brandContext = {
|
|
4682
|
+
...existingProfile.brand_context
|
|
4683
|
+
};
|
|
4230
4684
|
const voiceProfile = brandContext.voiceProfile ?? {};
|
|
4231
4685
|
const platformOverrides = voiceProfile.platformOverrides ?? {};
|
|
4232
4686
|
const existingOverride = platformOverrides[platform2] ?? {};
|
|
@@ -4250,16 +4704,19 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
4250
4704
|
...brandContext,
|
|
4251
4705
|
voiceProfile: updatedVoiceProfile
|
|
4252
4706
|
};
|
|
4253
|
-
const { data: profileId, error: saveError } = await supabase.rpc(
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
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
|
+
);
|
|
4263
4720
|
if (saveError) {
|
|
4264
4721
|
return {
|
|
4265
4722
|
content: [
|
|
@@ -4280,7 +4737,12 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
4280
4737
|
};
|
|
4281
4738
|
if ((response_format || "text") === "json") {
|
|
4282
4739
|
return {
|
|
4283
|
-
content: [
|
|
4740
|
+
content: [
|
|
4741
|
+
{
|
|
4742
|
+
type: "text",
|
|
4743
|
+
text: JSON.stringify(asEnvelope4(payload), null, 2)
|
|
4744
|
+
}
|
|
4745
|
+
],
|
|
4284
4746
|
isError: false
|
|
4285
4747
|
};
|
|
4286
4748
|
}
|
|
@@ -4378,155 +4840,6 @@ async function capturePageScreenshot(page, outputPath, selector) {
|
|
|
4378
4840
|
// src/tools/screenshot.ts
|
|
4379
4841
|
import { resolve, relative } from "node:path";
|
|
4380
4842
|
import { mkdir } from "node:fs/promises";
|
|
4381
|
-
|
|
4382
|
-
// src/lib/ssrf.ts
|
|
4383
|
-
var BLOCKED_IP_PATTERNS = [
|
|
4384
|
-
// IPv4 localhost/loopback
|
|
4385
|
-
/^127\./,
|
|
4386
|
-
/^0\./,
|
|
4387
|
-
// IPv4 private ranges (RFC 1918)
|
|
4388
|
-
/^10\./,
|
|
4389
|
-
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
|
4390
|
-
/^192\.168\./,
|
|
4391
|
-
// IPv4 link-local
|
|
4392
|
-
/^169\.254\./,
|
|
4393
|
-
// Cloud metadata endpoint (AWS, GCP, Azure)
|
|
4394
|
-
/^169\.254\.169\.254$/,
|
|
4395
|
-
// IPv4 broadcast
|
|
4396
|
-
/^255\./,
|
|
4397
|
-
// Shared address space (RFC 6598)
|
|
4398
|
-
/^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./
|
|
4399
|
-
];
|
|
4400
|
-
var BLOCKED_IPV6_PATTERNS = [
|
|
4401
|
-
/^::1$/i,
|
|
4402
|
-
// loopback
|
|
4403
|
-
/^::$/i,
|
|
4404
|
-
// unspecified
|
|
4405
|
-
/^fe[89ab][0-9a-f]:/i,
|
|
4406
|
-
// link-local fe80::/10
|
|
4407
|
-
/^fc[0-9a-f]:/i,
|
|
4408
|
-
// unique local fc00::/7
|
|
4409
|
-
/^fd[0-9a-f]:/i,
|
|
4410
|
-
// unique local fc00::/7
|
|
4411
|
-
/^::ffff:127\./i,
|
|
4412
|
-
// IPv4-mapped localhost
|
|
4413
|
-
/^::ffff:(0|10|127|169\.254|172\.(1[6-9]|2[0-9]|3[0-1])|192\.168)\./i
|
|
4414
|
-
// IPv4-mapped private
|
|
4415
|
-
];
|
|
4416
|
-
var BLOCKED_HOSTNAMES = [
|
|
4417
|
-
"localhost",
|
|
4418
|
-
"localhost.localdomain",
|
|
4419
|
-
"local",
|
|
4420
|
-
"127.0.0.1",
|
|
4421
|
-
"0.0.0.0",
|
|
4422
|
-
"[::1]",
|
|
4423
|
-
"[::ffff:127.0.0.1]",
|
|
4424
|
-
// Cloud metadata endpoints
|
|
4425
|
-
"metadata.google.internal",
|
|
4426
|
-
"metadata.goog",
|
|
4427
|
-
"instance-data",
|
|
4428
|
-
"instance-data.ec2.internal"
|
|
4429
|
-
];
|
|
4430
|
-
var ALLOWED_PROTOCOLS = ["http:", "https:"];
|
|
4431
|
-
var BLOCKED_PORTS = [22, 23, 25, 110, 143, 445, 3306, 5432, 6379, 27017, 11211];
|
|
4432
|
-
function isBlockedIP(ip) {
|
|
4433
|
-
const normalized = ip.replace(/^\[/, "").replace(/\]$/, "");
|
|
4434
|
-
if (normalized.includes(":")) {
|
|
4435
|
-
return BLOCKED_IPV6_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
4436
|
-
}
|
|
4437
|
-
return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
4438
|
-
}
|
|
4439
|
-
function isBlockedHostname(hostname) {
|
|
4440
|
-
return BLOCKED_HOSTNAMES.includes(hostname.toLowerCase());
|
|
4441
|
-
}
|
|
4442
|
-
function isIPAddress(hostname) {
|
|
4443
|
-
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
4444
|
-
const ipv6Pattern = /^\[?[a-fA-F0-9:]+\]?$/;
|
|
4445
|
-
return ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname);
|
|
4446
|
-
}
|
|
4447
|
-
async function validateUrlForSSRF(urlString) {
|
|
4448
|
-
try {
|
|
4449
|
-
const url = new URL(urlString);
|
|
4450
|
-
if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
|
|
4451
|
-
return {
|
|
4452
|
-
isValid: false,
|
|
4453
|
-
error: `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are allowed.`
|
|
4454
|
-
};
|
|
4455
|
-
}
|
|
4456
|
-
if (url.username || url.password) {
|
|
4457
|
-
return {
|
|
4458
|
-
isValid: false,
|
|
4459
|
-
error: "URLs with embedded credentials are not allowed."
|
|
4460
|
-
};
|
|
4461
|
-
}
|
|
4462
|
-
const hostname = url.hostname.toLowerCase();
|
|
4463
|
-
if (isBlockedHostname(hostname)) {
|
|
4464
|
-
return {
|
|
4465
|
-
isValid: false,
|
|
4466
|
-
error: "Access to internal/localhost addresses is not allowed."
|
|
4467
|
-
};
|
|
4468
|
-
}
|
|
4469
|
-
if (isIPAddress(hostname) && isBlockedIP(hostname)) {
|
|
4470
|
-
return {
|
|
4471
|
-
isValid: false,
|
|
4472
|
-
error: "Access to private/internal IP addresses is not allowed."
|
|
4473
|
-
};
|
|
4474
|
-
}
|
|
4475
|
-
const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80;
|
|
4476
|
-
if (BLOCKED_PORTS.includes(port)) {
|
|
4477
|
-
return {
|
|
4478
|
-
isValid: false,
|
|
4479
|
-
error: `Access to port ${port} is not allowed.`
|
|
4480
|
-
};
|
|
4481
|
-
}
|
|
4482
|
-
let resolvedIP;
|
|
4483
|
-
if (!isIPAddress(hostname)) {
|
|
4484
|
-
try {
|
|
4485
|
-
const dns = await import("node:dns");
|
|
4486
|
-
const resolver = new dns.promises.Resolver();
|
|
4487
|
-
const resolvedIPs = [];
|
|
4488
|
-
try {
|
|
4489
|
-
const aRecords = await resolver.resolve4(hostname);
|
|
4490
|
-
resolvedIPs.push(...aRecords);
|
|
4491
|
-
} catch {
|
|
4492
|
-
}
|
|
4493
|
-
try {
|
|
4494
|
-
const aaaaRecords = await resolver.resolve6(hostname);
|
|
4495
|
-
resolvedIPs.push(...aaaaRecords);
|
|
4496
|
-
} catch {
|
|
4497
|
-
}
|
|
4498
|
-
if (resolvedIPs.length === 0) {
|
|
4499
|
-
return {
|
|
4500
|
-
isValid: false,
|
|
4501
|
-
error: "DNS resolution failed: hostname did not resolve to any address."
|
|
4502
|
-
};
|
|
4503
|
-
}
|
|
4504
|
-
for (const ip of resolvedIPs) {
|
|
4505
|
-
if (isBlockedIP(ip)) {
|
|
4506
|
-
return {
|
|
4507
|
-
isValid: false,
|
|
4508
|
-
error: "Hostname resolves to a private/internal IP address."
|
|
4509
|
-
};
|
|
4510
|
-
}
|
|
4511
|
-
}
|
|
4512
|
-
resolvedIP = resolvedIPs[0];
|
|
4513
|
-
} catch {
|
|
4514
|
-
return {
|
|
4515
|
-
isValid: false,
|
|
4516
|
-
error: "DNS resolution failed. Cannot verify hostname safety."
|
|
4517
|
-
};
|
|
4518
|
-
}
|
|
4519
|
-
}
|
|
4520
|
-
return { isValid: true, sanitizedUrl: url.toString(), resolvedIP };
|
|
4521
|
-
} catch (error) {
|
|
4522
|
-
return {
|
|
4523
|
-
isValid: false,
|
|
4524
|
-
error: `Invalid URL format: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
4525
|
-
};
|
|
4526
|
-
}
|
|
4527
|
-
}
|
|
4528
|
-
|
|
4529
|
-
// src/tools/screenshot.ts
|
|
4530
4843
|
init_supabase();
|
|
4531
4844
|
function registerScreenshotTools(server) {
|
|
4532
4845
|
server.tool(
|
|
@@ -5102,6 +5415,7 @@ function registerRemotionTools(server) {
|
|
|
5102
5415
|
// src/tools/insights.ts
|
|
5103
5416
|
init_supabase();
|
|
5104
5417
|
import { z as z8 } from "zod";
|
|
5418
|
+
var MAX_INSIGHT_AGE_DAYS = 30;
|
|
5105
5419
|
var PLATFORM_ENUM = [
|
|
5106
5420
|
"youtube",
|
|
5107
5421
|
"tiktok",
|
|
@@ -5112,11 +5426,19 @@ var PLATFORM_ENUM = [
|
|
|
5112
5426
|
"threads",
|
|
5113
5427
|
"bluesky"
|
|
5114
5428
|
];
|
|
5115
|
-
var DAY_NAMES = [
|
|
5429
|
+
var DAY_NAMES = [
|
|
5430
|
+
"Sunday",
|
|
5431
|
+
"Monday",
|
|
5432
|
+
"Tuesday",
|
|
5433
|
+
"Wednesday",
|
|
5434
|
+
"Thursday",
|
|
5435
|
+
"Friday",
|
|
5436
|
+
"Saturday"
|
|
5437
|
+
];
|
|
5116
5438
|
function asEnvelope5(data) {
|
|
5117
5439
|
return {
|
|
5118
5440
|
_meta: {
|
|
5119
|
-
version:
|
|
5441
|
+
version: MCP_VERSION,
|
|
5120
5442
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5121
5443
|
},
|
|
5122
5444
|
data
|
|
@@ -5127,7 +5449,12 @@ function registerInsightsTools(server) {
|
|
|
5127
5449
|
"get_performance_insights",
|
|
5128
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.",
|
|
5129
5451
|
{
|
|
5130
|
-
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."),
|
|
5131
5458
|
days: z8.number().min(1).max(90).optional().describe("Number of days to look back. Defaults to 30. Max 90."),
|
|
5132
5459
|
limit: z8.number().min(1).max(50).optional().describe("Maximum number of insights to return. Defaults to 10."),
|
|
5133
5460
|
response_format: z8.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
@@ -5146,10 +5473,13 @@ function registerInsightsTools(server) {
|
|
|
5146
5473
|
projectIds.push(...projects.map((p) => p.id));
|
|
5147
5474
|
}
|
|
5148
5475
|
}
|
|
5476
|
+
const effectiveDays = Math.min(lookbackDays, MAX_INSIGHT_AGE_DAYS);
|
|
5149
5477
|
const since = /* @__PURE__ */ new Date();
|
|
5150
|
-
since.setDate(since.getDate() -
|
|
5478
|
+
since.setDate(since.getDate() - effectiveDays);
|
|
5151
5479
|
const sinceIso = since.toISOString();
|
|
5152
|
-
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);
|
|
5153
5483
|
if (projectIds.length > 0) {
|
|
5154
5484
|
query = query.in("project_id", projectIds);
|
|
5155
5485
|
} else {
|
|
@@ -5506,7 +5836,7 @@ init_supabase();
|
|
|
5506
5836
|
function asEnvelope6(data) {
|
|
5507
5837
|
return {
|
|
5508
5838
|
_meta: {
|
|
5509
|
-
version:
|
|
5839
|
+
version: MCP_VERSION,
|
|
5510
5840
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5511
5841
|
},
|
|
5512
5842
|
data
|
|
@@ -5517,7 +5847,9 @@ function registerCommentsTools(server) {
|
|
|
5517
5847
|
"list_comments",
|
|
5518
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.",
|
|
5519
5849
|
{
|
|
5520
|
-
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
|
+
),
|
|
5521
5853
|
max_results: z10.number().min(1).max(100).optional().describe("Maximum number of comments to return. Defaults to 50."),
|
|
5522
5854
|
page_token: z10.string().optional().describe("Pagination token from a previous list_comments call."),
|
|
5523
5855
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
@@ -5532,7 +5864,9 @@ function registerCommentsTools(server) {
|
|
|
5532
5864
|
});
|
|
5533
5865
|
if (error) {
|
|
5534
5866
|
return {
|
|
5535
|
-
content: [
|
|
5867
|
+
content: [
|
|
5868
|
+
{ type: "text", text: `Error listing comments: ${error}` }
|
|
5869
|
+
],
|
|
5536
5870
|
isError: true
|
|
5537
5871
|
};
|
|
5538
5872
|
}
|
|
@@ -5544,7 +5878,10 @@ function registerCommentsTools(server) {
|
|
|
5544
5878
|
{
|
|
5545
5879
|
type: "text",
|
|
5546
5880
|
text: JSON.stringify(
|
|
5547
|
-
asEnvelope6({
|
|
5881
|
+
asEnvelope6({
|
|
5882
|
+
comments,
|
|
5883
|
+
nextPageToken: result.nextPageToken ?? null
|
|
5884
|
+
}),
|
|
5548
5885
|
null,
|
|
5549
5886
|
2
|
|
5550
5887
|
)
|
|
@@ -5581,7 +5918,9 @@ function registerCommentsTools(server) {
|
|
|
5581
5918
|
"reply_to_comment",
|
|
5582
5919
|
"Reply to a YouTube comment. Requires the parent comment ID and reply text.",
|
|
5583
5920
|
{
|
|
5584
|
-
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
|
+
),
|
|
5585
5924
|
text: z10.string().min(1).describe("The reply text."),
|
|
5586
5925
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
5587
5926
|
},
|
|
@@ -5620,7 +5959,12 @@ function registerCommentsTools(server) {
|
|
|
5620
5959
|
details: { error }
|
|
5621
5960
|
});
|
|
5622
5961
|
return {
|
|
5623
|
-
content: [
|
|
5962
|
+
content: [
|
|
5963
|
+
{
|
|
5964
|
+
type: "text",
|
|
5965
|
+
text: `Error replying to comment: ${error}`
|
|
5966
|
+
}
|
|
5967
|
+
],
|
|
5624
5968
|
isError: true
|
|
5625
5969
|
};
|
|
5626
5970
|
}
|
|
@@ -5633,7 +5977,12 @@ function registerCommentsTools(server) {
|
|
|
5633
5977
|
});
|
|
5634
5978
|
if (format === "json") {
|
|
5635
5979
|
return {
|
|
5636
|
-
content: [
|
|
5980
|
+
content: [
|
|
5981
|
+
{
|
|
5982
|
+
type: "text",
|
|
5983
|
+
text: JSON.stringify(asEnvelope6(result), null, 2)
|
|
5984
|
+
}
|
|
5985
|
+
]
|
|
5637
5986
|
};
|
|
5638
5987
|
}
|
|
5639
5988
|
return {
|
|
@@ -5691,7 +6040,9 @@ function registerCommentsTools(server) {
|
|
|
5691
6040
|
details: { error }
|
|
5692
6041
|
});
|
|
5693
6042
|
return {
|
|
5694
|
-
content: [
|
|
6043
|
+
content: [
|
|
6044
|
+
{ type: "text", text: `Error posting comment: ${error}` }
|
|
6045
|
+
],
|
|
5695
6046
|
isError: true
|
|
5696
6047
|
};
|
|
5697
6048
|
}
|
|
@@ -5704,7 +6055,12 @@ function registerCommentsTools(server) {
|
|
|
5704
6055
|
});
|
|
5705
6056
|
if (format === "json") {
|
|
5706
6057
|
return {
|
|
5707
|
-
content: [
|
|
6058
|
+
content: [
|
|
6059
|
+
{
|
|
6060
|
+
type: "text",
|
|
6061
|
+
text: JSON.stringify(asEnvelope6(result), null, 2)
|
|
6062
|
+
}
|
|
6063
|
+
]
|
|
5708
6064
|
};
|
|
5709
6065
|
}
|
|
5710
6066
|
return {
|
|
@@ -5762,7 +6118,12 @@ function registerCommentsTools(server) {
|
|
|
5762
6118
|
details: { error }
|
|
5763
6119
|
});
|
|
5764
6120
|
return {
|
|
5765
|
-
content: [
|
|
6121
|
+
content: [
|
|
6122
|
+
{
|
|
6123
|
+
type: "text",
|
|
6124
|
+
text: `Error moderating comment: ${error}`
|
|
6125
|
+
}
|
|
6126
|
+
],
|
|
5766
6127
|
isError: true
|
|
5767
6128
|
};
|
|
5768
6129
|
}
|
|
@@ -5841,7 +6202,9 @@ function registerCommentsTools(server) {
|
|
|
5841
6202
|
details: { error }
|
|
5842
6203
|
});
|
|
5843
6204
|
return {
|
|
5844
|
-
content: [
|
|
6205
|
+
content: [
|
|
6206
|
+
{ type: "text", text: `Error deleting comment: ${error}` }
|
|
6207
|
+
],
|
|
5845
6208
|
isError: true
|
|
5846
6209
|
};
|
|
5847
6210
|
}
|
|
@@ -5856,13 +6219,22 @@ function registerCommentsTools(server) {
|
|
|
5856
6219
|
content: [
|
|
5857
6220
|
{
|
|
5858
6221
|
type: "text",
|
|
5859
|
-
text: JSON.stringify(
|
|
6222
|
+
text: JSON.stringify(
|
|
6223
|
+
asEnvelope6({ success: true, commentId: comment_id }),
|
|
6224
|
+
null,
|
|
6225
|
+
2
|
|
6226
|
+
)
|
|
5860
6227
|
}
|
|
5861
6228
|
]
|
|
5862
6229
|
};
|
|
5863
6230
|
}
|
|
5864
6231
|
return {
|
|
5865
|
-
content: [
|
|
6232
|
+
content: [
|
|
6233
|
+
{
|
|
6234
|
+
type: "text",
|
|
6235
|
+
text: `Comment ${comment_id} deleted successfully.`
|
|
6236
|
+
}
|
|
6237
|
+
]
|
|
5866
6238
|
};
|
|
5867
6239
|
}
|
|
5868
6240
|
);
|
|
@@ -5890,8 +6262,12 @@ function transformInsightsToPerformanceContext(projectId, insights) {
|
|
|
5890
6262
|
};
|
|
5891
6263
|
}
|
|
5892
6264
|
const topHooksInsight = insights.find((i) => i.insight_type === "top_hooks");
|
|
5893
|
-
const optimalTimingInsight = insights.find(
|
|
5894
|
-
|
|
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
|
+
);
|
|
5895
6271
|
const topHooks = topHooksInsight?.insight_data?.hooks || [];
|
|
5896
6272
|
const hooksSummary = topHooksInsight?.insight_data?.summary || "";
|
|
5897
6273
|
const timingSummary = optimalTimingInsight?.insight_data?.summary || "";
|
|
@@ -5902,7 +6278,10 @@ function transformInsightsToPerformanceContext(projectId, insights) {
|
|
|
5902
6278
|
if (hooksSummary) promptParts.push(hooksSummary);
|
|
5903
6279
|
if (timingSummary) promptParts.push(timingSummary);
|
|
5904
6280
|
if (modelSummary) promptParts.push(modelSummary);
|
|
5905
|
-
if (topHooks.length)
|
|
6281
|
+
if (topHooks.length)
|
|
6282
|
+
promptParts.push(
|
|
6283
|
+
`Top performing hooks: ${topHooks.slice(0, 3).join(", ")}`
|
|
6284
|
+
);
|
|
5906
6285
|
return {
|
|
5907
6286
|
projectId,
|
|
5908
6287
|
hasHistoricalData: true,
|
|
@@ -5928,7 +6307,7 @@ function transformInsightsToPerformanceContext(projectId, insights) {
|
|
|
5928
6307
|
function asEnvelope7(data) {
|
|
5929
6308
|
return {
|
|
5930
6309
|
_meta: {
|
|
5931
|
-
version:
|
|
6310
|
+
version: MCP_VERSION,
|
|
5932
6311
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5933
6312
|
},
|
|
5934
6313
|
data
|
|
@@ -5951,7 +6330,12 @@ function registerIdeationContextTools(server) {
|
|
|
5951
6330
|
const { data: member } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).limit(1).single();
|
|
5952
6331
|
if (!member?.organization_id) {
|
|
5953
6332
|
return {
|
|
5954
|
-
content: [
|
|
6333
|
+
content: [
|
|
6334
|
+
{
|
|
6335
|
+
type: "text",
|
|
6336
|
+
text: "No organization found for current user."
|
|
6337
|
+
}
|
|
6338
|
+
],
|
|
5955
6339
|
isError: true
|
|
5956
6340
|
};
|
|
5957
6341
|
}
|
|
@@ -5959,7 +6343,10 @@ function registerIdeationContextTools(server) {
|
|
|
5959
6343
|
if (projectsError) {
|
|
5960
6344
|
return {
|
|
5961
6345
|
content: [
|
|
5962
|
-
{
|
|
6346
|
+
{
|
|
6347
|
+
type: "text",
|
|
6348
|
+
text: `Failed to resolve projects: ${projectsError.message}`
|
|
6349
|
+
}
|
|
5963
6350
|
],
|
|
5964
6351
|
isError: true
|
|
5965
6352
|
};
|
|
@@ -5967,7 +6354,12 @@ function registerIdeationContextTools(server) {
|
|
|
5967
6354
|
const projectIds = (projects || []).map((p) => p.id);
|
|
5968
6355
|
if (projectIds.length === 0) {
|
|
5969
6356
|
return {
|
|
5970
|
-
content: [
|
|
6357
|
+
content: [
|
|
6358
|
+
{
|
|
6359
|
+
type: "text",
|
|
6360
|
+
text: "No projects found for current user."
|
|
6361
|
+
}
|
|
6362
|
+
],
|
|
5971
6363
|
isError: true
|
|
5972
6364
|
};
|
|
5973
6365
|
}
|
|
@@ -5976,7 +6368,10 @@ function registerIdeationContextTools(server) {
|
|
|
5976
6368
|
if (!selectedProjectId) {
|
|
5977
6369
|
return {
|
|
5978
6370
|
content: [
|
|
5979
|
-
{
|
|
6371
|
+
{
|
|
6372
|
+
type: "text",
|
|
6373
|
+
text: "No accessible project found for current user."
|
|
6374
|
+
}
|
|
5980
6375
|
],
|
|
5981
6376
|
isError: true
|
|
5982
6377
|
};
|
|
@@ -5994,7 +6389,9 @@ function registerIdeationContextTools(server) {
|
|
|
5994
6389
|
}
|
|
5995
6390
|
const since = /* @__PURE__ */ new Date();
|
|
5996
6391
|
since.setDate(since.getDate() - lookbackDays);
|
|
5997
|
-
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);
|
|
5998
6395
|
if (error) {
|
|
5999
6396
|
return {
|
|
6000
6397
|
content: [
|
|
@@ -6012,7 +6409,12 @@ function registerIdeationContextTools(server) {
|
|
|
6012
6409
|
);
|
|
6013
6410
|
if (format === "json") {
|
|
6014
6411
|
return {
|
|
6015
|
-
content: [
|
|
6412
|
+
content: [
|
|
6413
|
+
{
|
|
6414
|
+
type: "text",
|
|
6415
|
+
text: JSON.stringify(asEnvelope7(context), null, 2)
|
|
6416
|
+
}
|
|
6417
|
+
]
|
|
6016
6418
|
};
|
|
6017
6419
|
}
|
|
6018
6420
|
const lines = [
|
|
@@ -6036,7 +6438,7 @@ import { z as z12 } from "zod";
|
|
|
6036
6438
|
function asEnvelope8(data) {
|
|
6037
6439
|
return {
|
|
6038
6440
|
_meta: {
|
|
6039
|
-
version:
|
|
6441
|
+
version: MCP_VERSION,
|
|
6040
6442
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6041
6443
|
},
|
|
6042
6444
|
data
|
|
@@ -6075,7 +6477,12 @@ function registerCreditsTools(server) {
|
|
|
6075
6477
|
};
|
|
6076
6478
|
if ((response_format || "text") === "json") {
|
|
6077
6479
|
return {
|
|
6078
|
-
content: [
|
|
6480
|
+
content: [
|
|
6481
|
+
{
|
|
6482
|
+
type: "text",
|
|
6483
|
+
text: JSON.stringify(asEnvelope8(payload), null, 2)
|
|
6484
|
+
}
|
|
6485
|
+
]
|
|
6079
6486
|
};
|
|
6080
6487
|
}
|
|
6081
6488
|
return {
|
|
@@ -6109,7 +6516,12 @@ Monthly used: ${payload.monthlyUsed}` + (payload.monthlyLimit ? ` / ${payload.mo
|
|
|
6109
6516
|
};
|
|
6110
6517
|
if ((response_format || "text") === "json") {
|
|
6111
6518
|
return {
|
|
6112
|
-
content: [
|
|
6519
|
+
content: [
|
|
6520
|
+
{
|
|
6521
|
+
type: "text",
|
|
6522
|
+
text: JSON.stringify(asEnvelope8(payload), null, 2)
|
|
6523
|
+
}
|
|
6524
|
+
]
|
|
6113
6525
|
};
|
|
6114
6526
|
}
|
|
6115
6527
|
return {
|
|
@@ -6136,7 +6548,7 @@ import { z as z13 } from "zod";
|
|
|
6136
6548
|
function asEnvelope9(data) {
|
|
6137
6549
|
return {
|
|
6138
6550
|
_meta: {
|
|
6139
|
-
version:
|
|
6551
|
+
version: MCP_VERSION,
|
|
6140
6552
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6141
6553
|
},
|
|
6142
6554
|
data
|
|
@@ -6175,7 +6587,12 @@ function registerLoopSummaryTools(server) {
|
|
|
6175
6587
|
const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
|
|
6176
6588
|
if (!membership) {
|
|
6177
6589
|
return {
|
|
6178
|
-
content: [
|
|
6590
|
+
content: [
|
|
6591
|
+
{
|
|
6592
|
+
type: "text",
|
|
6593
|
+
text: "Project is not accessible to current user."
|
|
6594
|
+
}
|
|
6595
|
+
],
|
|
6179
6596
|
isError: true
|
|
6180
6597
|
};
|
|
6181
6598
|
}
|
|
@@ -6198,7 +6615,12 @@ function registerLoopSummaryTools(server) {
|
|
|
6198
6615
|
};
|
|
6199
6616
|
if ((response_format || "text") === "json") {
|
|
6200
6617
|
return {
|
|
6201
|
-
content: [
|
|
6618
|
+
content: [
|
|
6619
|
+
{
|
|
6620
|
+
type: "text",
|
|
6621
|
+
text: JSON.stringify(asEnvelope9(payload), null, 2)
|
|
6622
|
+
}
|
|
6623
|
+
]
|
|
6202
6624
|
};
|
|
6203
6625
|
}
|
|
6204
6626
|
return {
|
|
@@ -6221,11 +6643,6 @@ Next Action: ${payload.recommendedNextAction}`
|
|
|
6221
6643
|
// src/tools/usage.ts
|
|
6222
6644
|
init_supabase();
|
|
6223
6645
|
import { z as z14 } from "zod";
|
|
6224
|
-
|
|
6225
|
-
// src/lib/version.ts
|
|
6226
|
-
var MCP_VERSION = "1.4.0";
|
|
6227
|
-
|
|
6228
|
-
// src/tools/usage.ts
|
|
6229
6646
|
function asEnvelope10(data) {
|
|
6230
6647
|
return {
|
|
6231
6648
|
_meta: {
|
|
@@ -6531,7 +6948,10 @@ ${"=".repeat(40)}
|
|
|
6531
6948
|
import { z as z16 } from "zod";
|
|
6532
6949
|
init_supabase();
|
|
6533
6950
|
function asEnvelope12(data) {
|
|
6534
|
-
return {
|
|
6951
|
+
return {
|
|
6952
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
6953
|
+
data
|
|
6954
|
+
};
|
|
6535
6955
|
}
|
|
6536
6956
|
function isYouTubeUrl(url) {
|
|
6537
6957
|
if (/youtube\.com\/watch|youtu\.be\//.test(url)) return "video";
|
|
@@ -6562,13 +6982,17 @@ Metadata:`);
|
|
|
6562
6982
|
if (m.tags?.length) lines.push(` Tags: ${m.tags.join(", ")}`);
|
|
6563
6983
|
}
|
|
6564
6984
|
if (content.features?.length)
|
|
6565
|
-
lines.push(
|
|
6985
|
+
lines.push(
|
|
6986
|
+
`
|
|
6566
6987
|
Features:
|
|
6567
|
-
${content.features.map((f) => ` - ${f}`).join("\n")}`
|
|
6988
|
+
${content.features.map((f) => ` - ${f}`).join("\n")}`
|
|
6989
|
+
);
|
|
6568
6990
|
if (content.benefits?.length)
|
|
6569
|
-
lines.push(
|
|
6991
|
+
lines.push(
|
|
6992
|
+
`
|
|
6570
6993
|
Benefits:
|
|
6571
|
-
${content.benefits.map((b) => ` - ${b}`).join("\n")}`
|
|
6994
|
+
${content.benefits.map((b) => ` - ${b}`).join("\n")}`
|
|
6995
|
+
);
|
|
6572
6996
|
if (content.usp) lines.push(`
|
|
6573
6997
|
USP: ${content.usp}`);
|
|
6574
6998
|
if (content.suggested_hooks?.length)
|
|
@@ -6590,12 +7014,20 @@ function registerExtractionTools(server) {
|
|
|
6590
7014
|
max_results: z16.number().min(1).max(100).default(10).describe("Max comments to include"),
|
|
6591
7015
|
response_format: z16.enum(["text", "json"]).default("text")
|
|
6592
7016
|
},
|
|
6593
|
-
async ({
|
|
7017
|
+
async ({
|
|
7018
|
+
url,
|
|
7019
|
+
extract_type,
|
|
7020
|
+
include_comments,
|
|
7021
|
+
max_results,
|
|
7022
|
+
response_format
|
|
7023
|
+
}) => {
|
|
6594
7024
|
const startedAt = Date.now();
|
|
6595
7025
|
const ssrfCheck = await validateUrlForSSRF(url);
|
|
6596
7026
|
if (!ssrfCheck.isValid) {
|
|
6597
7027
|
return {
|
|
6598
|
-
content: [
|
|
7028
|
+
content: [
|
|
7029
|
+
{ type: "text", text: `URL blocked: ${ssrfCheck.error}` }
|
|
7030
|
+
],
|
|
6599
7031
|
isError: true
|
|
6600
7032
|
};
|
|
6601
7033
|
}
|
|
@@ -6723,13 +7155,21 @@ function registerExtractionTools(server) {
|
|
|
6723
7155
|
if (response_format === "json") {
|
|
6724
7156
|
return {
|
|
6725
7157
|
content: [
|
|
6726
|
-
{
|
|
7158
|
+
{
|
|
7159
|
+
type: "text",
|
|
7160
|
+
text: JSON.stringify(asEnvelope12(extracted), null, 2)
|
|
7161
|
+
}
|
|
6727
7162
|
],
|
|
6728
7163
|
isError: false
|
|
6729
7164
|
};
|
|
6730
7165
|
}
|
|
6731
7166
|
return {
|
|
6732
|
-
content: [
|
|
7167
|
+
content: [
|
|
7168
|
+
{
|
|
7169
|
+
type: "text",
|
|
7170
|
+
text: formatExtractedContentAsText(extracted)
|
|
7171
|
+
}
|
|
7172
|
+
],
|
|
6733
7173
|
isError: false
|
|
6734
7174
|
};
|
|
6735
7175
|
} catch (err) {
|
|
@@ -6742,7 +7182,9 @@ function registerExtractionTools(server) {
|
|
|
6742
7182
|
details: { url, error: message }
|
|
6743
7183
|
});
|
|
6744
7184
|
return {
|
|
6745
|
-
content: [
|
|
7185
|
+
content: [
|
|
7186
|
+
{ type: "text", text: `Extraction failed: ${message}` }
|
|
7187
|
+
],
|
|
6746
7188
|
isError: true
|
|
6747
7189
|
};
|
|
6748
7190
|
}
|
|
@@ -6754,7 +7196,10 @@ function registerExtractionTools(server) {
|
|
|
6754
7196
|
import { z as z17 } from "zod";
|
|
6755
7197
|
init_supabase();
|
|
6756
7198
|
function asEnvelope13(data) {
|
|
6757
|
-
return {
|
|
7199
|
+
return {
|
|
7200
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
7201
|
+
data
|
|
7202
|
+
};
|
|
6758
7203
|
}
|
|
6759
7204
|
function registerQualityTools(server) {
|
|
6760
7205
|
server.tool(
|
|
@@ -6810,7 +7255,12 @@ function registerQualityTools(server) {
|
|
|
6810
7255
|
});
|
|
6811
7256
|
if (response_format === "json") {
|
|
6812
7257
|
return {
|
|
6813
|
-
content: [
|
|
7258
|
+
content: [
|
|
7259
|
+
{
|
|
7260
|
+
type: "text",
|
|
7261
|
+
text: JSON.stringify(asEnvelope13(result), null, 2)
|
|
7262
|
+
}
|
|
7263
|
+
],
|
|
6814
7264
|
isError: false
|
|
6815
7265
|
};
|
|
6816
7266
|
}
|
|
@@ -6820,7 +7270,9 @@ function registerQualityTools(server) {
|
|
|
6820
7270
|
);
|
|
6821
7271
|
lines.push("");
|
|
6822
7272
|
for (const cat of result.categories) {
|
|
6823
|
-
lines.push(
|
|
7273
|
+
lines.push(
|
|
7274
|
+
` ${cat.name}: ${cat.score}/${cat.maxScore} \u2014 ${cat.detail}`
|
|
7275
|
+
);
|
|
6824
7276
|
}
|
|
6825
7277
|
if (result.blockers.length > 0) {
|
|
6826
7278
|
lines.push("");
|
|
@@ -6831,7 +7283,10 @@ function registerQualityTools(server) {
|
|
|
6831
7283
|
}
|
|
6832
7284
|
lines.push("");
|
|
6833
7285
|
lines.push(`Threshold: ${result.threshold}/${result.maxTotal}`);
|
|
6834
|
-
return {
|
|
7286
|
+
return {
|
|
7287
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
7288
|
+
isError: false
|
|
7289
|
+
};
|
|
6835
7290
|
}
|
|
6836
7291
|
);
|
|
6837
7292
|
server.tool(
|
|
@@ -6872,7 +7327,9 @@ function registerQualityTools(server) {
|
|
|
6872
7327
|
});
|
|
6873
7328
|
const scores = postsWithQuality.map((p) => p.quality.score);
|
|
6874
7329
|
const passed = postsWithQuality.filter((p) => p.quality.passed).length;
|
|
6875
|
-
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;
|
|
6876
7333
|
const summary = {
|
|
6877
7334
|
total_posts: plan.posts.length,
|
|
6878
7335
|
passed,
|
|
@@ -6891,25 +7348,36 @@ function registerQualityTools(server) {
|
|
|
6891
7348
|
content: [
|
|
6892
7349
|
{
|
|
6893
7350
|
type: "text",
|
|
6894
|
-
text: JSON.stringify(
|
|
7351
|
+
text: JSON.stringify(
|
|
7352
|
+
asEnvelope13({ posts: postsWithQuality, summary }),
|
|
7353
|
+
null,
|
|
7354
|
+
2
|
|
7355
|
+
)
|
|
6895
7356
|
}
|
|
6896
7357
|
],
|
|
6897
7358
|
isError: false
|
|
6898
7359
|
};
|
|
6899
7360
|
}
|
|
6900
7361
|
const lines = [];
|
|
6901
|
-
lines.push(
|
|
7362
|
+
lines.push(
|
|
7363
|
+
`PLAN QUALITY: ${passed}/${plan.posts.length} passed (avg: ${avgScore}/35)`
|
|
7364
|
+
);
|
|
6902
7365
|
lines.push("");
|
|
6903
7366
|
for (const post of postsWithQuality) {
|
|
6904
7367
|
const icon = post.quality.passed ? "[PASS]" : "[FAIL]";
|
|
6905
|
-
lines.push(
|
|
7368
|
+
lines.push(
|
|
7369
|
+
`${icon} ${post.id} | ${post.platform} | ${post.quality.score}/35`
|
|
7370
|
+
);
|
|
6906
7371
|
if (post.quality.blockers.length > 0) {
|
|
6907
7372
|
for (const b of post.quality.blockers) {
|
|
6908
7373
|
lines.push(` - ${b}`);
|
|
6909
7374
|
}
|
|
6910
7375
|
}
|
|
6911
7376
|
}
|
|
6912
|
-
return {
|
|
7377
|
+
return {
|
|
7378
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
7379
|
+
isError: false
|
|
7380
|
+
};
|
|
6913
7381
|
}
|
|
6914
7382
|
);
|
|
6915
7383
|
}
|
|
@@ -6922,7 +7390,10 @@ function toRecord(value) {
|
|
|
6922
7390
|
return value && typeof value === "object" ? value : void 0;
|
|
6923
7391
|
}
|
|
6924
7392
|
function asEnvelope14(data) {
|
|
6925
|
-
return {
|
|
7393
|
+
return {
|
|
7394
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
7395
|
+
data
|
|
7396
|
+
};
|
|
6926
7397
|
}
|
|
6927
7398
|
function tomorrowIsoDate() {
|
|
6928
7399
|
const d = /* @__PURE__ */ new Date();
|
|
@@ -6960,7 +7431,9 @@ function formatPlanAsText(plan) {
|
|
|
6960
7431
|
lines.push(`WEEKLY CONTENT PLAN: "${plan.topic}"`);
|
|
6961
7432
|
lines.push(`Period: ${plan.start_date} to ${plan.end_date}`);
|
|
6962
7433
|
lines.push(`Platforms: ${plan.platforms.join(", ")}`);
|
|
6963
|
-
lines.push(
|
|
7434
|
+
lines.push(
|
|
7435
|
+
`Posts: ${plan.posts.length} | Estimated credits: ~${plan.estimated_credits}`
|
|
7436
|
+
);
|
|
6964
7437
|
if (plan.plan_id) lines.push(`Plan ID: ${plan.plan_id}`);
|
|
6965
7438
|
if (plan.insights_applied?.has_historical_data) {
|
|
6966
7439
|
lines.push("");
|
|
@@ -6975,7 +7448,9 @@ function formatPlanAsText(plan) {
|
|
|
6975
7448
|
`- Best posting time: ${days[timing.dayOfWeek] ?? timing.dayOfWeek} ${timing.hourOfDay}:00`
|
|
6976
7449
|
);
|
|
6977
7450
|
}
|
|
6978
|
-
lines.push(
|
|
7451
|
+
lines.push(
|
|
7452
|
+
`- Recommended model: ${plan.insights_applied.recommended_model ?? "N/A"}`
|
|
7453
|
+
);
|
|
6979
7454
|
lines.push(`- Insights count: ${plan.insights_applied.insights_count}`);
|
|
6980
7455
|
}
|
|
6981
7456
|
lines.push("");
|
|
@@ -6996,9 +7471,11 @@ function formatPlanAsText(plan) {
|
|
|
6996
7471
|
` Caption: ${post.caption.slice(0, 200)}${post.caption.length > 200 ? "..." : ""}`
|
|
6997
7472
|
);
|
|
6998
7473
|
if (post.title) lines.push(` Title: ${post.title}`);
|
|
6999
|
-
if (post.visual_direction)
|
|
7474
|
+
if (post.visual_direction)
|
|
7475
|
+
lines.push(` Visual: ${post.visual_direction}`);
|
|
7000
7476
|
if (post.media_type) lines.push(` Media: ${post.media_type}`);
|
|
7001
|
-
if (post.hashtags?.length)
|
|
7477
|
+
if (post.hashtags?.length)
|
|
7478
|
+
lines.push(` Hashtags: ${post.hashtags.join(" ")}`);
|
|
7002
7479
|
lines.push("");
|
|
7003
7480
|
}
|
|
7004
7481
|
}
|
|
@@ -7081,7 +7558,10 @@ function registerPlanningTools(server) {
|
|
|
7081
7558
|
"mcp-data",
|
|
7082
7559
|
{
|
|
7083
7560
|
action: "brand-profile",
|
|
7084
|
-
...resolvedProjectId ? {
|
|
7561
|
+
...resolvedProjectId ? {
|
|
7562
|
+
projectId: resolvedProjectId,
|
|
7563
|
+
project_id: resolvedProjectId
|
|
7564
|
+
} : {}
|
|
7085
7565
|
},
|
|
7086
7566
|
{ timeoutMs: 15e3 }
|
|
7087
7567
|
);
|
|
@@ -7285,7 +7765,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7285
7765
|
details: { topic, error: `plan persistence failed: ${message}` }
|
|
7286
7766
|
});
|
|
7287
7767
|
return {
|
|
7288
|
-
content: [
|
|
7768
|
+
content: [
|
|
7769
|
+
{
|
|
7770
|
+
type: "text",
|
|
7771
|
+
text: `Plan persistence failed: ${message}`
|
|
7772
|
+
}
|
|
7773
|
+
],
|
|
7289
7774
|
isError: true
|
|
7290
7775
|
};
|
|
7291
7776
|
}
|
|
@@ -7299,7 +7784,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7299
7784
|
});
|
|
7300
7785
|
if (response_format === "json") {
|
|
7301
7786
|
return {
|
|
7302
|
-
content: [
|
|
7787
|
+
content: [
|
|
7788
|
+
{
|
|
7789
|
+
type: "text",
|
|
7790
|
+
text: JSON.stringify(asEnvelope14(plan), null, 2)
|
|
7791
|
+
}
|
|
7792
|
+
],
|
|
7303
7793
|
isError: false
|
|
7304
7794
|
};
|
|
7305
7795
|
}
|
|
@@ -7317,7 +7807,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7317
7807
|
details: { topic, error: message }
|
|
7318
7808
|
});
|
|
7319
7809
|
return {
|
|
7320
|
-
content: [
|
|
7810
|
+
content: [
|
|
7811
|
+
{
|
|
7812
|
+
type: "text",
|
|
7813
|
+
text: `Plan generation failed: ${message}`
|
|
7814
|
+
}
|
|
7815
|
+
],
|
|
7321
7816
|
isError: true
|
|
7322
7817
|
};
|
|
7323
7818
|
}
|
|
@@ -7377,7 +7872,11 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7377
7872
|
toolName: "save_content_plan",
|
|
7378
7873
|
status: "success",
|
|
7379
7874
|
durationMs,
|
|
7380
|
-
details: {
|
|
7875
|
+
details: {
|
|
7876
|
+
plan_id: planId,
|
|
7877
|
+
project_id: resolvedProjectId,
|
|
7878
|
+
status: normalizedStatus
|
|
7879
|
+
}
|
|
7381
7880
|
});
|
|
7382
7881
|
const result = {
|
|
7383
7882
|
plan_id: planId,
|
|
@@ -7386,13 +7885,21 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7386
7885
|
};
|
|
7387
7886
|
if (response_format === "json") {
|
|
7388
7887
|
return {
|
|
7389
|
-
content: [
|
|
7888
|
+
content: [
|
|
7889
|
+
{
|
|
7890
|
+
type: "text",
|
|
7891
|
+
text: JSON.stringify(asEnvelope14(result), null, 2)
|
|
7892
|
+
}
|
|
7893
|
+
],
|
|
7390
7894
|
isError: false
|
|
7391
7895
|
};
|
|
7392
7896
|
}
|
|
7393
7897
|
return {
|
|
7394
7898
|
content: [
|
|
7395
|
-
{
|
|
7899
|
+
{
|
|
7900
|
+
type: "text",
|
|
7901
|
+
text: `Saved content plan ${planId} (${normalizedStatus}).`
|
|
7902
|
+
}
|
|
7396
7903
|
],
|
|
7397
7904
|
isError: false
|
|
7398
7905
|
};
|
|
@@ -7406,7 +7913,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7406
7913
|
details: { error: message }
|
|
7407
7914
|
});
|
|
7408
7915
|
return {
|
|
7409
|
-
content: [
|
|
7916
|
+
content: [
|
|
7917
|
+
{
|
|
7918
|
+
type: "text",
|
|
7919
|
+
text: `Failed to save content plan: ${message}`
|
|
7920
|
+
}
|
|
7921
|
+
],
|
|
7410
7922
|
isError: true
|
|
7411
7923
|
};
|
|
7412
7924
|
}
|
|
@@ -7422,7 +7934,9 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7422
7934
|
async ({ plan_id, response_format }) => {
|
|
7423
7935
|
const supabase = getSupabaseClient();
|
|
7424
7936
|
const userId = await getDefaultUserId();
|
|
7425
|
-
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();
|
|
7426
7940
|
if (error) {
|
|
7427
7941
|
return {
|
|
7428
7942
|
content: [
|
|
@@ -7437,7 +7951,10 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7437
7951
|
if (!data) {
|
|
7438
7952
|
return {
|
|
7439
7953
|
content: [
|
|
7440
|
-
{
|
|
7954
|
+
{
|
|
7955
|
+
type: "text",
|
|
7956
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
7957
|
+
}
|
|
7441
7958
|
],
|
|
7442
7959
|
isError: true
|
|
7443
7960
|
};
|
|
@@ -7453,7 +7970,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7453
7970
|
};
|
|
7454
7971
|
if (response_format === "json") {
|
|
7455
7972
|
return {
|
|
7456
|
-
content: [
|
|
7973
|
+
content: [
|
|
7974
|
+
{
|
|
7975
|
+
type: "text",
|
|
7976
|
+
text: JSON.stringify(asEnvelope14(payload), null, 2)
|
|
7977
|
+
}
|
|
7978
|
+
],
|
|
7457
7979
|
isError: false
|
|
7458
7980
|
};
|
|
7459
7981
|
}
|
|
@@ -7464,7 +7986,10 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7464
7986
|
`Status: ${data.status}`,
|
|
7465
7987
|
`Posts: ${Array.isArray(plan?.posts) ? plan.posts.length : 0}`
|
|
7466
7988
|
];
|
|
7467
|
-
return {
|
|
7989
|
+
return {
|
|
7990
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
7991
|
+
isError: false
|
|
7992
|
+
};
|
|
7468
7993
|
}
|
|
7469
7994
|
);
|
|
7470
7995
|
server.tool(
|
|
@@ -7507,14 +8032,19 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7507
8032
|
if (!stored?.plan_payload) {
|
|
7508
8033
|
return {
|
|
7509
8034
|
content: [
|
|
7510
|
-
{
|
|
8035
|
+
{
|
|
8036
|
+
type: "text",
|
|
8037
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
8038
|
+
}
|
|
7511
8039
|
],
|
|
7512
8040
|
isError: true
|
|
7513
8041
|
};
|
|
7514
8042
|
}
|
|
7515
8043
|
const plan = stored.plan_payload;
|
|
7516
8044
|
const existingPosts = Array.isArray(plan.posts) ? plan.posts : [];
|
|
7517
|
-
const updatesById = new Map(
|
|
8045
|
+
const updatesById = new Map(
|
|
8046
|
+
post_updates.map((update) => [update.post_id, update])
|
|
8047
|
+
);
|
|
7518
8048
|
const updatedPosts = existingPosts.map((post) => {
|
|
7519
8049
|
const update = updatesById.get(post.id);
|
|
7520
8050
|
if (!update) return post;
|
|
@@ -7532,7 +8062,9 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7532
8062
|
...update.status !== void 0 ? { status: update.status } : {}
|
|
7533
8063
|
};
|
|
7534
8064
|
});
|
|
7535
|
-
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";
|
|
7536
8068
|
const updatedPlan = {
|
|
7537
8069
|
...plan,
|
|
7538
8070
|
posts: updatedPosts
|
|
@@ -7559,7 +8091,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7559
8091
|
};
|
|
7560
8092
|
if (response_format === "json") {
|
|
7561
8093
|
return {
|
|
7562
|
-
content: [
|
|
8094
|
+
content: [
|
|
8095
|
+
{
|
|
8096
|
+
type: "text",
|
|
8097
|
+
text: JSON.stringify(asEnvelope14(payload), null, 2)
|
|
8098
|
+
}
|
|
8099
|
+
],
|
|
7563
8100
|
isError: false
|
|
7564
8101
|
};
|
|
7565
8102
|
}
|
|
@@ -7599,7 +8136,10 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7599
8136
|
if (!stored?.plan_payload || !stored.project_id) {
|
|
7600
8137
|
return {
|
|
7601
8138
|
content: [
|
|
7602
|
-
{
|
|
8139
|
+
{
|
|
8140
|
+
type: "text",
|
|
8141
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
8142
|
+
}
|
|
7603
8143
|
],
|
|
7604
8144
|
isError: true
|
|
7605
8145
|
};
|
|
@@ -7608,7 +8148,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7608
8148
|
const posts = Array.isArray(plan.posts) ? plan.posts : [];
|
|
7609
8149
|
if (posts.length === 0) {
|
|
7610
8150
|
return {
|
|
7611
|
-
content: [
|
|
8151
|
+
content: [
|
|
8152
|
+
{
|
|
8153
|
+
type: "text",
|
|
8154
|
+
text: `Plan ${plan_id} has no posts to submit.`
|
|
8155
|
+
}
|
|
8156
|
+
],
|
|
7612
8157
|
isError: true
|
|
7613
8158
|
};
|
|
7614
8159
|
}
|
|
@@ -7651,7 +8196,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
7651
8196
|
};
|
|
7652
8197
|
if (response_format === "json") {
|
|
7653
8198
|
return {
|
|
7654
|
-
content: [
|
|
8199
|
+
content: [
|
|
8200
|
+
{
|
|
8201
|
+
type: "text",
|
|
8202
|
+
text: JSON.stringify(asEnvelope14(payload), null, 2)
|
|
8203
|
+
}
|
|
8204
|
+
],
|
|
7655
8205
|
isError: false
|
|
7656
8206
|
};
|
|
7657
8207
|
}
|
|
@@ -7674,7 +8224,7 @@ import { z as z19 } from "zod";
|
|
|
7674
8224
|
function asEnvelope15(data) {
|
|
7675
8225
|
return {
|
|
7676
8226
|
_meta: {
|
|
7677
|
-
version:
|
|
8227
|
+
version: MCP_VERSION,
|
|
7678
8228
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7679
8229
|
},
|
|
7680
8230
|
data
|
|
@@ -7713,14 +8263,24 @@ function registerPlanApprovalTools(server) {
|
|
|
7713
8263
|
if (!projectId) {
|
|
7714
8264
|
return {
|
|
7715
8265
|
content: [
|
|
7716
|
-
{
|
|
8266
|
+
{
|
|
8267
|
+
type: "text",
|
|
8268
|
+
text: "No project_id provided and no default project found."
|
|
8269
|
+
}
|
|
7717
8270
|
],
|
|
7718
8271
|
isError: true
|
|
7719
8272
|
};
|
|
7720
8273
|
}
|
|
7721
|
-
const accessError = await assertProjectAccess(
|
|
8274
|
+
const accessError = await assertProjectAccess(
|
|
8275
|
+
supabase,
|
|
8276
|
+
userId,
|
|
8277
|
+
projectId
|
|
8278
|
+
);
|
|
7722
8279
|
if (accessError) {
|
|
7723
|
-
return {
|
|
8280
|
+
return {
|
|
8281
|
+
content: [{ type: "text", text: accessError }],
|
|
8282
|
+
isError: true
|
|
8283
|
+
};
|
|
7724
8284
|
}
|
|
7725
8285
|
const rows = posts.map((post) => ({
|
|
7726
8286
|
plan_id,
|
|
@@ -7749,7 +8309,12 @@ function registerPlanApprovalTools(server) {
|
|
|
7749
8309
|
};
|
|
7750
8310
|
if ((response_format || "text") === "json") {
|
|
7751
8311
|
return {
|
|
7752
|
-
content: [
|
|
8312
|
+
content: [
|
|
8313
|
+
{
|
|
8314
|
+
type: "text",
|
|
8315
|
+
text: JSON.stringify(asEnvelope15(payload), null, 2)
|
|
8316
|
+
}
|
|
8317
|
+
],
|
|
7753
8318
|
isError: false
|
|
7754
8319
|
};
|
|
7755
8320
|
}
|
|
@@ -7798,14 +8363,22 @@ function registerPlanApprovalTools(server) {
|
|
|
7798
8363
|
};
|
|
7799
8364
|
if ((response_format || "text") === "json") {
|
|
7800
8365
|
return {
|
|
7801
|
-
content: [
|
|
8366
|
+
content: [
|
|
8367
|
+
{
|
|
8368
|
+
type: "text",
|
|
8369
|
+
text: JSON.stringify(asEnvelope15(payload), null, 2)
|
|
8370
|
+
}
|
|
8371
|
+
],
|
|
7802
8372
|
isError: false
|
|
7803
8373
|
};
|
|
7804
8374
|
}
|
|
7805
8375
|
if (!data || data.length === 0) {
|
|
7806
8376
|
return {
|
|
7807
8377
|
content: [
|
|
7808
|
-
{
|
|
8378
|
+
{
|
|
8379
|
+
type: "text",
|
|
8380
|
+
text: `No approval items found for plan ${plan_id}.`
|
|
8381
|
+
}
|
|
7809
8382
|
],
|
|
7810
8383
|
isError: false
|
|
7811
8384
|
};
|
|
@@ -7818,7 +8391,10 @@ function registerPlanApprovalTools(server) {
|
|
|
7818
8391
|
}
|
|
7819
8392
|
lines.push("");
|
|
7820
8393
|
lines.push(`Total: ${data.length}`);
|
|
7821
|
-
return {
|
|
8394
|
+
return {
|
|
8395
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
8396
|
+
isError: false
|
|
8397
|
+
};
|
|
7822
8398
|
}
|
|
7823
8399
|
);
|
|
7824
8400
|
server.tool(
|
|
@@ -7837,7 +8413,10 @@ function registerPlanApprovalTools(server) {
|
|
|
7837
8413
|
if (decision === "edited" && !edited_post) {
|
|
7838
8414
|
return {
|
|
7839
8415
|
content: [
|
|
7840
|
-
{
|
|
8416
|
+
{
|
|
8417
|
+
type: "text",
|
|
8418
|
+
text: 'edited_post is required when decision is "edited".'
|
|
8419
|
+
}
|
|
7841
8420
|
],
|
|
7842
8421
|
isError: true
|
|
7843
8422
|
};
|
|
@@ -7850,7 +8429,9 @@ function registerPlanApprovalTools(server) {
|
|
|
7850
8429
|
if (decision === "edited") {
|
|
7851
8430
|
updates.edited_post = edited_post;
|
|
7852
8431
|
}
|
|
7853
|
-
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();
|
|
7854
8435
|
if (error) {
|
|
7855
8436
|
return {
|
|
7856
8437
|
content: [
|
|
@@ -7875,7 +8456,12 @@ function registerPlanApprovalTools(server) {
|
|
|
7875
8456
|
}
|
|
7876
8457
|
if ((response_format || "text") === "json") {
|
|
7877
8458
|
return {
|
|
7878
|
-
content: [
|
|
8459
|
+
content: [
|
|
8460
|
+
{
|
|
8461
|
+
type: "text",
|
|
8462
|
+
text: JSON.stringify(asEnvelope15(data), null, 2)
|
|
8463
|
+
}
|
|
8464
|
+
],
|
|
7879
8465
|
isError: false
|
|
7880
8466
|
};
|
|
7881
8467
|
}
|
|
@@ -7940,7 +8526,7 @@ var TOOL_CATALOG = [
|
|
|
7940
8526
|
name: "check_status",
|
|
7941
8527
|
description: "Check status of async content generation job",
|
|
7942
8528
|
module: "content",
|
|
7943
|
-
scope: "mcp:
|
|
8529
|
+
scope: "mcp:read"
|
|
7944
8530
|
},
|
|
7945
8531
|
{
|
|
7946
8532
|
name: "create_storyboard",
|
|
@@ -8373,12 +8959,34 @@ function getJWKS(supabaseUrl) {
|
|
|
8373
8959
|
}
|
|
8374
8960
|
return jwks;
|
|
8375
8961
|
}
|
|
8962
|
+
var apiKeyCache = /* @__PURE__ */ new Map();
|
|
8963
|
+
var API_KEY_CACHE_TTL_MS = 6e4;
|
|
8376
8964
|
function createTokenVerifier(options) {
|
|
8377
8965
|
const { supabaseUrl, supabaseAnonKey } = options;
|
|
8378
8966
|
return {
|
|
8379
8967
|
async verifyAccessToken(token) {
|
|
8380
8968
|
if (token.startsWith("snk_")) {
|
|
8381
|
-
|
|
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;
|
|
8382
8990
|
}
|
|
8383
8991
|
return verifySupabaseJwt(token, supabaseUrl);
|
|
8384
8992
|
}
|
|
@@ -8458,6 +9066,12 @@ if (!SUPABASE_URL2 || !SUPABASE_ANON_KEY) {
|
|
|
8458
9066
|
console.error("[MCP HTTP] Missing SUPABASE_URL or SUPABASE_ANON_KEY");
|
|
8459
9067
|
process.exit(1);
|
|
8460
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
|
+
}
|
|
8461
9075
|
process.on("uncaughtException", (err) => {
|
|
8462
9076
|
console.error(`[MCP HTTP] Uncaught exception: ${err.message}`);
|
|
8463
9077
|
process.exit(1);
|
|
@@ -8498,14 +9112,63 @@ var cleanupInterval = setInterval(
|
|
|
8498
9112
|
5 * 60 * 1e3
|
|
8499
9113
|
);
|
|
8500
9114
|
var app = express();
|
|
9115
|
+
app.disable("x-powered-by");
|
|
8501
9116
|
app.use(express.json());
|
|
8502
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
|
+
});
|
|
8503
9164
|
app.use((_req, res, next) => {
|
|
8504
9165
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
8505
9166
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
8506
|
-
res.setHeader(
|
|
9167
|
+
res.setHeader(
|
|
9168
|
+
"Access-Control-Allow-Headers",
|
|
9169
|
+
"Content-Type, Authorization, Mcp-Session-Id"
|
|
9170
|
+
);
|
|
8507
9171
|
res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
|
|
8508
|
-
res.setHeader("Vary", "Origin");
|
|
8509
9172
|
if (_req.method === "OPTIONS") {
|
|
8510
9173
|
res.status(204).end();
|
|
8511
9174
|
return;
|
|
@@ -8540,9 +9203,16 @@ async function authenticateRequest(req, res, next) {
|
|
|
8540
9203
|
const token = authHeader.slice(7);
|
|
8541
9204
|
try {
|
|
8542
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
|
+
}
|
|
8543
9213
|
req.auth = {
|
|
8544
9214
|
userId: authInfo.extra?.userId ?? authInfo.clientId,
|
|
8545
|
-
scopes
|
|
9215
|
+
scopes,
|
|
8546
9216
|
clientId: authInfo.clientId,
|
|
8547
9217
|
token: authInfo.token
|
|
8548
9218
|
};
|
|
@@ -8558,97 +9228,115 @@ async function authenticateRequest(req, res, next) {
|
|
|
8558
9228
|
app.get("/health", (_req, res) => {
|
|
8559
9229
|
res.json({ status: "ok", version: MCP_VERSION });
|
|
8560
9230
|
});
|
|
8561
|
-
app.get(
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
|
|
8569
|
-
|
|
8570
|
-
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
const auth = req.auth;
|
|
8575
|
-
const existingSessionId = req.headers["mcp-session-id"];
|
|
8576
|
-
const rl = checkRateLimit("read", auth.userId);
|
|
8577
|
-
if (!rl.allowed) {
|
|
8578
|
-
res.setHeader("Retry-After", String(rl.retryAfter));
|
|
8579
|
-
res.status(429).json({
|
|
8580
|
-
error: "rate_limited",
|
|
8581
|
-
error_description: "Too many requests. Please slow down.",
|
|
8582
|
-
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
|
|
8583
9244
|
});
|
|
8584
|
-
return;
|
|
8585
9245
|
}
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
entry.lastActivity = Date.now();
|
|
8597
|
-
await requestContext.run(
|
|
8598
|
-
{ userId: auth.userId, scopes: auth.scopes, creditsUsed: 0, assetsGenerated: 0 },
|
|
8599
|
-
() => entry.transport.handleRequest(req, res, req.body)
|
|
8600
|
-
);
|
|
8601
|
-
return;
|
|
8602
|
-
}
|
|
8603
|
-
if (sessions.size >= MAX_SESSIONS) {
|
|
8604
|
-
res.status(429).json({
|
|
8605
|
-
error: "too_many_sessions",
|
|
8606
|
-
error_description: `Server session limit reached (${MAX_SESSIONS}). Try again later.`
|
|
8607
|
-
});
|
|
8608
|
-
return;
|
|
8609
|
-
}
|
|
8610
|
-
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));
|
|
8611
9256
|
res.status(429).json({
|
|
8612
|
-
error: "
|
|
8613
|
-
error_description:
|
|
9257
|
+
error: "rate_limited",
|
|
9258
|
+
error_description: "Too many requests. Please slow down.",
|
|
9259
|
+
retry_after: rl.retryAfter
|
|
8614
9260
|
});
|
|
8615
9261
|
return;
|
|
8616
9262
|
}
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
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.`
|
|
8631
9289
|
});
|
|
9290
|
+
return;
|
|
8632
9291
|
}
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
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 } });
|
|
8637
9336
|
}
|
|
8638
|
-
};
|
|
8639
|
-
await server.connect(transport);
|
|
8640
|
-
await requestContext.run(
|
|
8641
|
-
{ userId: auth.userId, scopes: auth.scopes, creditsUsed: 0, assetsGenerated: 0 },
|
|
8642
|
-
() => transport.handleRequest(req, res, req.body)
|
|
8643
|
-
);
|
|
8644
|
-
} catch (err) {
|
|
8645
|
-
const message = err instanceof Error ? err.message : "Internal server error";
|
|
8646
|
-
console.error(`[MCP HTTP] POST /mcp error: ${message}`);
|
|
8647
|
-
if (!res.headersSent) {
|
|
8648
|
-
res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message } });
|
|
8649
9337
|
}
|
|
8650
9338
|
}
|
|
8651
|
-
|
|
9339
|
+
);
|
|
8652
9340
|
app.get("/mcp", authenticateRequest, async (req, res) => {
|
|
8653
9341
|
const sessionId = req.headers["mcp-session-id"];
|
|
8654
9342
|
if (!sessionId || !sessions.has(sessionId)) {
|
|
@@ -8664,28 +9352,39 @@ app.get("/mcp", authenticateRequest, async (req, res) => {
|
|
|
8664
9352
|
res.setHeader("X-Accel-Buffering", "no");
|
|
8665
9353
|
res.setHeader("Cache-Control", "no-cache");
|
|
8666
9354
|
await requestContext.run(
|
|
8667
|
-
{
|
|
9355
|
+
{
|
|
9356
|
+
userId: req.auth.userId,
|
|
9357
|
+
scopes: req.auth.scopes,
|
|
9358
|
+
creditsUsed: 0,
|
|
9359
|
+
assetsGenerated: 0
|
|
9360
|
+
},
|
|
8668
9361
|
() => entry.transport.handleRequest(req, res)
|
|
8669
9362
|
);
|
|
8670
9363
|
});
|
|
8671
|
-
app.delete(
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
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" });
|
|
8681
9382
|
}
|
|
8682
|
-
|
|
8683
|
-
await entry.server.close();
|
|
8684
|
-
sessions.delete(sessionId);
|
|
8685
|
-
res.status(200).json({ status: "session_closed" });
|
|
8686
|
-
});
|
|
9383
|
+
);
|
|
8687
9384
|
var httpServer = app.listen(PORT, "0.0.0.0", () => {
|
|
8688
|
-
console.log(
|
|
9385
|
+
console.log(
|
|
9386
|
+
`[MCP HTTP] Social Neuron MCP Server listening on 0.0.0.0:${PORT}`
|
|
9387
|
+
);
|
|
8689
9388
|
console.log(`[MCP HTTP] Health: http://localhost:${PORT}/health`);
|
|
8690
9389
|
console.log(`[MCP HTTP] MCP endpoint: ${MCP_SERVER_URL}`);
|
|
8691
9390
|
console.log(`[MCP HTTP] Environment: ${NODE_ENV}`);
|