@socialneuron/mcp-server 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/http.js +1223 -524
- package/dist/index.js +1063 -426
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var MCP_VERSION;
|
|
|
14
14
|
var init_version = __esm({
|
|
15
15
|
"src/lib/version.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
MCP_VERSION = "1.4.
|
|
17
|
+
MCP_VERSION = "1.4.1";
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
20
|
|
|
@@ -883,7 +883,7 @@ var init_tool_catalog = __esm({
|
|
|
883
883
|
name: "check_status",
|
|
884
884
|
description: "Check status of async content generation job",
|
|
885
885
|
module: "content",
|
|
886
|
-
scope: "mcp:
|
|
886
|
+
scope: "mcp:read"
|
|
887
887
|
},
|
|
888
888
|
{
|
|
889
889
|
name: "create_storyboard",
|
|
@@ -3746,7 +3746,7 @@ var TOOL_SCOPES = {
|
|
|
3746
3746
|
adapt_content: "mcp:write",
|
|
3747
3747
|
generate_video: "mcp:write",
|
|
3748
3748
|
generate_image: "mcp:write",
|
|
3749
|
-
check_status: "mcp:
|
|
3749
|
+
check_status: "mcp:read",
|
|
3750
3750
|
render_demo_video: "mcp:write",
|
|
3751
3751
|
save_brand_profile: "mcp:write",
|
|
3752
3752
|
update_platform_voice: "mcp:write",
|
|
@@ -3800,6 +3800,77 @@ function hasScope(userScopes, required) {
|
|
|
3800
3800
|
// src/tools/ideation.ts
|
|
3801
3801
|
init_edge_function();
|
|
3802
3802
|
import { z } from "zod";
|
|
3803
|
+
|
|
3804
|
+
// src/lib/rate-limit.ts
|
|
3805
|
+
var CATEGORY_CONFIGS = {
|
|
3806
|
+
posting: { maxTokens: 30, refillRate: 30 / 60 },
|
|
3807
|
+
// 30 req/min
|
|
3808
|
+
screenshot: { maxTokens: 10, refillRate: 10 / 60 },
|
|
3809
|
+
// 10 req/min
|
|
3810
|
+
read: { maxTokens: 60, refillRate: 60 / 60 }
|
|
3811
|
+
// 60 req/min
|
|
3812
|
+
};
|
|
3813
|
+
var RateLimiter = class {
|
|
3814
|
+
tokens;
|
|
3815
|
+
lastRefill;
|
|
3816
|
+
maxTokens;
|
|
3817
|
+
refillRate;
|
|
3818
|
+
// tokens per second
|
|
3819
|
+
constructor(config) {
|
|
3820
|
+
this.maxTokens = config.maxTokens;
|
|
3821
|
+
this.refillRate = config.refillRate;
|
|
3822
|
+
this.tokens = config.maxTokens;
|
|
3823
|
+
this.lastRefill = Date.now();
|
|
3824
|
+
}
|
|
3825
|
+
/**
|
|
3826
|
+
* Try to consume one token. Returns true if the request is allowed,
|
|
3827
|
+
* false if rate-limited.
|
|
3828
|
+
*/
|
|
3829
|
+
consume() {
|
|
3830
|
+
this.refill();
|
|
3831
|
+
if (this.tokens >= 1) {
|
|
3832
|
+
this.tokens -= 1;
|
|
3833
|
+
return true;
|
|
3834
|
+
}
|
|
3835
|
+
return false;
|
|
3836
|
+
}
|
|
3837
|
+
/**
|
|
3838
|
+
* Seconds until at least one token is available.
|
|
3839
|
+
*/
|
|
3840
|
+
retryAfter() {
|
|
3841
|
+
this.refill();
|
|
3842
|
+
if (this.tokens >= 1) return 0;
|
|
3843
|
+
return Math.ceil((1 - this.tokens) / this.refillRate);
|
|
3844
|
+
}
|
|
3845
|
+
refill() {
|
|
3846
|
+
const now = Date.now();
|
|
3847
|
+
const elapsed = (now - this.lastRefill) / 1e3;
|
|
3848
|
+
this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
|
|
3849
|
+
this.lastRefill = now;
|
|
3850
|
+
}
|
|
3851
|
+
};
|
|
3852
|
+
var limiters = /* @__PURE__ */ new Map();
|
|
3853
|
+
function getRateLimiter(category) {
|
|
3854
|
+
let limiter = limiters.get(category);
|
|
3855
|
+
if (!limiter) {
|
|
3856
|
+
const config = CATEGORY_CONFIGS[category] ?? CATEGORY_CONFIGS.read;
|
|
3857
|
+
limiter = new RateLimiter(config);
|
|
3858
|
+
limiters.set(category, limiter);
|
|
3859
|
+
}
|
|
3860
|
+
return limiter;
|
|
3861
|
+
}
|
|
3862
|
+
function checkRateLimit(category, key) {
|
|
3863
|
+
const bucketKey = key ? `${category}:${key}` : category;
|
|
3864
|
+
const limiter = getRateLimiter(bucketKey);
|
|
3865
|
+
const allowed = limiter.consume();
|
|
3866
|
+
return {
|
|
3867
|
+
allowed,
|
|
3868
|
+
retryAfter: allowed ? 0 : limiter.retryAfter()
|
|
3869
|
+
};
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3872
|
+
// src/tools/ideation.ts
|
|
3873
|
+
init_supabase();
|
|
3803
3874
|
function registerIdeationTools(server2) {
|
|
3804
3875
|
server2.tool(
|
|
3805
3876
|
"generate_content",
|
|
@@ -3820,7 +3891,9 @@ function registerIdeationTools(server2) {
|
|
|
3820
3891
|
"facebook",
|
|
3821
3892
|
"threads",
|
|
3822
3893
|
"bluesky"
|
|
3823
|
-
]).optional().describe(
|
|
3894
|
+
]).optional().describe(
|
|
3895
|
+
"Target social media platform. Helps tailor tone, length, and format."
|
|
3896
|
+
),
|
|
3824
3897
|
brand_voice: z.string().max(500).optional().describe(
|
|
3825
3898
|
'Brand voice guidelines to follow (e.g. "professional and empathetic", "playful and Gen-Z"). Leave blank to use a neutral tone.'
|
|
3826
3899
|
),
|
|
@@ -3831,7 +3904,30 @@ function registerIdeationTools(server2) {
|
|
|
3831
3904
|
"Project ID to auto-load brand profile and performance context for prompt enrichment."
|
|
3832
3905
|
)
|
|
3833
3906
|
},
|
|
3834
|
-
async ({
|
|
3907
|
+
async ({
|
|
3908
|
+
prompt: prompt2,
|
|
3909
|
+
content_type,
|
|
3910
|
+
platform: platform3,
|
|
3911
|
+
brand_voice,
|
|
3912
|
+
model,
|
|
3913
|
+
project_id
|
|
3914
|
+
}) => {
|
|
3915
|
+
try {
|
|
3916
|
+
const userId = await getDefaultUserId();
|
|
3917
|
+
const rl = checkRateLimit("posting", userId);
|
|
3918
|
+
if (!rl.allowed) {
|
|
3919
|
+
return {
|
|
3920
|
+
content: [
|
|
3921
|
+
{
|
|
3922
|
+
type: "text",
|
|
3923
|
+
text: `Rate limited. Retry after ${rl.retryAfter}s.`
|
|
3924
|
+
}
|
|
3925
|
+
],
|
|
3926
|
+
isError: true
|
|
3927
|
+
};
|
|
3928
|
+
}
|
|
3929
|
+
} catch {
|
|
3930
|
+
}
|
|
3835
3931
|
let enrichedPrompt = prompt2;
|
|
3836
3932
|
if (platform3) {
|
|
3837
3933
|
enrichedPrompt += `
|
|
@@ -3963,8 +4059,12 @@ Content Type: ${content_type}`;
|
|
|
3963
4059
|
category: z.string().optional().describe(
|
|
3964
4060
|
"Category filter (for YouTube). Examples: general, entertainment, education, tech, music, gaming, sports, news."
|
|
3965
4061
|
),
|
|
3966
|
-
niche: z.string().optional().describe(
|
|
3967
|
-
|
|
4062
|
+
niche: z.string().optional().describe(
|
|
4063
|
+
"Niche keyword filter. Only return trends matching these keywords."
|
|
4064
|
+
),
|
|
4065
|
+
url: z.string().optional().describe(
|
|
4066
|
+
'Required when source is "rss" or "url". The feed or page URL to fetch.'
|
|
4067
|
+
),
|
|
3968
4068
|
force_refresh: z.boolean().optional().describe("Skip the server-side cache and fetch fresh data.")
|
|
3969
4069
|
},
|
|
3970
4070
|
async ({ source, category, niche, url, force_refresh }) => {
|
|
@@ -4039,7 +4139,9 @@ Content Type: ${content_type}`;
|
|
|
4039
4139
|
"adapt_content",
|
|
4040
4140
|
"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.",
|
|
4041
4141
|
{
|
|
4042
|
-
content: z.string().max(5e3).describe(
|
|
4142
|
+
content: z.string().max(5e3).describe(
|
|
4143
|
+
"The content to adapt. Can be a caption, script, blog excerpt, or any text."
|
|
4144
|
+
),
|
|
4043
4145
|
source_platform: z.enum([
|
|
4044
4146
|
"youtube",
|
|
4045
4147
|
"tiktok",
|
|
@@ -4049,7 +4151,9 @@ Content Type: ${content_type}`;
|
|
|
4049
4151
|
"facebook",
|
|
4050
4152
|
"threads",
|
|
4051
4153
|
"bluesky"
|
|
4052
|
-
]).optional().describe(
|
|
4154
|
+
]).optional().describe(
|
|
4155
|
+
"The platform the content was originally written for. Helps preserve intent."
|
|
4156
|
+
),
|
|
4053
4157
|
target_platform: z.enum([
|
|
4054
4158
|
"youtube",
|
|
4055
4159
|
"tiktok",
|
|
@@ -4063,9 +4167,33 @@ Content Type: ${content_type}`;
|
|
|
4063
4167
|
brand_voice: z.string().max(500).optional().describe(
|
|
4064
4168
|
'Brand voice guidelines to maintain during adaptation (e.g. "professional", "playful").'
|
|
4065
4169
|
),
|
|
4066
|
-
project_id: z.string().uuid().optional().describe(
|
|
4170
|
+
project_id: z.string().uuid().optional().describe(
|
|
4171
|
+
"Optional project ID to load platform voice overrides from brand profile."
|
|
4172
|
+
)
|
|
4067
4173
|
},
|
|
4068
|
-
async ({
|
|
4174
|
+
async ({
|
|
4175
|
+
content,
|
|
4176
|
+
source_platform,
|
|
4177
|
+
target_platform,
|
|
4178
|
+
brand_voice,
|
|
4179
|
+
project_id
|
|
4180
|
+
}) => {
|
|
4181
|
+
try {
|
|
4182
|
+
const userId = await getDefaultUserId();
|
|
4183
|
+
const rl = checkRateLimit("posting", userId);
|
|
4184
|
+
if (!rl.allowed) {
|
|
4185
|
+
return {
|
|
4186
|
+
content: [
|
|
4187
|
+
{
|
|
4188
|
+
type: "text",
|
|
4189
|
+
text: `Rate limited. Retry after ${rl.retryAfter}s.`
|
|
4190
|
+
}
|
|
4191
|
+
],
|
|
4192
|
+
isError: true
|
|
4193
|
+
};
|
|
4194
|
+
}
|
|
4195
|
+
} catch {
|
|
4196
|
+
}
|
|
4069
4197
|
const platformGuidelines = {
|
|
4070
4198
|
twitter: "Max 280 characters. Concise, punchy. 1-3 hashtags max. Thread-friendly.",
|
|
4071
4199
|
threads: "Max 500 characters. Conversational, opinion-driven. Minimal hashtags.",
|
|
@@ -4148,76 +4276,6 @@ ${content}`,
|
|
|
4148
4276
|
// src/tools/content.ts
|
|
4149
4277
|
init_edge_function();
|
|
4150
4278
|
import { z as z2 } from "zod";
|
|
4151
|
-
|
|
4152
|
-
// src/lib/rate-limit.ts
|
|
4153
|
-
var CATEGORY_CONFIGS = {
|
|
4154
|
-
posting: { maxTokens: 30, refillRate: 30 / 60 },
|
|
4155
|
-
// 30 req/min
|
|
4156
|
-
screenshot: { maxTokens: 10, refillRate: 10 / 60 },
|
|
4157
|
-
// 10 req/min
|
|
4158
|
-
read: { maxTokens: 60, refillRate: 60 / 60 }
|
|
4159
|
-
// 60 req/min
|
|
4160
|
-
};
|
|
4161
|
-
var RateLimiter = class {
|
|
4162
|
-
tokens;
|
|
4163
|
-
lastRefill;
|
|
4164
|
-
maxTokens;
|
|
4165
|
-
refillRate;
|
|
4166
|
-
// tokens per second
|
|
4167
|
-
constructor(config) {
|
|
4168
|
-
this.maxTokens = config.maxTokens;
|
|
4169
|
-
this.refillRate = config.refillRate;
|
|
4170
|
-
this.tokens = config.maxTokens;
|
|
4171
|
-
this.lastRefill = Date.now();
|
|
4172
|
-
}
|
|
4173
|
-
/**
|
|
4174
|
-
* Try to consume one token. Returns true if the request is allowed,
|
|
4175
|
-
* false if rate-limited.
|
|
4176
|
-
*/
|
|
4177
|
-
consume() {
|
|
4178
|
-
this.refill();
|
|
4179
|
-
if (this.tokens >= 1) {
|
|
4180
|
-
this.tokens -= 1;
|
|
4181
|
-
return true;
|
|
4182
|
-
}
|
|
4183
|
-
return false;
|
|
4184
|
-
}
|
|
4185
|
-
/**
|
|
4186
|
-
* Seconds until at least one token is available.
|
|
4187
|
-
*/
|
|
4188
|
-
retryAfter() {
|
|
4189
|
-
this.refill();
|
|
4190
|
-
if (this.tokens >= 1) return 0;
|
|
4191
|
-
return Math.ceil((1 - this.tokens) / this.refillRate);
|
|
4192
|
-
}
|
|
4193
|
-
refill() {
|
|
4194
|
-
const now = Date.now();
|
|
4195
|
-
const elapsed = (now - this.lastRefill) / 1e3;
|
|
4196
|
-
this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
|
|
4197
|
-
this.lastRefill = now;
|
|
4198
|
-
}
|
|
4199
|
-
};
|
|
4200
|
-
var limiters = /* @__PURE__ */ new Map();
|
|
4201
|
-
function getRateLimiter(category) {
|
|
4202
|
-
let limiter = limiters.get(category);
|
|
4203
|
-
if (!limiter) {
|
|
4204
|
-
const config = CATEGORY_CONFIGS[category] ?? CATEGORY_CONFIGS.read;
|
|
4205
|
-
limiter = new RateLimiter(config);
|
|
4206
|
-
limiters.set(category, limiter);
|
|
4207
|
-
}
|
|
4208
|
-
return limiter;
|
|
4209
|
-
}
|
|
4210
|
-
function checkRateLimit(category, key) {
|
|
4211
|
-
const bucketKey = key ? `${category}:${key}` : category;
|
|
4212
|
-
const limiter = getRateLimiter(bucketKey);
|
|
4213
|
-
const allowed = limiter.consume();
|
|
4214
|
-
return {
|
|
4215
|
-
allowed,
|
|
4216
|
-
retryAfter: allowed ? 0 : limiter.retryAfter()
|
|
4217
|
-
};
|
|
4218
|
-
}
|
|
4219
|
-
|
|
4220
|
-
// src/tools/content.ts
|
|
4221
4279
|
init_supabase();
|
|
4222
4280
|
|
|
4223
4281
|
// src/lib/sanitize-error.ts
|
|
@@ -4271,8 +4329,15 @@ function sanitizeDbError(error) {
|
|
|
4271
4329
|
|
|
4272
4330
|
// src/tools/content.ts
|
|
4273
4331
|
init_request_context();
|
|
4274
|
-
|
|
4275
|
-
var
|
|
4332
|
+
init_version();
|
|
4333
|
+
var MAX_CREDITS_PER_RUN = Math.max(
|
|
4334
|
+
0,
|
|
4335
|
+
Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0)
|
|
4336
|
+
);
|
|
4337
|
+
var MAX_ASSETS_PER_RUN = Math.max(
|
|
4338
|
+
0,
|
|
4339
|
+
Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0)
|
|
4340
|
+
);
|
|
4276
4341
|
var _globalCreditsUsed = 0;
|
|
4277
4342
|
var _globalAssetsGenerated = 0;
|
|
4278
4343
|
function getCreditsUsed() {
|
|
@@ -4314,7 +4379,7 @@ function getCurrentBudgetStatus() {
|
|
|
4314
4379
|
function asEnvelope(data) {
|
|
4315
4380
|
return {
|
|
4316
4381
|
_meta: {
|
|
4317
|
-
version:
|
|
4382
|
+
version: MCP_VERSION,
|
|
4318
4383
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4319
4384
|
},
|
|
4320
4385
|
data
|
|
@@ -4396,8 +4461,12 @@ function registerContentTools(server2) {
|
|
|
4396
4461
|
enable_audio: z2.boolean().optional().describe(
|
|
4397
4462
|
"Enable native audio generation. Kling 2.6: doubles cost. Kling 3.0: 50% more (std 30/sec, pro 40/sec). 5+ languages."
|
|
4398
4463
|
),
|
|
4399
|
-
image_url: z2.string().optional().describe(
|
|
4400
|
-
|
|
4464
|
+
image_url: z2.string().optional().describe(
|
|
4465
|
+
"Start frame image URL for image-to-video (Kling 3.0 frame control)."
|
|
4466
|
+
),
|
|
4467
|
+
end_frame_url: z2.string().optional().describe(
|
|
4468
|
+
"End frame image URL (Kling 3.0 only). Enables seamless loop transitions."
|
|
4469
|
+
),
|
|
4401
4470
|
response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
4402
4471
|
},
|
|
4403
4472
|
async ({
|
|
@@ -4835,10 +4904,13 @@ function registerContentTools(server2) {
|
|
|
4835
4904
|
};
|
|
4836
4905
|
}
|
|
4837
4906
|
if (job.external_id && (job.status === "pending" || job.status === "processing")) {
|
|
4838
|
-
const { data: liveStatus } = await callEdgeFunction(
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4907
|
+
const { data: liveStatus } = await callEdgeFunction(
|
|
4908
|
+
"kie-task-status",
|
|
4909
|
+
{
|
|
4910
|
+
taskId: job.external_id,
|
|
4911
|
+
model: job.model
|
|
4912
|
+
}
|
|
4913
|
+
);
|
|
4842
4914
|
if (liveStatus) {
|
|
4843
4915
|
const lines2 = [
|
|
4844
4916
|
`Job: ${job.id}`,
|
|
@@ -4912,7 +4984,12 @@ function registerContentTools(server2) {
|
|
|
4912
4984
|
});
|
|
4913
4985
|
if (format === "json") {
|
|
4914
4986
|
return {
|
|
4915
|
-
content: [
|
|
4987
|
+
content: [
|
|
4988
|
+
{
|
|
4989
|
+
type: "text",
|
|
4990
|
+
text: JSON.stringify(asEnvelope(job), null, 2)
|
|
4991
|
+
}
|
|
4992
|
+
]
|
|
4916
4993
|
};
|
|
4917
4994
|
}
|
|
4918
4995
|
return {
|
|
@@ -4930,7 +5007,15 @@ function registerContentTools(server2) {
|
|
|
4930
5007
|
brand_context: z2.string().max(3e3).optional().describe(
|
|
4931
5008
|
"Brand context JSON from extract_brand. Include colors, voice tone, visual style keywords for consistent branding across frames."
|
|
4932
5009
|
),
|
|
4933
|
-
platform: z2.enum([
|
|
5010
|
+
platform: z2.enum([
|
|
5011
|
+
"tiktok",
|
|
5012
|
+
"instagram-reels",
|
|
5013
|
+
"youtube-shorts",
|
|
5014
|
+
"youtube",
|
|
5015
|
+
"general"
|
|
5016
|
+
]).describe(
|
|
5017
|
+
"Target platform. Determines aspect ratio, duration, and pacing."
|
|
5018
|
+
),
|
|
4934
5019
|
target_duration: z2.number().min(5).max(120).optional().describe(
|
|
4935
5020
|
"Target total duration in seconds. Defaults to 30s for short-form, 60s for YouTube."
|
|
4936
5021
|
),
|
|
@@ -4938,7 +5023,9 @@ function registerContentTools(server2) {
|
|
|
4938
5023
|
style: z2.string().optional().describe(
|
|
4939
5024
|
'Visual style direction (e.g., "cinematic", "anime", "documentary", "motion graphics").'
|
|
4940
5025
|
),
|
|
4941
|
-
response_format: z2.enum(["text", "json"]).optional().describe(
|
|
5026
|
+
response_format: z2.enum(["text", "json"]).optional().describe(
|
|
5027
|
+
"Response format. Defaults to json for structured storyboard data."
|
|
5028
|
+
)
|
|
4942
5029
|
},
|
|
4943
5030
|
async ({
|
|
4944
5031
|
concept,
|
|
@@ -4951,7 +5038,11 @@ function registerContentTools(server2) {
|
|
|
4951
5038
|
}) => {
|
|
4952
5039
|
const format = response_format ?? "json";
|
|
4953
5040
|
const startedAt = Date.now();
|
|
4954
|
-
const isShortForm = [
|
|
5041
|
+
const isShortForm = [
|
|
5042
|
+
"tiktok",
|
|
5043
|
+
"instagram-reels",
|
|
5044
|
+
"youtube-shorts"
|
|
5045
|
+
].includes(platform3);
|
|
4955
5046
|
const duration = target_duration ?? (isShortForm ? 30 : 60);
|
|
4956
5047
|
const scenes = num_scenes ?? (isShortForm ? 7 : 10);
|
|
4957
5048
|
const aspectRatio = isShortForm ? "9:16" : "16:9";
|
|
@@ -5044,7 +5135,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
5044
5135
|
details: { error }
|
|
5045
5136
|
});
|
|
5046
5137
|
return {
|
|
5047
|
-
content: [
|
|
5138
|
+
content: [
|
|
5139
|
+
{
|
|
5140
|
+
type: "text",
|
|
5141
|
+
text: `Storyboard generation failed: ${error}`
|
|
5142
|
+
}
|
|
5143
|
+
],
|
|
5048
5144
|
isError: true
|
|
5049
5145
|
};
|
|
5050
5146
|
}
|
|
@@ -5060,7 +5156,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
5060
5156
|
try {
|
|
5061
5157
|
const parsed = JSON.parse(rawContent);
|
|
5062
5158
|
return {
|
|
5063
|
-
content: [
|
|
5159
|
+
content: [
|
|
5160
|
+
{
|
|
5161
|
+
type: "text",
|
|
5162
|
+
text: JSON.stringify(asEnvelope(parsed), null, 2)
|
|
5163
|
+
}
|
|
5164
|
+
]
|
|
5064
5165
|
};
|
|
5065
5166
|
} catch {
|
|
5066
5167
|
return {
|
|
@@ -5124,7 +5225,10 @@ Return ONLY valid JSON in this exact format:
|
|
|
5124
5225
|
isError: true
|
|
5125
5226
|
};
|
|
5126
5227
|
}
|
|
5127
|
-
const rateLimit = checkRateLimit(
|
|
5228
|
+
const rateLimit = checkRateLimit(
|
|
5229
|
+
"posting",
|
|
5230
|
+
`generate_voiceover:${userId}`
|
|
5231
|
+
);
|
|
5128
5232
|
if (!rateLimit.allowed) {
|
|
5129
5233
|
await logMcpToolInvocation({
|
|
5130
5234
|
toolName: "generate_voiceover",
|
|
@@ -5159,7 +5263,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
5159
5263
|
details: { error }
|
|
5160
5264
|
});
|
|
5161
5265
|
return {
|
|
5162
|
-
content: [
|
|
5266
|
+
content: [
|
|
5267
|
+
{
|
|
5268
|
+
type: "text",
|
|
5269
|
+
text: `Voiceover generation failed: ${error}`
|
|
5270
|
+
}
|
|
5271
|
+
],
|
|
5163
5272
|
isError: true
|
|
5164
5273
|
};
|
|
5165
5274
|
}
|
|
@@ -5172,7 +5281,10 @@ Return ONLY valid JSON in this exact format:
|
|
|
5172
5281
|
});
|
|
5173
5282
|
return {
|
|
5174
5283
|
content: [
|
|
5175
|
-
{
|
|
5284
|
+
{
|
|
5285
|
+
type: "text",
|
|
5286
|
+
text: "Voiceover generation failed: no audio URL returned."
|
|
5287
|
+
}
|
|
5176
5288
|
],
|
|
5177
5289
|
isError: true
|
|
5178
5290
|
};
|
|
@@ -5244,7 +5356,9 @@ Return ONLY valid JSON in this exact format:
|
|
|
5244
5356
|
"Carousel template. hormozi-authority: bold typography, one idea per slide, dark backgrounds. educational-series: numbered tips. Default: hormozi-authority."
|
|
5245
5357
|
),
|
|
5246
5358
|
slide_count: z2.number().min(3).max(10).optional().describe("Number of slides (3-10). Default: 7."),
|
|
5247
|
-
aspect_ratio: z2.enum(["1:1", "4:5", "9:16"]).optional().describe(
|
|
5359
|
+
aspect_ratio: z2.enum(["1:1", "4:5", "9:16"]).optional().describe(
|
|
5360
|
+
"Aspect ratio. 1:1 square (default), 4:5 portrait, 9:16 story."
|
|
5361
|
+
),
|
|
5248
5362
|
style: z2.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
|
|
5249
5363
|
"Visual style. hormozi: black bg, bold white text, gold accents. Default: hormozi (when using hormozi-authority template)."
|
|
5250
5364
|
),
|
|
@@ -5281,7 +5395,10 @@ Return ONLY valid JSON in this exact format:
|
|
|
5281
5395
|
};
|
|
5282
5396
|
}
|
|
5283
5397
|
const userId = await getDefaultUserId();
|
|
5284
|
-
const rateLimit = checkRateLimit(
|
|
5398
|
+
const rateLimit = checkRateLimit(
|
|
5399
|
+
"posting",
|
|
5400
|
+
`generate_carousel:${userId}`
|
|
5401
|
+
);
|
|
5285
5402
|
if (!rateLimit.allowed) {
|
|
5286
5403
|
await logMcpToolInvocation({
|
|
5287
5404
|
toolName: "generate_carousel",
|
|
@@ -5319,7 +5436,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
5319
5436
|
details: { error }
|
|
5320
5437
|
});
|
|
5321
5438
|
return {
|
|
5322
|
-
content: [
|
|
5439
|
+
content: [
|
|
5440
|
+
{
|
|
5441
|
+
type: "text",
|
|
5442
|
+
text: `Carousel generation failed: ${error}`
|
|
5443
|
+
}
|
|
5444
|
+
],
|
|
5323
5445
|
isError: true
|
|
5324
5446
|
};
|
|
5325
5447
|
}
|
|
@@ -5331,7 +5453,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
5331
5453
|
details: { error: "No carousel data returned" }
|
|
5332
5454
|
});
|
|
5333
5455
|
return {
|
|
5334
|
-
content: [
|
|
5456
|
+
content: [
|
|
5457
|
+
{
|
|
5458
|
+
type: "text",
|
|
5459
|
+
text: "Carousel generation returned no data."
|
|
5460
|
+
}
|
|
5461
|
+
],
|
|
5335
5462
|
isError: true
|
|
5336
5463
|
};
|
|
5337
5464
|
}
|
|
@@ -5397,6 +5524,7 @@ import { z as z3 } from "zod";
|
|
|
5397
5524
|
import { createHash as createHash2 } from "node:crypto";
|
|
5398
5525
|
init_supabase();
|
|
5399
5526
|
init_quality();
|
|
5527
|
+
init_version();
|
|
5400
5528
|
var PLATFORM_CASE_MAP = {
|
|
5401
5529
|
youtube: "YouTube",
|
|
5402
5530
|
tiktok: "TikTok",
|
|
@@ -5410,7 +5538,7 @@ var PLATFORM_CASE_MAP = {
|
|
|
5410
5538
|
function asEnvelope2(data) {
|
|
5411
5539
|
return {
|
|
5412
5540
|
_meta: {
|
|
5413
|
-
version:
|
|
5541
|
+
version: MCP_VERSION,
|
|
5414
5542
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5415
5543
|
},
|
|
5416
5544
|
data
|
|
@@ -5442,15 +5570,21 @@ function registerDistributionTools(server2) {
|
|
|
5442
5570
|
"threads",
|
|
5443
5571
|
"bluesky"
|
|
5444
5572
|
])
|
|
5445
|
-
).min(1).describe(
|
|
5573
|
+
).min(1).describe(
|
|
5574
|
+
"Target platforms to post to. Each must have an active OAuth connection."
|
|
5575
|
+
),
|
|
5446
5576
|
title: z3.string().optional().describe("Post title (used by YouTube and some other platforms)."),
|
|
5447
|
-
hashtags: z3.array(z3.string()).optional().describe(
|
|
5577
|
+
hashtags: z3.array(z3.string()).optional().describe(
|
|
5578
|
+
'Hashtags to append to the caption. Include or omit the "#" prefix.'
|
|
5579
|
+
),
|
|
5448
5580
|
schedule_at: z3.string().optional().describe(
|
|
5449
5581
|
'ISO 8601 datetime for scheduled posting (e.g. "2026-03-15T14:00:00Z"). Omit for immediate posting.'
|
|
5450
5582
|
),
|
|
5451
5583
|
project_id: z3.string().optional().describe("Social Neuron project ID to associate this post with."),
|
|
5452
5584
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
|
|
5453
|
-
attribution: z3.boolean().optional().describe(
|
|
5585
|
+
attribution: z3.boolean().optional().describe(
|
|
5586
|
+
'If true, appends "Created with Social Neuron" to the caption. Default: false.'
|
|
5587
|
+
)
|
|
5454
5588
|
},
|
|
5455
5589
|
async ({
|
|
5456
5590
|
media_url,
|
|
@@ -5469,7 +5603,12 @@ function registerDistributionTools(server2) {
|
|
|
5469
5603
|
const startedAt = Date.now();
|
|
5470
5604
|
if ((!caption || caption.trim().length === 0) && (!title || title.trim().length === 0)) {
|
|
5471
5605
|
return {
|
|
5472
|
-
content: [
|
|
5606
|
+
content: [
|
|
5607
|
+
{
|
|
5608
|
+
type: "text",
|
|
5609
|
+
text: "Either caption or title is required."
|
|
5610
|
+
}
|
|
5611
|
+
],
|
|
5473
5612
|
isError: true
|
|
5474
5613
|
};
|
|
5475
5614
|
}
|
|
@@ -5492,7 +5631,9 @@ function registerDistributionTools(server2) {
|
|
|
5492
5631
|
isError: true
|
|
5493
5632
|
};
|
|
5494
5633
|
}
|
|
5495
|
-
const normalizedPlatforms = platforms.map(
|
|
5634
|
+
const normalizedPlatforms = platforms.map(
|
|
5635
|
+
(p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
|
|
5636
|
+
);
|
|
5496
5637
|
let finalCaption = caption;
|
|
5497
5638
|
if (attribution && finalCaption) {
|
|
5498
5639
|
finalCaption = `${finalCaption}
|
|
@@ -5556,7 +5697,9 @@ Created with Social Neuron`;
|
|
|
5556
5697
|
];
|
|
5557
5698
|
for (const [platform3, result] of Object.entries(data.results)) {
|
|
5558
5699
|
if (result.success) {
|
|
5559
|
-
lines.push(
|
|
5700
|
+
lines.push(
|
|
5701
|
+
` ${platform3}: OK (jobId=${result.jobId}, postId=${result.postId})`
|
|
5702
|
+
);
|
|
5560
5703
|
} else {
|
|
5561
5704
|
lines.push(` ${platform3}: FAILED - ${result.error}`);
|
|
5562
5705
|
}
|
|
@@ -5573,7 +5716,12 @@ Created with Social Neuron`;
|
|
|
5573
5716
|
});
|
|
5574
5717
|
if (format === "json") {
|
|
5575
5718
|
return {
|
|
5576
|
-
content: [
|
|
5719
|
+
content: [
|
|
5720
|
+
{
|
|
5721
|
+
type: "text",
|
|
5722
|
+
text: JSON.stringify(asEnvelope2(data), null, 2)
|
|
5723
|
+
}
|
|
5724
|
+
],
|
|
5577
5725
|
isError: !data.success
|
|
5578
5726
|
};
|
|
5579
5727
|
}
|
|
@@ -5629,12 +5777,17 @@ Created with Social Neuron`;
|
|
|
5629
5777
|
for (const account of accounts) {
|
|
5630
5778
|
const name = account.username || "(unnamed)";
|
|
5631
5779
|
const platformLower = account.platform.toLowerCase();
|
|
5632
|
-
lines.push(
|
|
5780
|
+
lines.push(
|
|
5781
|
+
` ${platformLower}: ${name} (connected ${account.created_at.split("T")[0]})`
|
|
5782
|
+
);
|
|
5633
5783
|
}
|
|
5634
5784
|
if (format === "json") {
|
|
5635
5785
|
return {
|
|
5636
5786
|
content: [
|
|
5637
|
-
{
|
|
5787
|
+
{
|
|
5788
|
+
type: "text",
|
|
5789
|
+
text: JSON.stringify(asEnvelope2({ accounts }), null, 2)
|
|
5790
|
+
}
|
|
5638
5791
|
]
|
|
5639
5792
|
};
|
|
5640
5793
|
}
|
|
@@ -5696,7 +5849,10 @@ Created with Social Neuron`;
|
|
|
5696
5849
|
if (format === "json") {
|
|
5697
5850
|
return {
|
|
5698
5851
|
content: [
|
|
5699
|
-
{
|
|
5852
|
+
{
|
|
5853
|
+
type: "text",
|
|
5854
|
+
text: JSON.stringify(asEnvelope2({ posts: [] }), null, 2)
|
|
5855
|
+
}
|
|
5700
5856
|
]
|
|
5701
5857
|
};
|
|
5702
5858
|
}
|
|
@@ -5713,7 +5869,10 @@ Created with Social Neuron`;
|
|
|
5713
5869
|
if (format === "json") {
|
|
5714
5870
|
return {
|
|
5715
5871
|
content: [
|
|
5716
|
-
{
|
|
5872
|
+
{
|
|
5873
|
+
type: "text",
|
|
5874
|
+
text: JSON.stringify(asEnvelope2({ posts }), null, 2)
|
|
5875
|
+
}
|
|
5717
5876
|
]
|
|
5718
5877
|
};
|
|
5719
5878
|
}
|
|
@@ -5774,7 +5933,13 @@ Created with Social Neuron`;
|
|
|
5774
5933
|
min_gap_hours: z3.number().min(1).max(24).default(4).describe("Minimum gap between posts on same platform"),
|
|
5775
5934
|
response_format: z3.enum(["text", "json"]).default("text")
|
|
5776
5935
|
},
|
|
5777
|
-
async ({
|
|
5936
|
+
async ({
|
|
5937
|
+
platforms,
|
|
5938
|
+
count,
|
|
5939
|
+
start_after,
|
|
5940
|
+
min_gap_hours,
|
|
5941
|
+
response_format
|
|
5942
|
+
}) => {
|
|
5778
5943
|
const startedAt = Date.now();
|
|
5779
5944
|
try {
|
|
5780
5945
|
const userId = await getDefaultUserId();
|
|
@@ -5785,7 +5950,9 @@ Created with Social Neuron`;
|
|
|
5785
5950
|
const gapMs = min_gap_hours * 60 * 60 * 1e3;
|
|
5786
5951
|
const candidates = [];
|
|
5787
5952
|
for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
|
|
5788
|
-
const date = new Date(
|
|
5953
|
+
const date = new Date(
|
|
5954
|
+
startDate.getTime() + dayOffset * 24 * 60 * 60 * 1e3
|
|
5955
|
+
);
|
|
5789
5956
|
const dayOfWeek = date.getUTCDay();
|
|
5790
5957
|
for (const platform3 of platforms) {
|
|
5791
5958
|
const hours = PREFERRED_HOURS[platform3] ?? [12, 16];
|
|
@@ -5794,8 +5961,11 @@ Created with Social Neuron`;
|
|
|
5794
5961
|
slotDate.setUTCHours(hours[hourIdx], 0, 0, 0);
|
|
5795
5962
|
if (slotDate <= startDate) continue;
|
|
5796
5963
|
const hasConflict = (existingPosts ?? []).some((post) => {
|
|
5797
|
-
if (String(post.platform).toLowerCase() !== platform3)
|
|
5798
|
-
|
|
5964
|
+
if (String(post.platform).toLowerCase() !== platform3)
|
|
5965
|
+
return false;
|
|
5966
|
+
const postTime = new Date(
|
|
5967
|
+
post.scheduled_at ?? post.published_at
|
|
5968
|
+
).getTime();
|
|
5799
5969
|
return Math.abs(postTime - slotDate.getTime()) < gapMs;
|
|
5800
5970
|
});
|
|
5801
5971
|
let engagementScore = hours.length - hourIdx;
|
|
@@ -5840,15 +6010,22 @@ Created with Social Neuron`;
|
|
|
5840
6010
|
};
|
|
5841
6011
|
}
|
|
5842
6012
|
const lines = [];
|
|
5843
|
-
lines.push(
|
|
6013
|
+
lines.push(
|
|
6014
|
+
`Found ${slots.length} optimal slots (${conflictsAvoided} conflicts avoided):`
|
|
6015
|
+
);
|
|
5844
6016
|
lines.push("");
|
|
5845
6017
|
lines.push("Datetime (UTC) | Platform | Score");
|
|
5846
6018
|
lines.push("-------------------------+------------+------");
|
|
5847
6019
|
for (const s of slots) {
|
|
5848
6020
|
const dt = s.datetime.replace("T", " ").slice(0, 19);
|
|
5849
|
-
lines.push(
|
|
6021
|
+
lines.push(
|
|
6022
|
+
`${dt.padEnd(25)}| ${s.platform.padEnd(11)}| ${s.engagement_score}`
|
|
6023
|
+
);
|
|
5850
6024
|
}
|
|
5851
|
-
return {
|
|
6025
|
+
return {
|
|
6026
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
6027
|
+
isError: false
|
|
6028
|
+
};
|
|
5852
6029
|
} catch (err) {
|
|
5853
6030
|
const durationMs = Date.now() - startedAt;
|
|
5854
6031
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -5859,7 +6036,9 @@ Created with Social Neuron`;
|
|
|
5859
6036
|
details: { error: message }
|
|
5860
6037
|
});
|
|
5861
6038
|
return {
|
|
5862
|
-
content: [
|
|
6039
|
+
content: [
|
|
6040
|
+
{ type: "text", text: `Failed to find slots: ${message}` }
|
|
6041
|
+
],
|
|
5863
6042
|
isError: true
|
|
5864
6043
|
};
|
|
5865
6044
|
}
|
|
@@ -5886,8 +6065,12 @@ Created with Social Neuron`;
|
|
|
5886
6065
|
auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
|
|
5887
6066
|
dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
|
|
5888
6067
|
response_format: z3.enum(["text", "json"]).default("text"),
|
|
5889
|
-
enforce_quality: z3.boolean().default(true).describe(
|
|
5890
|
-
|
|
6068
|
+
enforce_quality: z3.boolean().default(true).describe(
|
|
6069
|
+
"When true, block scheduling for posts that fail quality checks."
|
|
6070
|
+
),
|
|
6071
|
+
quality_threshold: z3.number().int().min(0).max(35).optional().describe(
|
|
6072
|
+
"Optional quality threshold override. Defaults to project setting or 26."
|
|
6073
|
+
),
|
|
5891
6074
|
batch_size: z3.number().int().min(1).max(10).default(4).describe("Concurrent schedule calls per platform batch."),
|
|
5892
6075
|
idempotency_seed: z3.string().max(128).optional().describe("Optional stable seed used when building idempotency keys.")
|
|
5893
6076
|
},
|
|
@@ -5926,17 +6109,25 @@ Created with Social Neuron`;
|
|
|
5926
6109
|
if (!stored?.plan_payload) {
|
|
5927
6110
|
return {
|
|
5928
6111
|
content: [
|
|
5929
|
-
{
|
|
6112
|
+
{
|
|
6113
|
+
type: "text",
|
|
6114
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
6115
|
+
}
|
|
5930
6116
|
],
|
|
5931
6117
|
isError: true
|
|
5932
6118
|
};
|
|
5933
6119
|
}
|
|
5934
6120
|
const payload = stored.plan_payload;
|
|
5935
|
-
const postsFromPayload = Array.isArray(payload.posts) ? payload.posts : Array.isArray(
|
|
6121
|
+
const postsFromPayload = Array.isArray(payload.posts) ? payload.posts : Array.isArray(
|
|
6122
|
+
payload.data?.posts
|
|
6123
|
+
) ? payload.data.posts : null;
|
|
5936
6124
|
if (!postsFromPayload) {
|
|
5937
6125
|
return {
|
|
5938
6126
|
content: [
|
|
5939
|
-
{
|
|
6127
|
+
{
|
|
6128
|
+
type: "text",
|
|
6129
|
+
text: `Stored plan ${plan_id} has no posts array.`
|
|
6130
|
+
}
|
|
5940
6131
|
],
|
|
5941
6132
|
isError: true
|
|
5942
6133
|
};
|
|
@@ -6028,7 +6219,10 @@ Created with Social Neuron`;
|
|
|
6028
6219
|
approvalSummary = {
|
|
6029
6220
|
total: approvals.length,
|
|
6030
6221
|
eligible: approvedPosts.length,
|
|
6031
|
-
skipped: Math.max(
|
|
6222
|
+
skipped: Math.max(
|
|
6223
|
+
0,
|
|
6224
|
+
workingPlan.posts.length - approvedPosts.length
|
|
6225
|
+
)
|
|
6032
6226
|
};
|
|
6033
6227
|
workingPlan = {
|
|
6034
6228
|
...workingPlan,
|
|
@@ -6062,9 +6256,14 @@ Created with Social Neuron`;
|
|
|
6062
6256
|
try {
|
|
6063
6257
|
const { data: settingsData } = await supabase.from("system_settings").select("value").eq("key", "content_safety").maybeSingle();
|
|
6064
6258
|
if (settingsData?.value?.quality_threshold !== void 0) {
|
|
6065
|
-
const parsedThreshold = Number(
|
|
6259
|
+
const parsedThreshold = Number(
|
|
6260
|
+
settingsData.value.quality_threshold
|
|
6261
|
+
);
|
|
6066
6262
|
if (Number.isFinite(parsedThreshold)) {
|
|
6067
|
-
effectiveQualityThreshold = Math.max(
|
|
6263
|
+
effectiveQualityThreshold = Math.max(
|
|
6264
|
+
0,
|
|
6265
|
+
Math.min(35, Math.trunc(parsedThreshold))
|
|
6266
|
+
);
|
|
6068
6267
|
}
|
|
6069
6268
|
}
|
|
6070
6269
|
if (Array.isArray(settingsData?.value?.custom_banned_terms)) {
|
|
@@ -6100,13 +6299,18 @@ Created with Social Neuron`;
|
|
|
6100
6299
|
}
|
|
6101
6300
|
};
|
|
6102
6301
|
});
|
|
6103
|
-
const qualityPassed = postsWithResults.filter(
|
|
6302
|
+
const qualityPassed = postsWithResults.filter(
|
|
6303
|
+
(post) => post.quality.passed
|
|
6304
|
+
).length;
|
|
6104
6305
|
const qualitySummary = {
|
|
6105
6306
|
total_posts: postsWithResults.length,
|
|
6106
6307
|
passed: qualityPassed,
|
|
6107
6308
|
failed: postsWithResults.length - qualityPassed,
|
|
6108
6309
|
avg_score: postsWithResults.length > 0 ? Number(
|
|
6109
|
-
(postsWithResults.reduce(
|
|
6310
|
+
(postsWithResults.reduce(
|
|
6311
|
+
(sum, post) => sum + post.quality.score,
|
|
6312
|
+
0
|
|
6313
|
+
) / postsWithResults.length).toFixed(2)
|
|
6110
6314
|
) : 0
|
|
6111
6315
|
};
|
|
6112
6316
|
if (dry_run) {
|
|
@@ -6176,8 +6380,13 @@ Created with Social Neuron`;
|
|
|
6176
6380
|
}
|
|
6177
6381
|
}
|
|
6178
6382
|
lines2.push("");
|
|
6179
|
-
lines2.push(
|
|
6180
|
-
|
|
6383
|
+
lines2.push(
|
|
6384
|
+
`Summary: ${passed}/${workingPlan.posts.length} passed quality check`
|
|
6385
|
+
);
|
|
6386
|
+
return {
|
|
6387
|
+
content: [{ type: "text", text: lines2.join("\n") }],
|
|
6388
|
+
isError: false
|
|
6389
|
+
};
|
|
6181
6390
|
}
|
|
6182
6391
|
let scheduled = 0;
|
|
6183
6392
|
let failed = 0;
|
|
@@ -6269,7 +6478,8 @@ Created with Social Neuron`;
|
|
|
6269
6478
|
}
|
|
6270
6479
|
const chunk = (arr, size) => {
|
|
6271
6480
|
const out = [];
|
|
6272
|
-
for (let i = 0; i < arr.length; i += size)
|
|
6481
|
+
for (let i = 0; i < arr.length; i += size)
|
|
6482
|
+
out.push(arr.slice(i, i + size));
|
|
6273
6483
|
return out;
|
|
6274
6484
|
};
|
|
6275
6485
|
const platformBatches = Array.from(grouped.entries()).map(
|
|
@@ -6277,7 +6487,9 @@ Created with Social Neuron`;
|
|
|
6277
6487
|
const platformResults = [];
|
|
6278
6488
|
const batches = chunk(platformPosts, batch_size);
|
|
6279
6489
|
for (const batch of batches) {
|
|
6280
|
-
const settled = await Promise.allSettled(
|
|
6490
|
+
const settled = await Promise.allSettled(
|
|
6491
|
+
batch.map((post) => scheduleOne(post))
|
|
6492
|
+
);
|
|
6281
6493
|
for (const outcome of settled) {
|
|
6282
6494
|
if (outcome.status === "fulfilled") {
|
|
6283
6495
|
platformResults.push(outcome.value);
|
|
@@ -6343,7 +6555,11 @@ Created with Social Neuron`;
|
|
|
6343
6555
|
plan_id: effectivePlanId,
|
|
6344
6556
|
approvals: approvalSummary,
|
|
6345
6557
|
posts: results,
|
|
6346
|
-
summary: {
|
|
6558
|
+
summary: {
|
|
6559
|
+
total_posts: workingPlan.posts.length,
|
|
6560
|
+
scheduled,
|
|
6561
|
+
failed
|
|
6562
|
+
}
|
|
6347
6563
|
}),
|
|
6348
6564
|
null,
|
|
6349
6565
|
2
|
|
@@ -6381,7 +6597,12 @@ Created with Social Neuron`;
|
|
|
6381
6597
|
details: { error: message }
|
|
6382
6598
|
});
|
|
6383
6599
|
return {
|
|
6384
|
-
content: [
|
|
6600
|
+
content: [
|
|
6601
|
+
{
|
|
6602
|
+
type: "text",
|
|
6603
|
+
text: `Batch scheduling failed: ${message}`
|
|
6604
|
+
}
|
|
6605
|
+
],
|
|
6385
6606
|
isError: true
|
|
6386
6607
|
};
|
|
6387
6608
|
}
|
|
@@ -6393,10 +6614,11 @@ Created with Social Neuron`;
|
|
|
6393
6614
|
init_supabase();
|
|
6394
6615
|
init_edge_function();
|
|
6395
6616
|
import { z as z4 } from "zod";
|
|
6617
|
+
init_version();
|
|
6396
6618
|
function asEnvelope3(data) {
|
|
6397
6619
|
return {
|
|
6398
6620
|
_meta: {
|
|
6399
|
-
version:
|
|
6621
|
+
version: MCP_VERSION,
|
|
6400
6622
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6401
6623
|
},
|
|
6402
6624
|
data
|
|
@@ -6509,7 +6731,9 @@ function registerAnalyticsTools(server2) {
|
|
|
6509
6731
|
]
|
|
6510
6732
|
};
|
|
6511
6733
|
}
|
|
6512
|
-
const { data: simpleRows, error: simpleError } = await supabase.from("post_analytics").select(
|
|
6734
|
+
const { data: simpleRows, error: simpleError } = await supabase.from("post_analytics").select(
|
|
6735
|
+
"id, post_id, platform, views, likes, comments, shares, captured_at"
|
|
6736
|
+
).in("post_id", postIds).gte("captured_at", sinceIso).order("captured_at", { ascending: false }).limit(maxPosts);
|
|
6513
6737
|
if (simpleError) {
|
|
6514
6738
|
return {
|
|
6515
6739
|
content: [
|
|
@@ -6521,7 +6745,12 @@ function registerAnalyticsTools(server2) {
|
|
|
6521
6745
|
isError: true
|
|
6522
6746
|
};
|
|
6523
6747
|
}
|
|
6524
|
-
return formatSimpleAnalytics(
|
|
6748
|
+
return formatSimpleAnalytics(
|
|
6749
|
+
simpleRows,
|
|
6750
|
+
platform3,
|
|
6751
|
+
lookbackDays,
|
|
6752
|
+
format
|
|
6753
|
+
);
|
|
6525
6754
|
}
|
|
6526
6755
|
if (!rows || rows.length === 0) {
|
|
6527
6756
|
if (format === "json") {
|
|
@@ -6598,7 +6827,10 @@ function registerAnalyticsTools(server2) {
|
|
|
6598
6827
|
const format = response_format ?? "text";
|
|
6599
6828
|
const startedAt = Date.now();
|
|
6600
6829
|
const userId = await getDefaultUserId();
|
|
6601
|
-
const rateLimit = checkRateLimit(
|
|
6830
|
+
const rateLimit = checkRateLimit(
|
|
6831
|
+
"posting",
|
|
6832
|
+
`refresh_platform_analytics:${userId}`
|
|
6833
|
+
);
|
|
6602
6834
|
if (!rateLimit.allowed) {
|
|
6603
6835
|
await logMcpToolInvocation({
|
|
6604
6836
|
toolName: "refresh_platform_analytics",
|
|
@@ -6616,7 +6848,9 @@ function registerAnalyticsTools(server2) {
|
|
|
6616
6848
|
isError: true
|
|
6617
6849
|
};
|
|
6618
6850
|
}
|
|
6619
|
-
const { data, error } = await callEdgeFunction("fetch-analytics", {
|
|
6851
|
+
const { data, error } = await callEdgeFunction("fetch-analytics", {
|
|
6852
|
+
userId
|
|
6853
|
+
});
|
|
6620
6854
|
if (error) {
|
|
6621
6855
|
await logMcpToolInvocation({
|
|
6622
6856
|
toolName: "refresh_platform_analytics",
|
|
@@ -6625,7 +6859,12 @@ function registerAnalyticsTools(server2) {
|
|
|
6625
6859
|
details: { error }
|
|
6626
6860
|
});
|
|
6627
6861
|
return {
|
|
6628
|
-
content: [
|
|
6862
|
+
content: [
|
|
6863
|
+
{
|
|
6864
|
+
type: "text",
|
|
6865
|
+
text: `Error refreshing analytics: ${error}`
|
|
6866
|
+
}
|
|
6867
|
+
],
|
|
6629
6868
|
isError: true
|
|
6630
6869
|
};
|
|
6631
6870
|
}
|
|
@@ -6638,12 +6877,18 @@ function registerAnalyticsTools(server2) {
|
|
|
6638
6877
|
details: { error: "Edge function returned success=false" }
|
|
6639
6878
|
});
|
|
6640
6879
|
return {
|
|
6641
|
-
content: [
|
|
6880
|
+
content: [
|
|
6881
|
+
{ type: "text", text: "Analytics refresh failed." }
|
|
6882
|
+
],
|
|
6642
6883
|
isError: true
|
|
6643
6884
|
};
|
|
6644
6885
|
}
|
|
6645
|
-
const queued = (result.results ?? []).filter(
|
|
6646
|
-
|
|
6886
|
+
const queued = (result.results ?? []).filter(
|
|
6887
|
+
(r) => r.status === "queued"
|
|
6888
|
+
).length;
|
|
6889
|
+
const errored = (result.results ?? []).filter(
|
|
6890
|
+
(r) => r.status === "error"
|
|
6891
|
+
).length;
|
|
6647
6892
|
const lines = [
|
|
6648
6893
|
`Analytics refresh triggered successfully.`,
|
|
6649
6894
|
` Posts processed: ${result.postsProcessed}`,
|
|
@@ -6689,7 +6934,10 @@ function formatAnalytics(summary, days, format) {
|
|
|
6689
6934
|
if (format === "json") {
|
|
6690
6935
|
return {
|
|
6691
6936
|
content: [
|
|
6692
|
-
{
|
|
6937
|
+
{
|
|
6938
|
+
type: "text",
|
|
6939
|
+
text: JSON.stringify(asEnvelope3({ ...summary, days }), null, 2)
|
|
6940
|
+
}
|
|
6693
6941
|
]
|
|
6694
6942
|
};
|
|
6695
6943
|
}
|
|
@@ -6807,10 +7055,160 @@ function formatSimpleAnalytics(rows, platform3, days, format) {
|
|
|
6807
7055
|
init_edge_function();
|
|
6808
7056
|
init_supabase();
|
|
6809
7057
|
import { z as z5 } from "zod";
|
|
7058
|
+
|
|
7059
|
+
// src/lib/ssrf.ts
|
|
7060
|
+
var BLOCKED_IP_PATTERNS = [
|
|
7061
|
+
// IPv4 localhost/loopback
|
|
7062
|
+
/^127\./,
|
|
7063
|
+
/^0\./,
|
|
7064
|
+
// IPv4 private ranges (RFC 1918)
|
|
7065
|
+
/^10\./,
|
|
7066
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
|
7067
|
+
/^192\.168\./,
|
|
7068
|
+
// IPv4 link-local
|
|
7069
|
+
/^169\.254\./,
|
|
7070
|
+
// Cloud metadata endpoint (AWS, GCP, Azure)
|
|
7071
|
+
/^169\.254\.169\.254$/,
|
|
7072
|
+
// IPv4 broadcast
|
|
7073
|
+
/^255\./,
|
|
7074
|
+
// Shared address space (RFC 6598)
|
|
7075
|
+
/^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./
|
|
7076
|
+
];
|
|
7077
|
+
var BLOCKED_IPV6_PATTERNS = [
|
|
7078
|
+
/^::1$/i,
|
|
7079
|
+
// loopback
|
|
7080
|
+
/^::$/i,
|
|
7081
|
+
// unspecified
|
|
7082
|
+
/^fe[89ab][0-9a-f]:/i,
|
|
7083
|
+
// link-local fe80::/10
|
|
7084
|
+
/^fc[0-9a-f]:/i,
|
|
7085
|
+
// unique local fc00::/7
|
|
7086
|
+
/^fd[0-9a-f]:/i,
|
|
7087
|
+
// unique local fc00::/7
|
|
7088
|
+
/^::ffff:127\./i,
|
|
7089
|
+
// IPv4-mapped localhost
|
|
7090
|
+
/^::ffff:(0|10|127|169\.254|172\.(1[6-9]|2[0-9]|3[0-1])|192\.168)\./i
|
|
7091
|
+
// IPv4-mapped private
|
|
7092
|
+
];
|
|
7093
|
+
var BLOCKED_HOSTNAMES = [
|
|
7094
|
+
"localhost",
|
|
7095
|
+
"localhost.localdomain",
|
|
7096
|
+
"local",
|
|
7097
|
+
"127.0.0.1",
|
|
7098
|
+
"0.0.0.0",
|
|
7099
|
+
"[::1]",
|
|
7100
|
+
"[::ffff:127.0.0.1]",
|
|
7101
|
+
// Cloud metadata endpoints
|
|
7102
|
+
"metadata.google.internal",
|
|
7103
|
+
"metadata.goog",
|
|
7104
|
+
"instance-data",
|
|
7105
|
+
"instance-data.ec2.internal"
|
|
7106
|
+
];
|
|
7107
|
+
var ALLOWED_PROTOCOLS = ["http:", "https:"];
|
|
7108
|
+
var BLOCKED_PORTS = [22, 23, 25, 110, 143, 445, 3306, 5432, 6379, 27017, 11211];
|
|
7109
|
+
function isBlockedIP(ip) {
|
|
7110
|
+
const normalized = ip.replace(/^\[/, "").replace(/\]$/, "");
|
|
7111
|
+
if (normalized.includes(":")) {
|
|
7112
|
+
return BLOCKED_IPV6_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
7113
|
+
}
|
|
7114
|
+
return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
7115
|
+
}
|
|
7116
|
+
function isBlockedHostname(hostname) {
|
|
7117
|
+
return BLOCKED_HOSTNAMES.includes(hostname.toLowerCase());
|
|
7118
|
+
}
|
|
7119
|
+
function isIPAddress(hostname) {
|
|
7120
|
+
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
7121
|
+
const ipv6Pattern = /^\[?[a-fA-F0-9:]+\]?$/;
|
|
7122
|
+
return ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname);
|
|
7123
|
+
}
|
|
7124
|
+
async function validateUrlForSSRF(urlString) {
|
|
7125
|
+
try {
|
|
7126
|
+
const url = new URL(urlString);
|
|
7127
|
+
if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
|
|
7128
|
+
return {
|
|
7129
|
+
isValid: false,
|
|
7130
|
+
error: `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are allowed.`
|
|
7131
|
+
};
|
|
7132
|
+
}
|
|
7133
|
+
if (url.username || url.password) {
|
|
7134
|
+
return {
|
|
7135
|
+
isValid: false,
|
|
7136
|
+
error: "URLs with embedded credentials are not allowed."
|
|
7137
|
+
};
|
|
7138
|
+
}
|
|
7139
|
+
const hostname = url.hostname.toLowerCase();
|
|
7140
|
+
if (isBlockedHostname(hostname)) {
|
|
7141
|
+
return {
|
|
7142
|
+
isValid: false,
|
|
7143
|
+
error: "Access to internal/localhost addresses is not allowed."
|
|
7144
|
+
};
|
|
7145
|
+
}
|
|
7146
|
+
if (isIPAddress(hostname) && isBlockedIP(hostname)) {
|
|
7147
|
+
return {
|
|
7148
|
+
isValid: false,
|
|
7149
|
+
error: "Access to private/internal IP addresses is not allowed."
|
|
7150
|
+
};
|
|
7151
|
+
}
|
|
7152
|
+
const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80;
|
|
7153
|
+
if (BLOCKED_PORTS.includes(port)) {
|
|
7154
|
+
return {
|
|
7155
|
+
isValid: false,
|
|
7156
|
+
error: `Access to port ${port} is not allowed.`
|
|
7157
|
+
};
|
|
7158
|
+
}
|
|
7159
|
+
let resolvedIP;
|
|
7160
|
+
if (!isIPAddress(hostname)) {
|
|
7161
|
+
try {
|
|
7162
|
+
const dns = await import("node:dns");
|
|
7163
|
+
const resolver = new dns.promises.Resolver();
|
|
7164
|
+
const resolvedIPs = [];
|
|
7165
|
+
try {
|
|
7166
|
+
const aRecords = await resolver.resolve4(hostname);
|
|
7167
|
+
resolvedIPs.push(...aRecords);
|
|
7168
|
+
} catch {
|
|
7169
|
+
}
|
|
7170
|
+
try {
|
|
7171
|
+
const aaaaRecords = await resolver.resolve6(hostname);
|
|
7172
|
+
resolvedIPs.push(...aaaaRecords);
|
|
7173
|
+
} catch {
|
|
7174
|
+
}
|
|
7175
|
+
if (resolvedIPs.length === 0) {
|
|
7176
|
+
return {
|
|
7177
|
+
isValid: false,
|
|
7178
|
+
error: "DNS resolution failed: hostname did not resolve to any address."
|
|
7179
|
+
};
|
|
7180
|
+
}
|
|
7181
|
+
for (const ip of resolvedIPs) {
|
|
7182
|
+
if (isBlockedIP(ip)) {
|
|
7183
|
+
return {
|
|
7184
|
+
isValid: false,
|
|
7185
|
+
error: "Hostname resolves to a private/internal IP address."
|
|
7186
|
+
};
|
|
7187
|
+
}
|
|
7188
|
+
}
|
|
7189
|
+
resolvedIP = resolvedIPs[0];
|
|
7190
|
+
} catch {
|
|
7191
|
+
return {
|
|
7192
|
+
isValid: false,
|
|
7193
|
+
error: "DNS resolution failed. Cannot verify hostname safety."
|
|
7194
|
+
};
|
|
7195
|
+
}
|
|
7196
|
+
}
|
|
7197
|
+
return { isValid: true, sanitizedUrl: url.toString(), resolvedIP };
|
|
7198
|
+
} catch (error) {
|
|
7199
|
+
return {
|
|
7200
|
+
isValid: false,
|
|
7201
|
+
error: `Invalid URL format: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
7202
|
+
};
|
|
7203
|
+
}
|
|
7204
|
+
}
|
|
7205
|
+
|
|
7206
|
+
// src/tools/brand.ts
|
|
7207
|
+
init_version();
|
|
6810
7208
|
function asEnvelope4(data) {
|
|
6811
7209
|
return {
|
|
6812
7210
|
_meta: {
|
|
6813
|
-
version:
|
|
7211
|
+
version: MCP_VERSION,
|
|
6814
7212
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6815
7213
|
},
|
|
6816
7214
|
data
|
|
@@ -6827,6 +7225,15 @@ function registerBrandTools(server2) {
|
|
|
6827
7225
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6828
7226
|
},
|
|
6829
7227
|
async ({ url, response_format }) => {
|
|
7228
|
+
const ssrfCheck = await validateUrlForSSRF(url);
|
|
7229
|
+
if (!ssrfCheck.isValid) {
|
|
7230
|
+
return {
|
|
7231
|
+
content: [
|
|
7232
|
+
{ type: "text", text: `URL blocked: ${ssrfCheck.error}` }
|
|
7233
|
+
],
|
|
7234
|
+
isError: true
|
|
7235
|
+
};
|
|
7236
|
+
}
|
|
6830
7237
|
const { data, error } = await callEdgeFunction(
|
|
6831
7238
|
"brand-extract",
|
|
6832
7239
|
{ url },
|
|
@@ -6856,7 +7263,12 @@ function registerBrandTools(server2) {
|
|
|
6856
7263
|
}
|
|
6857
7264
|
if ((response_format || "text") === "json") {
|
|
6858
7265
|
return {
|
|
6859
|
-
content: [
|
|
7266
|
+
content: [
|
|
7267
|
+
{
|
|
7268
|
+
type: "text",
|
|
7269
|
+
text: JSON.stringify(asEnvelope4(data), null, 2)
|
|
7270
|
+
}
|
|
7271
|
+
]
|
|
6860
7272
|
};
|
|
6861
7273
|
}
|
|
6862
7274
|
const lines = [
|
|
@@ -6920,7 +7332,12 @@ function registerBrandTools(server2) {
|
|
|
6920
7332
|
const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
|
|
6921
7333
|
if (!membership) {
|
|
6922
7334
|
return {
|
|
6923
|
-
content: [
|
|
7335
|
+
content: [
|
|
7336
|
+
{
|
|
7337
|
+
type: "text",
|
|
7338
|
+
text: "Project is not accessible to current user."
|
|
7339
|
+
}
|
|
7340
|
+
],
|
|
6924
7341
|
isError: true
|
|
6925
7342
|
};
|
|
6926
7343
|
}
|
|
@@ -6939,13 +7356,21 @@ function registerBrandTools(server2) {
|
|
|
6939
7356
|
if (!data) {
|
|
6940
7357
|
return {
|
|
6941
7358
|
content: [
|
|
6942
|
-
{
|
|
7359
|
+
{
|
|
7360
|
+
type: "text",
|
|
7361
|
+
text: "No active brand profile found for this project."
|
|
7362
|
+
}
|
|
6943
7363
|
]
|
|
6944
7364
|
};
|
|
6945
7365
|
}
|
|
6946
7366
|
if ((response_format || "text") === "json") {
|
|
6947
7367
|
return {
|
|
6948
|
-
content: [
|
|
7368
|
+
content: [
|
|
7369
|
+
{
|
|
7370
|
+
type: "text",
|
|
7371
|
+
text: JSON.stringify(asEnvelope4(data), null, 2)
|
|
7372
|
+
}
|
|
7373
|
+
]
|
|
6949
7374
|
};
|
|
6950
7375
|
}
|
|
6951
7376
|
const lines = [
|
|
@@ -6966,11 +7391,18 @@ function registerBrandTools(server2) {
|
|
|
6966
7391
|
"Persist a brand profile as the active profile for a project.",
|
|
6967
7392
|
{
|
|
6968
7393
|
project_id: z5.string().uuid().optional().describe("Project ID. Defaults to active project context."),
|
|
6969
|
-
brand_context: z5.record(z5.string(), z5.unknown()).describe(
|
|
7394
|
+
brand_context: z5.record(z5.string(), z5.unknown()).describe(
|
|
7395
|
+
"Brand context payload to save to brand_profiles.brand_context."
|
|
7396
|
+
),
|
|
6970
7397
|
change_summary: z5.string().max(500).optional().describe("Optional summary of changes."),
|
|
6971
7398
|
changed_paths: z5.array(z5.string()).optional().describe("Optional changed path list."),
|
|
6972
7399
|
source_url: z5.string().url().optional().describe("Optional source URL for provenance."),
|
|
6973
|
-
extraction_method: z5.enum([
|
|
7400
|
+
extraction_method: z5.enum([
|
|
7401
|
+
"manual",
|
|
7402
|
+
"url_extract",
|
|
7403
|
+
"business_profiler",
|
|
7404
|
+
"product_showcase"
|
|
7405
|
+
]).optional().describe("Extraction method metadata."),
|
|
6974
7406
|
overall_confidence: z5.number().min(0).max(1).optional().describe("Optional overall confidence score in range 0..1."),
|
|
6975
7407
|
extraction_metadata: z5.record(z5.string(), z5.unknown()).optional(),
|
|
6976
7408
|
response_format: z5.enum(["text", "json"]).optional()
|
|
@@ -7010,20 +7442,28 @@ function registerBrandTools(server2) {
|
|
|
7010
7442
|
const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
|
|
7011
7443
|
if (!membership) {
|
|
7012
7444
|
return {
|
|
7013
|
-
content: [
|
|
7445
|
+
content: [
|
|
7446
|
+
{
|
|
7447
|
+
type: "text",
|
|
7448
|
+
text: "Project is not accessible to current user."
|
|
7449
|
+
}
|
|
7450
|
+
],
|
|
7014
7451
|
isError: true
|
|
7015
7452
|
};
|
|
7016
7453
|
}
|
|
7017
|
-
const { data: profileId, error } = await supabase.rpc(
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7454
|
+
const { data: profileId, error } = await supabase.rpc(
|
|
7455
|
+
"set_active_brand_profile",
|
|
7456
|
+
{
|
|
7457
|
+
p_project_id: projectId,
|
|
7458
|
+
p_brand_context: brand_context,
|
|
7459
|
+
p_change_summary: change_summary || null,
|
|
7460
|
+
p_changed_paths: changed_paths || [],
|
|
7461
|
+
p_source_url: source_url || null,
|
|
7462
|
+
p_extraction_method: extraction_method || "manual",
|
|
7463
|
+
p_overall_confidence: overall_confidence ?? null,
|
|
7464
|
+
p_extraction_metadata: extraction_metadata || null
|
|
7465
|
+
}
|
|
7466
|
+
);
|
|
7027
7467
|
if (error) {
|
|
7028
7468
|
return {
|
|
7029
7469
|
content: [
|
|
@@ -7044,7 +7484,12 @@ function registerBrandTools(server2) {
|
|
|
7044
7484
|
};
|
|
7045
7485
|
if ((response_format || "text") === "json") {
|
|
7046
7486
|
return {
|
|
7047
|
-
content: [
|
|
7487
|
+
content: [
|
|
7488
|
+
{
|
|
7489
|
+
type: "text",
|
|
7490
|
+
text: JSON.stringify(asEnvelope4(payload), null, 2)
|
|
7491
|
+
}
|
|
7492
|
+
]
|
|
7048
7493
|
};
|
|
7049
7494
|
}
|
|
7050
7495
|
return {
|
|
@@ -7100,7 +7545,10 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
7100
7545
|
if (!projectId) {
|
|
7101
7546
|
return {
|
|
7102
7547
|
content: [
|
|
7103
|
-
{
|
|
7548
|
+
{
|
|
7549
|
+
type: "text",
|
|
7550
|
+
text: "No project_id provided and no default project found."
|
|
7551
|
+
}
|
|
7104
7552
|
],
|
|
7105
7553
|
isError: true
|
|
7106
7554
|
};
|
|
@@ -7115,7 +7563,12 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
7115
7563
|
const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
|
|
7116
7564
|
if (!membership) {
|
|
7117
7565
|
return {
|
|
7118
|
-
content: [
|
|
7566
|
+
content: [
|
|
7567
|
+
{
|
|
7568
|
+
type: "text",
|
|
7569
|
+
text: "Project is not accessible to current user."
|
|
7570
|
+
}
|
|
7571
|
+
],
|
|
7119
7572
|
isError: true
|
|
7120
7573
|
};
|
|
7121
7574
|
}
|
|
@@ -7131,7 +7584,9 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
7131
7584
|
isError: true
|
|
7132
7585
|
};
|
|
7133
7586
|
}
|
|
7134
|
-
const brandContext = {
|
|
7587
|
+
const brandContext = {
|
|
7588
|
+
...existingProfile.brand_context
|
|
7589
|
+
};
|
|
7135
7590
|
const voiceProfile = brandContext.voiceProfile ?? {};
|
|
7136
7591
|
const platformOverrides = voiceProfile.platformOverrides ?? {};
|
|
7137
7592
|
const existingOverride = platformOverrides[platform3] ?? {};
|
|
@@ -7155,16 +7610,19 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
7155
7610
|
...brandContext,
|
|
7156
7611
|
voiceProfile: updatedVoiceProfile
|
|
7157
7612
|
};
|
|
7158
|
-
const { data: profileId, error: saveError } = await supabase.rpc(
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7613
|
+
const { data: profileId, error: saveError } = await supabase.rpc(
|
|
7614
|
+
"set_active_brand_profile",
|
|
7615
|
+
{
|
|
7616
|
+
p_project_id: projectId,
|
|
7617
|
+
p_brand_context: updatedContext,
|
|
7618
|
+
p_change_summary: `Updated platform voice override for ${platform3}`,
|
|
7619
|
+
p_changed_paths: [`voiceProfile.platformOverrides.${platform3}`],
|
|
7620
|
+
p_source_url: null,
|
|
7621
|
+
p_extraction_method: "manual",
|
|
7622
|
+
p_overall_confidence: null,
|
|
7623
|
+
p_extraction_metadata: null
|
|
7624
|
+
}
|
|
7625
|
+
);
|
|
7168
7626
|
if (saveError) {
|
|
7169
7627
|
return {
|
|
7170
7628
|
content: [
|
|
@@ -7185,7 +7643,12 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
7185
7643
|
};
|
|
7186
7644
|
if ((response_format || "text") === "json") {
|
|
7187
7645
|
return {
|
|
7188
|
-
content: [
|
|
7646
|
+
content: [
|
|
7647
|
+
{
|
|
7648
|
+
type: "text",
|
|
7649
|
+
text: JSON.stringify(asEnvelope4(payload), null, 2)
|
|
7650
|
+
}
|
|
7651
|
+
],
|
|
7189
7652
|
isError: false
|
|
7190
7653
|
};
|
|
7191
7654
|
}
|
|
@@ -7283,155 +7746,6 @@ async function capturePageScreenshot(page, outputPath, selector) {
|
|
|
7283
7746
|
// src/tools/screenshot.ts
|
|
7284
7747
|
import { resolve, relative } from "node:path";
|
|
7285
7748
|
import { mkdir } from "node:fs/promises";
|
|
7286
|
-
|
|
7287
|
-
// src/lib/ssrf.ts
|
|
7288
|
-
var BLOCKED_IP_PATTERNS = [
|
|
7289
|
-
// IPv4 localhost/loopback
|
|
7290
|
-
/^127\./,
|
|
7291
|
-
/^0\./,
|
|
7292
|
-
// IPv4 private ranges (RFC 1918)
|
|
7293
|
-
/^10\./,
|
|
7294
|
-
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
|
7295
|
-
/^192\.168\./,
|
|
7296
|
-
// IPv4 link-local
|
|
7297
|
-
/^169\.254\./,
|
|
7298
|
-
// Cloud metadata endpoint (AWS, GCP, Azure)
|
|
7299
|
-
/^169\.254\.169\.254$/,
|
|
7300
|
-
// IPv4 broadcast
|
|
7301
|
-
/^255\./,
|
|
7302
|
-
// Shared address space (RFC 6598)
|
|
7303
|
-
/^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./
|
|
7304
|
-
];
|
|
7305
|
-
var BLOCKED_IPV6_PATTERNS = [
|
|
7306
|
-
/^::1$/i,
|
|
7307
|
-
// loopback
|
|
7308
|
-
/^::$/i,
|
|
7309
|
-
// unspecified
|
|
7310
|
-
/^fe[89ab][0-9a-f]:/i,
|
|
7311
|
-
// link-local fe80::/10
|
|
7312
|
-
/^fc[0-9a-f]:/i,
|
|
7313
|
-
// unique local fc00::/7
|
|
7314
|
-
/^fd[0-9a-f]:/i,
|
|
7315
|
-
// unique local fc00::/7
|
|
7316
|
-
/^::ffff:127\./i,
|
|
7317
|
-
// IPv4-mapped localhost
|
|
7318
|
-
/^::ffff:(0|10|127|169\.254|172\.(1[6-9]|2[0-9]|3[0-1])|192\.168)\./i
|
|
7319
|
-
// IPv4-mapped private
|
|
7320
|
-
];
|
|
7321
|
-
var BLOCKED_HOSTNAMES = [
|
|
7322
|
-
"localhost",
|
|
7323
|
-
"localhost.localdomain",
|
|
7324
|
-
"local",
|
|
7325
|
-
"127.0.0.1",
|
|
7326
|
-
"0.0.0.0",
|
|
7327
|
-
"[::1]",
|
|
7328
|
-
"[::ffff:127.0.0.1]",
|
|
7329
|
-
// Cloud metadata endpoints
|
|
7330
|
-
"metadata.google.internal",
|
|
7331
|
-
"metadata.goog",
|
|
7332
|
-
"instance-data",
|
|
7333
|
-
"instance-data.ec2.internal"
|
|
7334
|
-
];
|
|
7335
|
-
var ALLOWED_PROTOCOLS = ["http:", "https:"];
|
|
7336
|
-
var BLOCKED_PORTS = [22, 23, 25, 110, 143, 445, 3306, 5432, 6379, 27017, 11211];
|
|
7337
|
-
function isBlockedIP(ip) {
|
|
7338
|
-
const normalized = ip.replace(/^\[/, "").replace(/\]$/, "");
|
|
7339
|
-
if (normalized.includes(":")) {
|
|
7340
|
-
return BLOCKED_IPV6_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
7341
|
-
}
|
|
7342
|
-
return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
7343
|
-
}
|
|
7344
|
-
function isBlockedHostname(hostname) {
|
|
7345
|
-
return BLOCKED_HOSTNAMES.includes(hostname.toLowerCase());
|
|
7346
|
-
}
|
|
7347
|
-
function isIPAddress(hostname) {
|
|
7348
|
-
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
7349
|
-
const ipv6Pattern = /^\[?[a-fA-F0-9:]+\]?$/;
|
|
7350
|
-
return ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname);
|
|
7351
|
-
}
|
|
7352
|
-
async function validateUrlForSSRF(urlString) {
|
|
7353
|
-
try {
|
|
7354
|
-
const url = new URL(urlString);
|
|
7355
|
-
if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
|
|
7356
|
-
return {
|
|
7357
|
-
isValid: false,
|
|
7358
|
-
error: `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are allowed.`
|
|
7359
|
-
};
|
|
7360
|
-
}
|
|
7361
|
-
if (url.username || url.password) {
|
|
7362
|
-
return {
|
|
7363
|
-
isValid: false,
|
|
7364
|
-
error: "URLs with embedded credentials are not allowed."
|
|
7365
|
-
};
|
|
7366
|
-
}
|
|
7367
|
-
const hostname = url.hostname.toLowerCase();
|
|
7368
|
-
if (isBlockedHostname(hostname)) {
|
|
7369
|
-
return {
|
|
7370
|
-
isValid: false,
|
|
7371
|
-
error: "Access to internal/localhost addresses is not allowed."
|
|
7372
|
-
};
|
|
7373
|
-
}
|
|
7374
|
-
if (isIPAddress(hostname) && isBlockedIP(hostname)) {
|
|
7375
|
-
return {
|
|
7376
|
-
isValid: false,
|
|
7377
|
-
error: "Access to private/internal IP addresses is not allowed."
|
|
7378
|
-
};
|
|
7379
|
-
}
|
|
7380
|
-
const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80;
|
|
7381
|
-
if (BLOCKED_PORTS.includes(port)) {
|
|
7382
|
-
return {
|
|
7383
|
-
isValid: false,
|
|
7384
|
-
error: `Access to port ${port} is not allowed.`
|
|
7385
|
-
};
|
|
7386
|
-
}
|
|
7387
|
-
let resolvedIP;
|
|
7388
|
-
if (!isIPAddress(hostname)) {
|
|
7389
|
-
try {
|
|
7390
|
-
const dns = await import("node:dns");
|
|
7391
|
-
const resolver = new dns.promises.Resolver();
|
|
7392
|
-
const resolvedIPs = [];
|
|
7393
|
-
try {
|
|
7394
|
-
const aRecords = await resolver.resolve4(hostname);
|
|
7395
|
-
resolvedIPs.push(...aRecords);
|
|
7396
|
-
} catch {
|
|
7397
|
-
}
|
|
7398
|
-
try {
|
|
7399
|
-
const aaaaRecords = await resolver.resolve6(hostname);
|
|
7400
|
-
resolvedIPs.push(...aaaaRecords);
|
|
7401
|
-
} catch {
|
|
7402
|
-
}
|
|
7403
|
-
if (resolvedIPs.length === 0) {
|
|
7404
|
-
return {
|
|
7405
|
-
isValid: false,
|
|
7406
|
-
error: "DNS resolution failed: hostname did not resolve to any address."
|
|
7407
|
-
};
|
|
7408
|
-
}
|
|
7409
|
-
for (const ip of resolvedIPs) {
|
|
7410
|
-
if (isBlockedIP(ip)) {
|
|
7411
|
-
return {
|
|
7412
|
-
isValid: false,
|
|
7413
|
-
error: "Hostname resolves to a private/internal IP address."
|
|
7414
|
-
};
|
|
7415
|
-
}
|
|
7416
|
-
}
|
|
7417
|
-
resolvedIP = resolvedIPs[0];
|
|
7418
|
-
} catch {
|
|
7419
|
-
return {
|
|
7420
|
-
isValid: false,
|
|
7421
|
-
error: "DNS resolution failed. Cannot verify hostname safety."
|
|
7422
|
-
};
|
|
7423
|
-
}
|
|
7424
|
-
}
|
|
7425
|
-
return { isValid: true, sanitizedUrl: url.toString(), resolvedIP };
|
|
7426
|
-
} catch (error) {
|
|
7427
|
-
return {
|
|
7428
|
-
isValid: false,
|
|
7429
|
-
error: `Invalid URL format: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
7430
|
-
};
|
|
7431
|
-
}
|
|
7432
|
-
}
|
|
7433
|
-
|
|
7434
|
-
// src/tools/screenshot.ts
|
|
7435
7749
|
init_supabase();
|
|
7436
7750
|
function registerScreenshotTools(server2) {
|
|
7437
7751
|
server2.tool(
|
|
@@ -8007,6 +8321,8 @@ function registerRemotionTools(server2) {
|
|
|
8007
8321
|
// src/tools/insights.ts
|
|
8008
8322
|
init_supabase();
|
|
8009
8323
|
import { z as z8 } from "zod";
|
|
8324
|
+
init_version();
|
|
8325
|
+
var MAX_INSIGHT_AGE_DAYS = 30;
|
|
8010
8326
|
var PLATFORM_ENUM = [
|
|
8011
8327
|
"youtube",
|
|
8012
8328
|
"tiktok",
|
|
@@ -8017,11 +8333,19 @@ var PLATFORM_ENUM = [
|
|
|
8017
8333
|
"threads",
|
|
8018
8334
|
"bluesky"
|
|
8019
8335
|
];
|
|
8020
|
-
var DAY_NAMES = [
|
|
8336
|
+
var DAY_NAMES = [
|
|
8337
|
+
"Sunday",
|
|
8338
|
+
"Monday",
|
|
8339
|
+
"Tuesday",
|
|
8340
|
+
"Wednesday",
|
|
8341
|
+
"Thursday",
|
|
8342
|
+
"Friday",
|
|
8343
|
+
"Saturday"
|
|
8344
|
+
];
|
|
8021
8345
|
function asEnvelope5(data) {
|
|
8022
8346
|
return {
|
|
8023
8347
|
_meta: {
|
|
8024
|
-
version:
|
|
8348
|
+
version: MCP_VERSION,
|
|
8025
8349
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8026
8350
|
},
|
|
8027
8351
|
data
|
|
@@ -8032,7 +8356,12 @@ function registerInsightsTools(server2) {
|
|
|
8032
8356
|
"get_performance_insights",
|
|
8033
8357
|
"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.",
|
|
8034
8358
|
{
|
|
8035
|
-
insight_type: z8.enum([
|
|
8359
|
+
insight_type: z8.enum([
|
|
8360
|
+
"top_hooks",
|
|
8361
|
+
"optimal_timing",
|
|
8362
|
+
"best_models",
|
|
8363
|
+
"competitor_patterns"
|
|
8364
|
+
]).optional().describe("Filter to a specific insight type."),
|
|
8036
8365
|
days: z8.number().min(1).max(90).optional().describe("Number of days to look back. Defaults to 30. Max 90."),
|
|
8037
8366
|
limit: z8.number().min(1).max(50).optional().describe("Maximum number of insights to return. Defaults to 10."),
|
|
8038
8367
|
response_format: z8.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
@@ -8051,10 +8380,13 @@ function registerInsightsTools(server2) {
|
|
|
8051
8380
|
projectIds.push(...projects.map((p) => p.id));
|
|
8052
8381
|
}
|
|
8053
8382
|
}
|
|
8383
|
+
const effectiveDays = Math.min(lookbackDays, MAX_INSIGHT_AGE_DAYS);
|
|
8054
8384
|
const since = /* @__PURE__ */ new Date();
|
|
8055
|
-
since.setDate(since.getDate() -
|
|
8385
|
+
since.setDate(since.getDate() - effectiveDays);
|
|
8056
8386
|
const sinceIso = since.toISOString();
|
|
8057
|
-
let query = supabase.from("performance_insights").select(
|
|
8387
|
+
let query = supabase.from("performance_insights").select(
|
|
8388
|
+
"id, project_id, insight_type, insight_data, confidence_score, generated_at"
|
|
8389
|
+
).gte("generated_at", sinceIso).order("generated_at", { ascending: false }).limit(maxRows);
|
|
8058
8390
|
if (projectIds.length > 0) {
|
|
8059
8391
|
query = query.in("project_id", projectIds);
|
|
8060
8392
|
} else {
|
|
@@ -8410,10 +8742,11 @@ function registerYouTubeAnalyticsTools(server2) {
|
|
|
8410
8742
|
init_edge_function();
|
|
8411
8743
|
import { z as z10 } from "zod";
|
|
8412
8744
|
init_supabase();
|
|
8745
|
+
init_version();
|
|
8413
8746
|
function asEnvelope6(data) {
|
|
8414
8747
|
return {
|
|
8415
8748
|
_meta: {
|
|
8416
|
-
version:
|
|
8749
|
+
version: MCP_VERSION,
|
|
8417
8750
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8418
8751
|
},
|
|
8419
8752
|
data
|
|
@@ -8424,7 +8757,9 @@ function registerCommentsTools(server2) {
|
|
|
8424
8757
|
"list_comments",
|
|
8425
8758
|
"List YouTube comments. Without a video_id, returns recent comments across all channel videos. With a video_id, returns comments for that specific video.",
|
|
8426
8759
|
{
|
|
8427
|
-
video_id: z10.string().optional().describe(
|
|
8760
|
+
video_id: z10.string().optional().describe(
|
|
8761
|
+
"YouTube video ID. If omitted, returns comments across all channel videos."
|
|
8762
|
+
),
|
|
8428
8763
|
max_results: z10.number().min(1).max(100).optional().describe("Maximum number of comments to return. Defaults to 50."),
|
|
8429
8764
|
page_token: z10.string().optional().describe("Pagination token from a previous list_comments call."),
|
|
8430
8765
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
@@ -8439,7 +8774,9 @@ function registerCommentsTools(server2) {
|
|
|
8439
8774
|
});
|
|
8440
8775
|
if (error) {
|
|
8441
8776
|
return {
|
|
8442
|
-
content: [
|
|
8777
|
+
content: [
|
|
8778
|
+
{ type: "text", text: `Error listing comments: ${error}` }
|
|
8779
|
+
],
|
|
8443
8780
|
isError: true
|
|
8444
8781
|
};
|
|
8445
8782
|
}
|
|
@@ -8451,7 +8788,10 @@ function registerCommentsTools(server2) {
|
|
|
8451
8788
|
{
|
|
8452
8789
|
type: "text",
|
|
8453
8790
|
text: JSON.stringify(
|
|
8454
|
-
asEnvelope6({
|
|
8791
|
+
asEnvelope6({
|
|
8792
|
+
comments,
|
|
8793
|
+
nextPageToken: result.nextPageToken ?? null
|
|
8794
|
+
}),
|
|
8455
8795
|
null,
|
|
8456
8796
|
2
|
|
8457
8797
|
)
|
|
@@ -8488,7 +8828,9 @@ function registerCommentsTools(server2) {
|
|
|
8488
8828
|
"reply_to_comment",
|
|
8489
8829
|
"Reply to a YouTube comment. Requires the parent comment ID and reply text.",
|
|
8490
8830
|
{
|
|
8491
|
-
parent_id: z10.string().describe(
|
|
8831
|
+
parent_id: z10.string().describe(
|
|
8832
|
+
"The ID of the parent comment to reply to (from list_comments)."
|
|
8833
|
+
),
|
|
8492
8834
|
text: z10.string().min(1).describe("The reply text."),
|
|
8493
8835
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8494
8836
|
},
|
|
@@ -8527,7 +8869,12 @@ function registerCommentsTools(server2) {
|
|
|
8527
8869
|
details: { error }
|
|
8528
8870
|
});
|
|
8529
8871
|
return {
|
|
8530
|
-
content: [
|
|
8872
|
+
content: [
|
|
8873
|
+
{
|
|
8874
|
+
type: "text",
|
|
8875
|
+
text: `Error replying to comment: ${error}`
|
|
8876
|
+
}
|
|
8877
|
+
],
|
|
8531
8878
|
isError: true
|
|
8532
8879
|
};
|
|
8533
8880
|
}
|
|
@@ -8540,7 +8887,12 @@ function registerCommentsTools(server2) {
|
|
|
8540
8887
|
});
|
|
8541
8888
|
if (format === "json") {
|
|
8542
8889
|
return {
|
|
8543
|
-
content: [
|
|
8890
|
+
content: [
|
|
8891
|
+
{
|
|
8892
|
+
type: "text",
|
|
8893
|
+
text: JSON.stringify(asEnvelope6(result), null, 2)
|
|
8894
|
+
}
|
|
8895
|
+
]
|
|
8544
8896
|
};
|
|
8545
8897
|
}
|
|
8546
8898
|
return {
|
|
@@ -8598,7 +8950,9 @@ function registerCommentsTools(server2) {
|
|
|
8598
8950
|
details: { error }
|
|
8599
8951
|
});
|
|
8600
8952
|
return {
|
|
8601
|
-
content: [
|
|
8953
|
+
content: [
|
|
8954
|
+
{ type: "text", text: `Error posting comment: ${error}` }
|
|
8955
|
+
],
|
|
8602
8956
|
isError: true
|
|
8603
8957
|
};
|
|
8604
8958
|
}
|
|
@@ -8611,7 +8965,12 @@ function registerCommentsTools(server2) {
|
|
|
8611
8965
|
});
|
|
8612
8966
|
if (format === "json") {
|
|
8613
8967
|
return {
|
|
8614
|
-
content: [
|
|
8968
|
+
content: [
|
|
8969
|
+
{
|
|
8970
|
+
type: "text",
|
|
8971
|
+
text: JSON.stringify(asEnvelope6(result), null, 2)
|
|
8972
|
+
}
|
|
8973
|
+
]
|
|
8615
8974
|
};
|
|
8616
8975
|
}
|
|
8617
8976
|
return {
|
|
@@ -8669,7 +9028,12 @@ function registerCommentsTools(server2) {
|
|
|
8669
9028
|
details: { error }
|
|
8670
9029
|
});
|
|
8671
9030
|
return {
|
|
8672
|
-
content: [
|
|
9031
|
+
content: [
|
|
9032
|
+
{
|
|
9033
|
+
type: "text",
|
|
9034
|
+
text: `Error moderating comment: ${error}`
|
|
9035
|
+
}
|
|
9036
|
+
],
|
|
8673
9037
|
isError: true
|
|
8674
9038
|
};
|
|
8675
9039
|
}
|
|
@@ -8748,7 +9112,9 @@ function registerCommentsTools(server2) {
|
|
|
8748
9112
|
details: { error }
|
|
8749
9113
|
});
|
|
8750
9114
|
return {
|
|
8751
|
-
content: [
|
|
9115
|
+
content: [
|
|
9116
|
+
{ type: "text", text: `Error deleting comment: ${error}` }
|
|
9117
|
+
],
|
|
8752
9118
|
isError: true
|
|
8753
9119
|
};
|
|
8754
9120
|
}
|
|
@@ -8763,13 +9129,22 @@ function registerCommentsTools(server2) {
|
|
|
8763
9129
|
content: [
|
|
8764
9130
|
{
|
|
8765
9131
|
type: "text",
|
|
8766
|
-
text: JSON.stringify(
|
|
9132
|
+
text: JSON.stringify(
|
|
9133
|
+
asEnvelope6({ success: true, commentId: comment_id }),
|
|
9134
|
+
null,
|
|
9135
|
+
2
|
|
9136
|
+
)
|
|
8767
9137
|
}
|
|
8768
9138
|
]
|
|
8769
9139
|
};
|
|
8770
9140
|
}
|
|
8771
9141
|
return {
|
|
8772
|
-
content: [
|
|
9142
|
+
content: [
|
|
9143
|
+
{
|
|
9144
|
+
type: "text",
|
|
9145
|
+
text: `Comment ${comment_id} deleted successfully.`
|
|
9146
|
+
}
|
|
9147
|
+
]
|
|
8773
9148
|
};
|
|
8774
9149
|
}
|
|
8775
9150
|
);
|
|
@@ -8778,6 +9153,7 @@ function registerCommentsTools(server2) {
|
|
|
8778
9153
|
// src/tools/ideation-context.ts
|
|
8779
9154
|
init_supabase();
|
|
8780
9155
|
import { z as z11 } from "zod";
|
|
9156
|
+
init_version();
|
|
8781
9157
|
function transformInsightsToPerformanceContext(projectId, insights) {
|
|
8782
9158
|
if (!insights.length) {
|
|
8783
9159
|
return {
|
|
@@ -8797,8 +9173,12 @@ function transformInsightsToPerformanceContext(projectId, insights) {
|
|
|
8797
9173
|
};
|
|
8798
9174
|
}
|
|
8799
9175
|
const topHooksInsight = insights.find((i) => i.insight_type === "top_hooks");
|
|
8800
|
-
const optimalTimingInsight = insights.find(
|
|
8801
|
-
|
|
9176
|
+
const optimalTimingInsight = insights.find(
|
|
9177
|
+
(i) => i.insight_type === "optimal_timing"
|
|
9178
|
+
);
|
|
9179
|
+
const bestModelsInsight = insights.find(
|
|
9180
|
+
(i) => i.insight_type === "best_models"
|
|
9181
|
+
);
|
|
8802
9182
|
const topHooks = topHooksInsight?.insight_data?.hooks || [];
|
|
8803
9183
|
const hooksSummary = topHooksInsight?.insight_data?.summary || "";
|
|
8804
9184
|
const timingSummary = optimalTimingInsight?.insight_data?.summary || "";
|
|
@@ -8809,7 +9189,10 @@ function transformInsightsToPerformanceContext(projectId, insights) {
|
|
|
8809
9189
|
if (hooksSummary) promptParts.push(hooksSummary);
|
|
8810
9190
|
if (timingSummary) promptParts.push(timingSummary);
|
|
8811
9191
|
if (modelSummary) promptParts.push(modelSummary);
|
|
8812
|
-
if (topHooks.length)
|
|
9192
|
+
if (topHooks.length)
|
|
9193
|
+
promptParts.push(
|
|
9194
|
+
`Top performing hooks: ${topHooks.slice(0, 3).join(", ")}`
|
|
9195
|
+
);
|
|
8813
9196
|
return {
|
|
8814
9197
|
projectId,
|
|
8815
9198
|
hasHistoricalData: true,
|
|
@@ -8835,7 +9218,7 @@ function transformInsightsToPerformanceContext(projectId, insights) {
|
|
|
8835
9218
|
function asEnvelope7(data) {
|
|
8836
9219
|
return {
|
|
8837
9220
|
_meta: {
|
|
8838
|
-
version:
|
|
9221
|
+
version: MCP_VERSION,
|
|
8839
9222
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8840
9223
|
},
|
|
8841
9224
|
data
|
|
@@ -8858,7 +9241,12 @@ function registerIdeationContextTools(server2) {
|
|
|
8858
9241
|
const { data: member } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).limit(1).single();
|
|
8859
9242
|
if (!member?.organization_id) {
|
|
8860
9243
|
return {
|
|
8861
|
-
content: [
|
|
9244
|
+
content: [
|
|
9245
|
+
{
|
|
9246
|
+
type: "text",
|
|
9247
|
+
text: "No organization found for current user."
|
|
9248
|
+
}
|
|
9249
|
+
],
|
|
8862
9250
|
isError: true
|
|
8863
9251
|
};
|
|
8864
9252
|
}
|
|
@@ -8866,7 +9254,10 @@ function registerIdeationContextTools(server2) {
|
|
|
8866
9254
|
if (projectsError) {
|
|
8867
9255
|
return {
|
|
8868
9256
|
content: [
|
|
8869
|
-
{
|
|
9257
|
+
{
|
|
9258
|
+
type: "text",
|
|
9259
|
+
text: `Failed to resolve projects: ${projectsError.message}`
|
|
9260
|
+
}
|
|
8870
9261
|
],
|
|
8871
9262
|
isError: true
|
|
8872
9263
|
};
|
|
@@ -8874,7 +9265,12 @@ function registerIdeationContextTools(server2) {
|
|
|
8874
9265
|
const projectIds = (projects || []).map((p) => p.id);
|
|
8875
9266
|
if (projectIds.length === 0) {
|
|
8876
9267
|
return {
|
|
8877
|
-
content: [
|
|
9268
|
+
content: [
|
|
9269
|
+
{
|
|
9270
|
+
type: "text",
|
|
9271
|
+
text: "No projects found for current user."
|
|
9272
|
+
}
|
|
9273
|
+
],
|
|
8878
9274
|
isError: true
|
|
8879
9275
|
};
|
|
8880
9276
|
}
|
|
@@ -8883,7 +9279,10 @@ function registerIdeationContextTools(server2) {
|
|
|
8883
9279
|
if (!selectedProjectId) {
|
|
8884
9280
|
return {
|
|
8885
9281
|
content: [
|
|
8886
|
-
{
|
|
9282
|
+
{
|
|
9283
|
+
type: "text",
|
|
9284
|
+
text: "No accessible project found for current user."
|
|
9285
|
+
}
|
|
8887
9286
|
],
|
|
8888
9287
|
isError: true
|
|
8889
9288
|
};
|
|
@@ -8901,7 +9300,9 @@ function registerIdeationContextTools(server2) {
|
|
|
8901
9300
|
}
|
|
8902
9301
|
const since = /* @__PURE__ */ new Date();
|
|
8903
9302
|
since.setDate(since.getDate() - lookbackDays);
|
|
8904
|
-
const { data: insights, error } = await supabase.from("performance_insights").select(
|
|
9303
|
+
const { data: insights, error } = await supabase.from("performance_insights").select(
|
|
9304
|
+
"id, project_id, insight_type, insight_data, generated_at, expires_at"
|
|
9305
|
+
).eq("project_id", selectedProjectId).gte("generated_at", since.toISOString()).gt("expires_at", (/* @__PURE__ */ new Date()).toISOString()).order("generated_at", { ascending: false }).limit(30);
|
|
8905
9306
|
if (error) {
|
|
8906
9307
|
return {
|
|
8907
9308
|
content: [
|
|
@@ -8919,7 +9320,12 @@ function registerIdeationContextTools(server2) {
|
|
|
8919
9320
|
);
|
|
8920
9321
|
if (format === "json") {
|
|
8921
9322
|
return {
|
|
8922
|
-
content: [
|
|
9323
|
+
content: [
|
|
9324
|
+
{
|
|
9325
|
+
type: "text",
|
|
9326
|
+
text: JSON.stringify(asEnvelope7(context), null, 2)
|
|
9327
|
+
}
|
|
9328
|
+
]
|
|
8923
9329
|
};
|
|
8924
9330
|
}
|
|
8925
9331
|
const lines = [
|
|
@@ -8940,10 +9346,11 @@ function registerIdeationContextTools(server2) {
|
|
|
8940
9346
|
// src/tools/credits.ts
|
|
8941
9347
|
init_supabase();
|
|
8942
9348
|
import { z as z12 } from "zod";
|
|
9349
|
+
init_version();
|
|
8943
9350
|
function asEnvelope8(data) {
|
|
8944
9351
|
return {
|
|
8945
9352
|
_meta: {
|
|
8946
|
-
version:
|
|
9353
|
+
version: MCP_VERSION,
|
|
8947
9354
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8948
9355
|
},
|
|
8949
9356
|
data
|
|
@@ -8982,7 +9389,12 @@ function registerCreditsTools(server2) {
|
|
|
8982
9389
|
};
|
|
8983
9390
|
if ((response_format || "text") === "json") {
|
|
8984
9391
|
return {
|
|
8985
|
-
content: [
|
|
9392
|
+
content: [
|
|
9393
|
+
{
|
|
9394
|
+
type: "text",
|
|
9395
|
+
text: JSON.stringify(asEnvelope8(payload), null, 2)
|
|
9396
|
+
}
|
|
9397
|
+
]
|
|
8986
9398
|
};
|
|
8987
9399
|
}
|
|
8988
9400
|
return {
|
|
@@ -9016,7 +9428,12 @@ Monthly used: ${payload.monthlyUsed}` + (payload.monthlyLimit ? ` / ${payload.mo
|
|
|
9016
9428
|
};
|
|
9017
9429
|
if ((response_format || "text") === "json") {
|
|
9018
9430
|
return {
|
|
9019
|
-
content: [
|
|
9431
|
+
content: [
|
|
9432
|
+
{
|
|
9433
|
+
type: "text",
|
|
9434
|
+
text: JSON.stringify(asEnvelope8(payload), null, 2)
|
|
9435
|
+
}
|
|
9436
|
+
]
|
|
9020
9437
|
};
|
|
9021
9438
|
}
|
|
9022
9439
|
return {
|
|
@@ -9039,11 +9456,12 @@ Assets remaining: ${payload.remainingAssets ?? "unlimited"}`
|
|
|
9039
9456
|
|
|
9040
9457
|
// src/tools/loop-summary.ts
|
|
9041
9458
|
init_supabase();
|
|
9459
|
+
init_version();
|
|
9042
9460
|
import { z as z13 } from "zod";
|
|
9043
9461
|
function asEnvelope9(data) {
|
|
9044
9462
|
return {
|
|
9045
9463
|
_meta: {
|
|
9046
|
-
version:
|
|
9464
|
+
version: MCP_VERSION,
|
|
9047
9465
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9048
9466
|
},
|
|
9049
9467
|
data
|
|
@@ -9082,7 +9500,12 @@ function registerLoopSummaryTools(server2) {
|
|
|
9082
9500
|
const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
|
|
9083
9501
|
if (!membership) {
|
|
9084
9502
|
return {
|
|
9085
|
-
content: [
|
|
9503
|
+
content: [
|
|
9504
|
+
{
|
|
9505
|
+
type: "text",
|
|
9506
|
+
text: "Project is not accessible to current user."
|
|
9507
|
+
}
|
|
9508
|
+
],
|
|
9086
9509
|
isError: true
|
|
9087
9510
|
};
|
|
9088
9511
|
}
|
|
@@ -9105,7 +9528,12 @@ function registerLoopSummaryTools(server2) {
|
|
|
9105
9528
|
};
|
|
9106
9529
|
if ((response_format || "text") === "json") {
|
|
9107
9530
|
return {
|
|
9108
|
-
content: [
|
|
9531
|
+
content: [
|
|
9532
|
+
{
|
|
9533
|
+
type: "text",
|
|
9534
|
+
text: JSON.stringify(asEnvelope9(payload), null, 2)
|
|
9535
|
+
}
|
|
9536
|
+
]
|
|
9109
9537
|
};
|
|
9110
9538
|
}
|
|
9111
9539
|
return {
|
|
@@ -9435,8 +9863,12 @@ ${"=".repeat(40)}
|
|
|
9435
9863
|
init_edge_function();
|
|
9436
9864
|
init_supabase();
|
|
9437
9865
|
import { z as z16 } from "zod";
|
|
9866
|
+
init_version();
|
|
9438
9867
|
function asEnvelope12(data) {
|
|
9439
|
-
return {
|
|
9868
|
+
return {
|
|
9869
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
9870
|
+
data
|
|
9871
|
+
};
|
|
9440
9872
|
}
|
|
9441
9873
|
function isYouTubeUrl(url) {
|
|
9442
9874
|
if (/youtube\.com\/watch|youtu\.be\//.test(url)) return "video";
|
|
@@ -9467,13 +9899,17 @@ Metadata:`);
|
|
|
9467
9899
|
if (m.tags?.length) lines.push(` Tags: ${m.tags.join(", ")}`);
|
|
9468
9900
|
}
|
|
9469
9901
|
if (content.features?.length)
|
|
9470
|
-
lines.push(
|
|
9902
|
+
lines.push(
|
|
9903
|
+
`
|
|
9471
9904
|
Features:
|
|
9472
|
-
${content.features.map((f) => ` - ${f}`).join("\n")}`
|
|
9905
|
+
${content.features.map((f) => ` - ${f}`).join("\n")}`
|
|
9906
|
+
);
|
|
9473
9907
|
if (content.benefits?.length)
|
|
9474
|
-
lines.push(
|
|
9908
|
+
lines.push(
|
|
9909
|
+
`
|
|
9475
9910
|
Benefits:
|
|
9476
|
-
${content.benefits.map((b) => ` - ${b}`).join("\n")}`
|
|
9911
|
+
${content.benefits.map((b) => ` - ${b}`).join("\n")}`
|
|
9912
|
+
);
|
|
9477
9913
|
if (content.usp) lines.push(`
|
|
9478
9914
|
USP: ${content.usp}`);
|
|
9479
9915
|
if (content.suggested_hooks?.length)
|
|
@@ -9495,12 +9931,20 @@ function registerExtractionTools(server2) {
|
|
|
9495
9931
|
max_results: z16.number().min(1).max(100).default(10).describe("Max comments to include"),
|
|
9496
9932
|
response_format: z16.enum(["text", "json"]).default("text")
|
|
9497
9933
|
},
|
|
9498
|
-
async ({
|
|
9934
|
+
async ({
|
|
9935
|
+
url,
|
|
9936
|
+
extract_type,
|
|
9937
|
+
include_comments,
|
|
9938
|
+
max_results,
|
|
9939
|
+
response_format
|
|
9940
|
+
}) => {
|
|
9499
9941
|
const startedAt = Date.now();
|
|
9500
9942
|
const ssrfCheck = await validateUrlForSSRF(url);
|
|
9501
9943
|
if (!ssrfCheck.isValid) {
|
|
9502
9944
|
return {
|
|
9503
|
-
content: [
|
|
9945
|
+
content: [
|
|
9946
|
+
{ type: "text", text: `URL blocked: ${ssrfCheck.error}` }
|
|
9947
|
+
],
|
|
9504
9948
|
isError: true
|
|
9505
9949
|
};
|
|
9506
9950
|
}
|
|
@@ -9628,13 +10072,21 @@ function registerExtractionTools(server2) {
|
|
|
9628
10072
|
if (response_format === "json") {
|
|
9629
10073
|
return {
|
|
9630
10074
|
content: [
|
|
9631
|
-
{
|
|
10075
|
+
{
|
|
10076
|
+
type: "text",
|
|
10077
|
+
text: JSON.stringify(asEnvelope12(extracted), null, 2)
|
|
10078
|
+
}
|
|
9632
10079
|
],
|
|
9633
10080
|
isError: false
|
|
9634
10081
|
};
|
|
9635
10082
|
}
|
|
9636
10083
|
return {
|
|
9637
|
-
content: [
|
|
10084
|
+
content: [
|
|
10085
|
+
{
|
|
10086
|
+
type: "text",
|
|
10087
|
+
text: formatExtractedContentAsText(extracted)
|
|
10088
|
+
}
|
|
10089
|
+
],
|
|
9638
10090
|
isError: false
|
|
9639
10091
|
};
|
|
9640
10092
|
} catch (err) {
|
|
@@ -9647,7 +10099,9 @@ function registerExtractionTools(server2) {
|
|
|
9647
10099
|
details: { url, error: message }
|
|
9648
10100
|
});
|
|
9649
10101
|
return {
|
|
9650
|
-
content: [
|
|
10102
|
+
content: [
|
|
10103
|
+
{ type: "text", text: `Extraction failed: ${message}` }
|
|
10104
|
+
],
|
|
9651
10105
|
isError: true
|
|
9652
10106
|
};
|
|
9653
10107
|
}
|
|
@@ -9658,9 +10112,13 @@ function registerExtractionTools(server2) {
|
|
|
9658
10112
|
// src/tools/quality.ts
|
|
9659
10113
|
init_quality();
|
|
9660
10114
|
init_supabase();
|
|
10115
|
+
init_version();
|
|
9661
10116
|
import { z as z17 } from "zod";
|
|
9662
10117
|
function asEnvelope13(data) {
|
|
9663
|
-
return {
|
|
10118
|
+
return {
|
|
10119
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
10120
|
+
data
|
|
10121
|
+
};
|
|
9664
10122
|
}
|
|
9665
10123
|
function registerQualityTools(server2) {
|
|
9666
10124
|
server2.tool(
|
|
@@ -9716,7 +10174,12 @@ function registerQualityTools(server2) {
|
|
|
9716
10174
|
});
|
|
9717
10175
|
if (response_format === "json") {
|
|
9718
10176
|
return {
|
|
9719
|
-
content: [
|
|
10177
|
+
content: [
|
|
10178
|
+
{
|
|
10179
|
+
type: "text",
|
|
10180
|
+
text: JSON.stringify(asEnvelope13(result), null, 2)
|
|
10181
|
+
}
|
|
10182
|
+
],
|
|
9720
10183
|
isError: false
|
|
9721
10184
|
};
|
|
9722
10185
|
}
|
|
@@ -9726,7 +10189,9 @@ function registerQualityTools(server2) {
|
|
|
9726
10189
|
);
|
|
9727
10190
|
lines.push("");
|
|
9728
10191
|
for (const cat of result.categories) {
|
|
9729
|
-
lines.push(
|
|
10192
|
+
lines.push(
|
|
10193
|
+
` ${cat.name}: ${cat.score}/${cat.maxScore} \u2014 ${cat.detail}`
|
|
10194
|
+
);
|
|
9730
10195
|
}
|
|
9731
10196
|
if (result.blockers.length > 0) {
|
|
9732
10197
|
lines.push("");
|
|
@@ -9737,7 +10202,10 @@ function registerQualityTools(server2) {
|
|
|
9737
10202
|
}
|
|
9738
10203
|
lines.push("");
|
|
9739
10204
|
lines.push(`Threshold: ${result.threshold}/${result.maxTotal}`);
|
|
9740
|
-
return {
|
|
10205
|
+
return {
|
|
10206
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
10207
|
+
isError: false
|
|
10208
|
+
};
|
|
9741
10209
|
}
|
|
9742
10210
|
);
|
|
9743
10211
|
server2.tool(
|
|
@@ -9778,7 +10246,9 @@ function registerQualityTools(server2) {
|
|
|
9778
10246
|
});
|
|
9779
10247
|
const scores = postsWithQuality.map((p) => p.quality.score);
|
|
9780
10248
|
const passed = postsWithQuality.filter((p) => p.quality.passed).length;
|
|
9781
|
-
const avgScore = scores.length > 0 ? Math.round(
|
|
10249
|
+
const avgScore = scores.length > 0 ? Math.round(
|
|
10250
|
+
scores.reduce((a, b) => a + b, 0) / scores.length * 10
|
|
10251
|
+
) / 10 : 0;
|
|
9782
10252
|
const summary = {
|
|
9783
10253
|
total_posts: plan.posts.length,
|
|
9784
10254
|
passed,
|
|
@@ -9797,25 +10267,36 @@ function registerQualityTools(server2) {
|
|
|
9797
10267
|
content: [
|
|
9798
10268
|
{
|
|
9799
10269
|
type: "text",
|
|
9800
|
-
text: JSON.stringify(
|
|
10270
|
+
text: JSON.stringify(
|
|
10271
|
+
asEnvelope13({ posts: postsWithQuality, summary }),
|
|
10272
|
+
null,
|
|
10273
|
+
2
|
|
10274
|
+
)
|
|
9801
10275
|
}
|
|
9802
10276
|
],
|
|
9803
10277
|
isError: false
|
|
9804
10278
|
};
|
|
9805
10279
|
}
|
|
9806
10280
|
const lines = [];
|
|
9807
|
-
lines.push(
|
|
10281
|
+
lines.push(
|
|
10282
|
+
`PLAN QUALITY: ${passed}/${plan.posts.length} passed (avg: ${avgScore}/35)`
|
|
10283
|
+
);
|
|
9808
10284
|
lines.push("");
|
|
9809
10285
|
for (const post of postsWithQuality) {
|
|
9810
10286
|
const icon = post.quality.passed ? "[PASS]" : "[FAIL]";
|
|
9811
|
-
lines.push(
|
|
10287
|
+
lines.push(
|
|
10288
|
+
`${icon} ${post.id} | ${post.platform} | ${post.quality.score}/35`
|
|
10289
|
+
);
|
|
9812
10290
|
if (post.quality.blockers.length > 0) {
|
|
9813
10291
|
for (const b of post.quality.blockers) {
|
|
9814
10292
|
lines.push(` - ${b}`);
|
|
9815
10293
|
}
|
|
9816
10294
|
}
|
|
9817
10295
|
}
|
|
9818
|
-
return {
|
|
10296
|
+
return {
|
|
10297
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
10298
|
+
isError: false
|
|
10299
|
+
};
|
|
9819
10300
|
}
|
|
9820
10301
|
);
|
|
9821
10302
|
}
|
|
@@ -9825,11 +10306,15 @@ init_edge_function();
|
|
|
9825
10306
|
init_supabase();
|
|
9826
10307
|
import { z as z18 } from "zod";
|
|
9827
10308
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10309
|
+
init_version();
|
|
9828
10310
|
function toRecord(value) {
|
|
9829
10311
|
return value && typeof value === "object" ? value : void 0;
|
|
9830
10312
|
}
|
|
9831
10313
|
function asEnvelope14(data) {
|
|
9832
|
-
return {
|
|
10314
|
+
return {
|
|
10315
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
10316
|
+
data
|
|
10317
|
+
};
|
|
9833
10318
|
}
|
|
9834
10319
|
function tomorrowIsoDate() {
|
|
9835
10320
|
const d = /* @__PURE__ */ new Date();
|
|
@@ -9867,7 +10352,9 @@ function formatPlanAsText(plan) {
|
|
|
9867
10352
|
lines.push(`WEEKLY CONTENT PLAN: "${plan.topic}"`);
|
|
9868
10353
|
lines.push(`Period: ${plan.start_date} to ${plan.end_date}`);
|
|
9869
10354
|
lines.push(`Platforms: ${plan.platforms.join(", ")}`);
|
|
9870
|
-
lines.push(
|
|
10355
|
+
lines.push(
|
|
10356
|
+
`Posts: ${plan.posts.length} | Estimated credits: ~${plan.estimated_credits}`
|
|
10357
|
+
);
|
|
9871
10358
|
if (plan.plan_id) lines.push(`Plan ID: ${plan.plan_id}`);
|
|
9872
10359
|
if (plan.insights_applied?.has_historical_data) {
|
|
9873
10360
|
lines.push("");
|
|
@@ -9882,7 +10369,9 @@ function formatPlanAsText(plan) {
|
|
|
9882
10369
|
`- Best posting time: ${days[timing.dayOfWeek] ?? timing.dayOfWeek} ${timing.hourOfDay}:00`
|
|
9883
10370
|
);
|
|
9884
10371
|
}
|
|
9885
|
-
lines.push(
|
|
10372
|
+
lines.push(
|
|
10373
|
+
`- Recommended model: ${plan.insights_applied.recommended_model ?? "N/A"}`
|
|
10374
|
+
);
|
|
9886
10375
|
lines.push(`- Insights count: ${plan.insights_applied.insights_count}`);
|
|
9887
10376
|
}
|
|
9888
10377
|
lines.push("");
|
|
@@ -9903,9 +10392,11 @@ function formatPlanAsText(plan) {
|
|
|
9903
10392
|
` Caption: ${post.caption.slice(0, 200)}${post.caption.length > 200 ? "..." : ""}`
|
|
9904
10393
|
);
|
|
9905
10394
|
if (post.title) lines.push(` Title: ${post.title}`);
|
|
9906
|
-
if (post.visual_direction)
|
|
10395
|
+
if (post.visual_direction)
|
|
10396
|
+
lines.push(` Visual: ${post.visual_direction}`);
|
|
9907
10397
|
if (post.media_type) lines.push(` Media: ${post.media_type}`);
|
|
9908
|
-
if (post.hashtags?.length)
|
|
10398
|
+
if (post.hashtags?.length)
|
|
10399
|
+
lines.push(` Hashtags: ${post.hashtags.join(" ")}`);
|
|
9909
10400
|
lines.push("");
|
|
9910
10401
|
}
|
|
9911
10402
|
}
|
|
@@ -9988,7 +10479,10 @@ function registerPlanningTools(server2) {
|
|
|
9988
10479
|
"mcp-data",
|
|
9989
10480
|
{
|
|
9990
10481
|
action: "brand-profile",
|
|
9991
|
-
...resolvedProjectId ? {
|
|
10482
|
+
...resolvedProjectId ? {
|
|
10483
|
+
projectId: resolvedProjectId,
|
|
10484
|
+
project_id: resolvedProjectId
|
|
10485
|
+
} : {}
|
|
9992
10486
|
},
|
|
9993
10487
|
{ timeoutMs: 15e3 }
|
|
9994
10488
|
);
|
|
@@ -10192,7 +10686,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10192
10686
|
details: { topic, error: `plan persistence failed: ${message}` }
|
|
10193
10687
|
});
|
|
10194
10688
|
return {
|
|
10195
|
-
content: [
|
|
10689
|
+
content: [
|
|
10690
|
+
{
|
|
10691
|
+
type: "text",
|
|
10692
|
+
text: `Plan persistence failed: ${message}`
|
|
10693
|
+
}
|
|
10694
|
+
],
|
|
10196
10695
|
isError: true
|
|
10197
10696
|
};
|
|
10198
10697
|
}
|
|
@@ -10206,7 +10705,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10206
10705
|
});
|
|
10207
10706
|
if (response_format === "json") {
|
|
10208
10707
|
return {
|
|
10209
|
-
content: [
|
|
10708
|
+
content: [
|
|
10709
|
+
{
|
|
10710
|
+
type: "text",
|
|
10711
|
+
text: JSON.stringify(asEnvelope14(plan), null, 2)
|
|
10712
|
+
}
|
|
10713
|
+
],
|
|
10210
10714
|
isError: false
|
|
10211
10715
|
};
|
|
10212
10716
|
}
|
|
@@ -10224,7 +10728,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10224
10728
|
details: { topic, error: message }
|
|
10225
10729
|
});
|
|
10226
10730
|
return {
|
|
10227
|
-
content: [
|
|
10731
|
+
content: [
|
|
10732
|
+
{
|
|
10733
|
+
type: "text",
|
|
10734
|
+
text: `Plan generation failed: ${message}`
|
|
10735
|
+
}
|
|
10736
|
+
],
|
|
10228
10737
|
isError: true
|
|
10229
10738
|
};
|
|
10230
10739
|
}
|
|
@@ -10284,7 +10793,11 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10284
10793
|
toolName: "save_content_plan",
|
|
10285
10794
|
status: "success",
|
|
10286
10795
|
durationMs,
|
|
10287
|
-
details: {
|
|
10796
|
+
details: {
|
|
10797
|
+
plan_id: planId,
|
|
10798
|
+
project_id: resolvedProjectId,
|
|
10799
|
+
status: normalizedStatus
|
|
10800
|
+
}
|
|
10288
10801
|
});
|
|
10289
10802
|
const result = {
|
|
10290
10803
|
plan_id: planId,
|
|
@@ -10293,13 +10806,21 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10293
10806
|
};
|
|
10294
10807
|
if (response_format === "json") {
|
|
10295
10808
|
return {
|
|
10296
|
-
content: [
|
|
10809
|
+
content: [
|
|
10810
|
+
{
|
|
10811
|
+
type: "text",
|
|
10812
|
+
text: JSON.stringify(asEnvelope14(result), null, 2)
|
|
10813
|
+
}
|
|
10814
|
+
],
|
|
10297
10815
|
isError: false
|
|
10298
10816
|
};
|
|
10299
10817
|
}
|
|
10300
10818
|
return {
|
|
10301
10819
|
content: [
|
|
10302
|
-
{
|
|
10820
|
+
{
|
|
10821
|
+
type: "text",
|
|
10822
|
+
text: `Saved content plan ${planId} (${normalizedStatus}).`
|
|
10823
|
+
}
|
|
10303
10824
|
],
|
|
10304
10825
|
isError: false
|
|
10305
10826
|
};
|
|
@@ -10313,7 +10834,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10313
10834
|
details: { error: message }
|
|
10314
10835
|
});
|
|
10315
10836
|
return {
|
|
10316
|
-
content: [
|
|
10837
|
+
content: [
|
|
10838
|
+
{
|
|
10839
|
+
type: "text",
|
|
10840
|
+
text: `Failed to save content plan: ${message}`
|
|
10841
|
+
}
|
|
10842
|
+
],
|
|
10317
10843
|
isError: true
|
|
10318
10844
|
};
|
|
10319
10845
|
}
|
|
@@ -10329,7 +10855,9 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10329
10855
|
async ({ plan_id, response_format }) => {
|
|
10330
10856
|
const supabase = getSupabaseClient();
|
|
10331
10857
|
const userId = await getDefaultUserId();
|
|
10332
|
-
const { data, error } = await supabase.from("content_plans").select(
|
|
10858
|
+
const { data, error } = await supabase.from("content_plans").select(
|
|
10859
|
+
"id, topic, status, plan_payload, insights_applied, created_at, updated_at"
|
|
10860
|
+
).eq("id", plan_id).eq("user_id", userId).maybeSingle();
|
|
10333
10861
|
if (error) {
|
|
10334
10862
|
return {
|
|
10335
10863
|
content: [
|
|
@@ -10344,7 +10872,10 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10344
10872
|
if (!data) {
|
|
10345
10873
|
return {
|
|
10346
10874
|
content: [
|
|
10347
|
-
{
|
|
10875
|
+
{
|
|
10876
|
+
type: "text",
|
|
10877
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
10878
|
+
}
|
|
10348
10879
|
],
|
|
10349
10880
|
isError: true
|
|
10350
10881
|
};
|
|
@@ -10360,7 +10891,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10360
10891
|
};
|
|
10361
10892
|
if (response_format === "json") {
|
|
10362
10893
|
return {
|
|
10363
|
-
content: [
|
|
10894
|
+
content: [
|
|
10895
|
+
{
|
|
10896
|
+
type: "text",
|
|
10897
|
+
text: JSON.stringify(asEnvelope14(payload), null, 2)
|
|
10898
|
+
}
|
|
10899
|
+
],
|
|
10364
10900
|
isError: false
|
|
10365
10901
|
};
|
|
10366
10902
|
}
|
|
@@ -10371,7 +10907,10 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10371
10907
|
`Status: ${data.status}`,
|
|
10372
10908
|
`Posts: ${Array.isArray(plan?.posts) ? plan.posts.length : 0}`
|
|
10373
10909
|
];
|
|
10374
|
-
return {
|
|
10910
|
+
return {
|
|
10911
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
10912
|
+
isError: false
|
|
10913
|
+
};
|
|
10375
10914
|
}
|
|
10376
10915
|
);
|
|
10377
10916
|
server2.tool(
|
|
@@ -10414,14 +10953,19 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10414
10953
|
if (!stored?.plan_payload) {
|
|
10415
10954
|
return {
|
|
10416
10955
|
content: [
|
|
10417
|
-
{
|
|
10956
|
+
{
|
|
10957
|
+
type: "text",
|
|
10958
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
10959
|
+
}
|
|
10418
10960
|
],
|
|
10419
10961
|
isError: true
|
|
10420
10962
|
};
|
|
10421
10963
|
}
|
|
10422
10964
|
const plan = stored.plan_payload;
|
|
10423
10965
|
const existingPosts = Array.isArray(plan.posts) ? plan.posts : [];
|
|
10424
|
-
const updatesById = new Map(
|
|
10966
|
+
const updatesById = new Map(
|
|
10967
|
+
post_updates.map((update) => [update.post_id, update])
|
|
10968
|
+
);
|
|
10425
10969
|
const updatedPosts = existingPosts.map((post) => {
|
|
10426
10970
|
const update = updatesById.get(post.id);
|
|
10427
10971
|
if (!update) return post;
|
|
@@ -10439,7 +10983,9 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10439
10983
|
...update.status !== void 0 ? { status: update.status } : {}
|
|
10440
10984
|
};
|
|
10441
10985
|
});
|
|
10442
|
-
const nextStatus = updatedPosts.length > 0 && updatedPosts.every(
|
|
10986
|
+
const nextStatus = updatedPosts.length > 0 && updatedPosts.every(
|
|
10987
|
+
(post) => post.status === "approved" || post.status === "edited"
|
|
10988
|
+
) ? "approved" : stored.status === "scheduled" || stored.status === "completed" ? stored.status : "draft";
|
|
10443
10989
|
const updatedPlan = {
|
|
10444
10990
|
...plan,
|
|
10445
10991
|
posts: updatedPosts
|
|
@@ -10466,7 +11012,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10466
11012
|
};
|
|
10467
11013
|
if (response_format === "json") {
|
|
10468
11014
|
return {
|
|
10469
|
-
content: [
|
|
11015
|
+
content: [
|
|
11016
|
+
{
|
|
11017
|
+
type: "text",
|
|
11018
|
+
text: JSON.stringify(asEnvelope14(payload), null, 2)
|
|
11019
|
+
}
|
|
11020
|
+
],
|
|
10470
11021
|
isError: false
|
|
10471
11022
|
};
|
|
10472
11023
|
}
|
|
@@ -10506,7 +11057,10 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10506
11057
|
if (!stored?.plan_payload || !stored.project_id) {
|
|
10507
11058
|
return {
|
|
10508
11059
|
content: [
|
|
10509
|
-
{
|
|
11060
|
+
{
|
|
11061
|
+
type: "text",
|
|
11062
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
11063
|
+
}
|
|
10510
11064
|
],
|
|
10511
11065
|
isError: true
|
|
10512
11066
|
};
|
|
@@ -10515,7 +11069,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10515
11069
|
const posts = Array.isArray(plan.posts) ? plan.posts : [];
|
|
10516
11070
|
if (posts.length === 0) {
|
|
10517
11071
|
return {
|
|
10518
|
-
content: [
|
|
11072
|
+
content: [
|
|
11073
|
+
{
|
|
11074
|
+
type: "text",
|
|
11075
|
+
text: `Plan ${plan_id} has no posts to submit.`
|
|
11076
|
+
}
|
|
11077
|
+
],
|
|
10519
11078
|
isError: true
|
|
10520
11079
|
};
|
|
10521
11080
|
}
|
|
@@ -10558,7 +11117,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10558
11117
|
};
|
|
10559
11118
|
if (response_format === "json") {
|
|
10560
11119
|
return {
|
|
10561
|
-
content: [
|
|
11120
|
+
content: [
|
|
11121
|
+
{
|
|
11122
|
+
type: "text",
|
|
11123
|
+
text: JSON.stringify(asEnvelope14(payload), null, 2)
|
|
11124
|
+
}
|
|
11125
|
+
],
|
|
10562
11126
|
isError: false
|
|
10563
11127
|
};
|
|
10564
11128
|
}
|
|
@@ -10578,10 +11142,11 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10578
11142
|
// src/tools/plan-approvals.ts
|
|
10579
11143
|
init_supabase();
|
|
10580
11144
|
import { z as z19 } from "zod";
|
|
11145
|
+
init_version();
|
|
10581
11146
|
function asEnvelope15(data) {
|
|
10582
11147
|
return {
|
|
10583
11148
|
_meta: {
|
|
10584
|
-
version:
|
|
11149
|
+
version: MCP_VERSION,
|
|
10585
11150
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10586
11151
|
},
|
|
10587
11152
|
data
|
|
@@ -10620,14 +11185,24 @@ function registerPlanApprovalTools(server2) {
|
|
|
10620
11185
|
if (!projectId) {
|
|
10621
11186
|
return {
|
|
10622
11187
|
content: [
|
|
10623
|
-
{
|
|
11188
|
+
{
|
|
11189
|
+
type: "text",
|
|
11190
|
+
text: "No project_id provided and no default project found."
|
|
11191
|
+
}
|
|
10624
11192
|
],
|
|
10625
11193
|
isError: true
|
|
10626
11194
|
};
|
|
10627
11195
|
}
|
|
10628
|
-
const accessError = await assertProjectAccess(
|
|
11196
|
+
const accessError = await assertProjectAccess(
|
|
11197
|
+
supabase,
|
|
11198
|
+
userId,
|
|
11199
|
+
projectId
|
|
11200
|
+
);
|
|
10629
11201
|
if (accessError) {
|
|
10630
|
-
return {
|
|
11202
|
+
return {
|
|
11203
|
+
content: [{ type: "text", text: accessError }],
|
|
11204
|
+
isError: true
|
|
11205
|
+
};
|
|
10631
11206
|
}
|
|
10632
11207
|
const rows = posts.map((post) => ({
|
|
10633
11208
|
plan_id,
|
|
@@ -10656,7 +11231,12 @@ function registerPlanApprovalTools(server2) {
|
|
|
10656
11231
|
};
|
|
10657
11232
|
if ((response_format || "text") === "json") {
|
|
10658
11233
|
return {
|
|
10659
|
-
content: [
|
|
11234
|
+
content: [
|
|
11235
|
+
{
|
|
11236
|
+
type: "text",
|
|
11237
|
+
text: JSON.stringify(asEnvelope15(payload), null, 2)
|
|
11238
|
+
}
|
|
11239
|
+
],
|
|
10660
11240
|
isError: false
|
|
10661
11241
|
};
|
|
10662
11242
|
}
|
|
@@ -10705,14 +11285,22 @@ function registerPlanApprovalTools(server2) {
|
|
|
10705
11285
|
};
|
|
10706
11286
|
if ((response_format || "text") === "json") {
|
|
10707
11287
|
return {
|
|
10708
|
-
content: [
|
|
11288
|
+
content: [
|
|
11289
|
+
{
|
|
11290
|
+
type: "text",
|
|
11291
|
+
text: JSON.stringify(asEnvelope15(payload), null, 2)
|
|
11292
|
+
}
|
|
11293
|
+
],
|
|
10709
11294
|
isError: false
|
|
10710
11295
|
};
|
|
10711
11296
|
}
|
|
10712
11297
|
if (!data || data.length === 0) {
|
|
10713
11298
|
return {
|
|
10714
11299
|
content: [
|
|
10715
|
-
{
|
|
11300
|
+
{
|
|
11301
|
+
type: "text",
|
|
11302
|
+
text: `No approval items found for plan ${plan_id}.`
|
|
11303
|
+
}
|
|
10716
11304
|
],
|
|
10717
11305
|
isError: false
|
|
10718
11306
|
};
|
|
@@ -10725,7 +11313,10 @@ function registerPlanApprovalTools(server2) {
|
|
|
10725
11313
|
}
|
|
10726
11314
|
lines.push("");
|
|
10727
11315
|
lines.push(`Total: ${data.length}`);
|
|
10728
|
-
return {
|
|
11316
|
+
return {
|
|
11317
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
11318
|
+
isError: false
|
|
11319
|
+
};
|
|
10729
11320
|
}
|
|
10730
11321
|
);
|
|
10731
11322
|
server2.tool(
|
|
@@ -10744,7 +11335,10 @@ function registerPlanApprovalTools(server2) {
|
|
|
10744
11335
|
if (decision === "edited" && !edited_post) {
|
|
10745
11336
|
return {
|
|
10746
11337
|
content: [
|
|
10747
|
-
{
|
|
11338
|
+
{
|
|
11339
|
+
type: "text",
|
|
11340
|
+
text: 'edited_post is required when decision is "edited".'
|
|
11341
|
+
}
|
|
10748
11342
|
],
|
|
10749
11343
|
isError: true
|
|
10750
11344
|
};
|
|
@@ -10757,7 +11351,9 @@ function registerPlanApprovalTools(server2) {
|
|
|
10757
11351
|
if (decision === "edited") {
|
|
10758
11352
|
updates.edited_post = edited_post;
|
|
10759
11353
|
}
|
|
10760
|
-
const { data, error } = await supabase.from("content_plan_approvals").update(updates).eq("id", approval_id).eq("user_id", userId).eq("status", "pending").select(
|
|
11354
|
+
const { data, error } = await supabase.from("content_plan_approvals").update(updates).eq("id", approval_id).eq("user_id", userId).eq("status", "pending").select(
|
|
11355
|
+
"id, plan_id, post_id, status, reason, decided_at, original_post, edited_post"
|
|
11356
|
+
).maybeSingle();
|
|
10761
11357
|
if (error) {
|
|
10762
11358
|
return {
|
|
10763
11359
|
content: [
|
|
@@ -10782,7 +11378,12 @@ function registerPlanApprovalTools(server2) {
|
|
|
10782
11378
|
}
|
|
10783
11379
|
if ((response_format || "text") === "json") {
|
|
10784
11380
|
return {
|
|
10785
|
-
content: [
|
|
11381
|
+
content: [
|
|
11382
|
+
{
|
|
11383
|
+
type: "text",
|
|
11384
|
+
text: JSON.stringify(asEnvelope15(data), null, 2)
|
|
11385
|
+
}
|
|
11386
|
+
],
|
|
10786
11387
|
isError: false
|
|
10787
11388
|
};
|
|
10788
11389
|
}
|
|
@@ -10922,16 +11523,40 @@ function registerAllTools(server2, options) {
|
|
|
10922
11523
|
init_posthog();
|
|
10923
11524
|
init_supabase();
|
|
10924
11525
|
init_sn();
|
|
11526
|
+
function flushAndExit(code) {
|
|
11527
|
+
const done = { out: false, err: false };
|
|
11528
|
+
const tryExit = () => {
|
|
11529
|
+
if (done.out && done.err) process.exit(code);
|
|
11530
|
+
};
|
|
11531
|
+
if (process.stdout.writableFinished) {
|
|
11532
|
+
done.out = true;
|
|
11533
|
+
} else {
|
|
11534
|
+
process.stdout.end(() => {
|
|
11535
|
+
done.out = true;
|
|
11536
|
+
tryExit();
|
|
11537
|
+
});
|
|
11538
|
+
}
|
|
11539
|
+
if (process.stderr.writableFinished) {
|
|
11540
|
+
done.err = true;
|
|
11541
|
+
} else {
|
|
11542
|
+
process.stderr.end(() => {
|
|
11543
|
+
done.err = true;
|
|
11544
|
+
tryExit();
|
|
11545
|
+
});
|
|
11546
|
+
}
|
|
11547
|
+
tryExit();
|
|
11548
|
+
setTimeout(() => process.exit(code), 2e3).unref();
|
|
11549
|
+
}
|
|
10925
11550
|
process.on("uncaughtException", (err) => {
|
|
10926
11551
|
process.stderr.write(`MCP server error: ${err.message}
|
|
10927
11552
|
`);
|
|
10928
|
-
|
|
11553
|
+
flushAndExit(1);
|
|
10929
11554
|
});
|
|
10930
11555
|
process.on("unhandledRejection", (reason) => {
|
|
10931
11556
|
const message = reason instanceof Error ? reason.message : String(reason);
|
|
10932
11557
|
process.stderr.write(`MCP server error: ${message}
|
|
10933
11558
|
`);
|
|
10934
|
-
|
|
11559
|
+
flushAndExit(1);
|
|
10935
11560
|
});
|
|
10936
11561
|
var command = process.argv[2];
|
|
10937
11562
|
if (command === "--version" || command === "-v") {
|
|
@@ -10940,7 +11565,11 @@ if (command === "--version" || command === "-v") {
|
|
|
10940
11565
|
const { fileURLToPath } = await import("node:url");
|
|
10941
11566
|
let version = MCP_VERSION;
|
|
10942
11567
|
try {
|
|
10943
|
-
const pkgPath = resolve3(
|
|
11568
|
+
const pkgPath = resolve3(
|
|
11569
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
11570
|
+
"..",
|
|
11571
|
+
"package.json"
|
|
11572
|
+
);
|
|
10944
11573
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
|
|
10945
11574
|
version = pkg.version;
|
|
10946
11575
|
} catch {
|
|
@@ -10972,7 +11601,11 @@ if (command === "--help" || command === "-h") {
|
|
|
10972
11601
|
ok: true,
|
|
10973
11602
|
command: "help",
|
|
10974
11603
|
commands: [
|
|
10975
|
-
{
|
|
11604
|
+
{
|
|
11605
|
+
name: "setup",
|
|
11606
|
+
aliases: ["login"],
|
|
11607
|
+
description: "Interactive OAuth setup"
|
|
11608
|
+
},
|
|
10976
11609
|
{ name: "logout", description: "Remove credentials" },
|
|
10977
11610
|
{ name: "whoami", description: "Show auth info" },
|
|
10978
11611
|
{ name: "health", description: "Check connectivity" },
|
|
@@ -11061,10 +11694,14 @@ if (command === "sn") {
|
|
|
11061
11694
|
await runSnCli(process.argv.slice(3));
|
|
11062
11695
|
process.exit(0);
|
|
11063
11696
|
}
|
|
11064
|
-
if (command && !["setup", "login", "logout", "whoami", "health", "sn", "repl"].includes(
|
|
11065
|
-
|
|
11697
|
+
if (command && !["setup", "login", "logout", "whoami", "health", "sn", "repl"].includes(
|
|
11698
|
+
command
|
|
11699
|
+
)) {
|
|
11700
|
+
process.stderr.write(
|
|
11701
|
+
`Unknown command: ${command}
|
|
11066
11702
|
Run socialneuron-mcp --help for usage.
|
|
11067
|
-
`
|
|
11703
|
+
`
|
|
11704
|
+
);
|
|
11068
11705
|
process.exit(1);
|
|
11069
11706
|
}
|
|
11070
11707
|
await initializeAuth();
|