@socialneuron/mcp-server 1.3.2 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +13 -4
- package/dist/http.js +1234 -527
- package/dist/index.js +1074 -429
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -14,22 +14,30 @@ var MCP_VERSION;
|
|
|
14
14
|
var init_version = __esm({
|
|
15
15
|
"src/lib/version.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
MCP_VERSION = "1.
|
|
17
|
+
MCP_VERSION = "1.4.1";
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
// src/lib/posthog.ts
|
|
22
22
|
import { createHash } from "node:crypto";
|
|
23
|
-
import { PostHog } from "posthog-node";
|
|
24
23
|
function hashUserId(userId) {
|
|
25
24
|
return createHash("sha256").update(`${POSTHOG_SALT}:${userId}`).digest("hex").substring(0, 32);
|
|
26
25
|
}
|
|
26
|
+
function isTelemetryOptedIn() {
|
|
27
|
+
if (process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true" || process.env.SOCIALNEURON_NO_TELEMETRY === "1") {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return process.env.SOCIALNEURON_TELEMETRY === "1";
|
|
31
|
+
}
|
|
27
32
|
function initPostHog() {
|
|
28
|
-
if (
|
|
33
|
+
if (!isTelemetryOptedIn()) return;
|
|
29
34
|
const key = process.env.POSTHOG_KEY || process.env.VITE_POSTHOG_KEY;
|
|
30
35
|
const host = process.env.POSTHOG_HOST || process.env.VITE_POSTHOG_HOST || "https://eu.i.posthog.com";
|
|
31
36
|
if (!key) return;
|
|
32
|
-
|
|
37
|
+
import("posthog-node").then(({ PostHog }) => {
|
|
38
|
+
client = new PostHog(key, { host, flushAt: 5, flushInterval: 1e4 });
|
|
39
|
+
}).catch(() => {
|
|
40
|
+
});
|
|
33
41
|
}
|
|
34
42
|
async function captureToolEvent(args) {
|
|
35
43
|
if (!client) return;
|
|
@@ -875,7 +883,7 @@ var init_tool_catalog = __esm({
|
|
|
875
883
|
name: "check_status",
|
|
876
884
|
description: "Check status of async content generation job",
|
|
877
885
|
module: "content",
|
|
878
|
-
scope: "mcp:
|
|
886
|
+
scope: "mcp:read"
|
|
879
887
|
},
|
|
880
888
|
{
|
|
881
889
|
name: "create_storyboard",
|
|
@@ -3738,7 +3746,7 @@ var TOOL_SCOPES = {
|
|
|
3738
3746
|
adapt_content: "mcp:write",
|
|
3739
3747
|
generate_video: "mcp:write",
|
|
3740
3748
|
generate_image: "mcp:write",
|
|
3741
|
-
check_status: "mcp:
|
|
3749
|
+
check_status: "mcp:read",
|
|
3742
3750
|
render_demo_video: "mcp:write",
|
|
3743
3751
|
save_brand_profile: "mcp:write",
|
|
3744
3752
|
update_platform_voice: "mcp:write",
|
|
@@ -3792,6 +3800,77 @@ function hasScope(userScopes, required) {
|
|
|
3792
3800
|
// src/tools/ideation.ts
|
|
3793
3801
|
init_edge_function();
|
|
3794
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();
|
|
3795
3874
|
function registerIdeationTools(server2) {
|
|
3796
3875
|
server2.tool(
|
|
3797
3876
|
"generate_content",
|
|
@@ -3812,7 +3891,9 @@ function registerIdeationTools(server2) {
|
|
|
3812
3891
|
"facebook",
|
|
3813
3892
|
"threads",
|
|
3814
3893
|
"bluesky"
|
|
3815
|
-
]).optional().describe(
|
|
3894
|
+
]).optional().describe(
|
|
3895
|
+
"Target social media platform. Helps tailor tone, length, and format."
|
|
3896
|
+
),
|
|
3816
3897
|
brand_voice: z.string().max(500).optional().describe(
|
|
3817
3898
|
'Brand voice guidelines to follow (e.g. "professional and empathetic", "playful and Gen-Z"). Leave blank to use a neutral tone.'
|
|
3818
3899
|
),
|
|
@@ -3823,7 +3904,30 @@ function registerIdeationTools(server2) {
|
|
|
3823
3904
|
"Project ID to auto-load brand profile and performance context for prompt enrichment."
|
|
3824
3905
|
)
|
|
3825
3906
|
},
|
|
3826
|
-
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
|
+
}
|
|
3827
3931
|
let enrichedPrompt = prompt2;
|
|
3828
3932
|
if (platform3) {
|
|
3829
3933
|
enrichedPrompt += `
|
|
@@ -3955,8 +4059,12 @@ Content Type: ${content_type}`;
|
|
|
3955
4059
|
category: z.string().optional().describe(
|
|
3956
4060
|
"Category filter (for YouTube). Examples: general, entertainment, education, tech, music, gaming, sports, news."
|
|
3957
4061
|
),
|
|
3958
|
-
niche: z.string().optional().describe(
|
|
3959
|
-
|
|
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
|
+
),
|
|
3960
4068
|
force_refresh: z.boolean().optional().describe("Skip the server-side cache and fetch fresh data.")
|
|
3961
4069
|
},
|
|
3962
4070
|
async ({ source, category, niche, url, force_refresh }) => {
|
|
@@ -4031,7 +4139,9 @@ Content Type: ${content_type}`;
|
|
|
4031
4139
|
"adapt_content",
|
|
4032
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.",
|
|
4033
4141
|
{
|
|
4034
|
-
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
|
+
),
|
|
4035
4145
|
source_platform: z.enum([
|
|
4036
4146
|
"youtube",
|
|
4037
4147
|
"tiktok",
|
|
@@ -4041,7 +4151,9 @@ Content Type: ${content_type}`;
|
|
|
4041
4151
|
"facebook",
|
|
4042
4152
|
"threads",
|
|
4043
4153
|
"bluesky"
|
|
4044
|
-
]).optional().describe(
|
|
4154
|
+
]).optional().describe(
|
|
4155
|
+
"The platform the content was originally written for. Helps preserve intent."
|
|
4156
|
+
),
|
|
4045
4157
|
target_platform: z.enum([
|
|
4046
4158
|
"youtube",
|
|
4047
4159
|
"tiktok",
|
|
@@ -4055,9 +4167,33 @@ Content Type: ${content_type}`;
|
|
|
4055
4167
|
brand_voice: z.string().max(500).optional().describe(
|
|
4056
4168
|
'Brand voice guidelines to maintain during adaptation (e.g. "professional", "playful").'
|
|
4057
4169
|
),
|
|
4058
|
-
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
|
+
)
|
|
4059
4173
|
},
|
|
4060
|
-
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
|
+
}
|
|
4061
4197
|
const platformGuidelines = {
|
|
4062
4198
|
twitter: "Max 280 characters. Concise, punchy. 1-3 hashtags max. Thread-friendly.",
|
|
4063
4199
|
threads: "Max 500 characters. Conversational, opinion-driven. Minimal hashtags.",
|
|
@@ -4140,76 +4276,6 @@ ${content}`,
|
|
|
4140
4276
|
// src/tools/content.ts
|
|
4141
4277
|
init_edge_function();
|
|
4142
4278
|
import { z as z2 } from "zod";
|
|
4143
|
-
|
|
4144
|
-
// src/lib/rate-limit.ts
|
|
4145
|
-
var CATEGORY_CONFIGS = {
|
|
4146
|
-
posting: { maxTokens: 30, refillRate: 30 / 60 },
|
|
4147
|
-
// 30 req/min
|
|
4148
|
-
screenshot: { maxTokens: 10, refillRate: 10 / 60 },
|
|
4149
|
-
// 10 req/min
|
|
4150
|
-
read: { maxTokens: 60, refillRate: 60 / 60 }
|
|
4151
|
-
// 60 req/min
|
|
4152
|
-
};
|
|
4153
|
-
var RateLimiter = class {
|
|
4154
|
-
tokens;
|
|
4155
|
-
lastRefill;
|
|
4156
|
-
maxTokens;
|
|
4157
|
-
refillRate;
|
|
4158
|
-
// tokens per second
|
|
4159
|
-
constructor(config) {
|
|
4160
|
-
this.maxTokens = config.maxTokens;
|
|
4161
|
-
this.refillRate = config.refillRate;
|
|
4162
|
-
this.tokens = config.maxTokens;
|
|
4163
|
-
this.lastRefill = Date.now();
|
|
4164
|
-
}
|
|
4165
|
-
/**
|
|
4166
|
-
* Try to consume one token. Returns true if the request is allowed,
|
|
4167
|
-
* false if rate-limited.
|
|
4168
|
-
*/
|
|
4169
|
-
consume() {
|
|
4170
|
-
this.refill();
|
|
4171
|
-
if (this.tokens >= 1) {
|
|
4172
|
-
this.tokens -= 1;
|
|
4173
|
-
return true;
|
|
4174
|
-
}
|
|
4175
|
-
return false;
|
|
4176
|
-
}
|
|
4177
|
-
/**
|
|
4178
|
-
* Seconds until at least one token is available.
|
|
4179
|
-
*/
|
|
4180
|
-
retryAfter() {
|
|
4181
|
-
this.refill();
|
|
4182
|
-
if (this.tokens >= 1) return 0;
|
|
4183
|
-
return Math.ceil((1 - this.tokens) / this.refillRate);
|
|
4184
|
-
}
|
|
4185
|
-
refill() {
|
|
4186
|
-
const now = Date.now();
|
|
4187
|
-
const elapsed = (now - this.lastRefill) / 1e3;
|
|
4188
|
-
this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
|
|
4189
|
-
this.lastRefill = now;
|
|
4190
|
-
}
|
|
4191
|
-
};
|
|
4192
|
-
var limiters = /* @__PURE__ */ new Map();
|
|
4193
|
-
function getRateLimiter(category) {
|
|
4194
|
-
let limiter = limiters.get(category);
|
|
4195
|
-
if (!limiter) {
|
|
4196
|
-
const config = CATEGORY_CONFIGS[category] ?? CATEGORY_CONFIGS.read;
|
|
4197
|
-
limiter = new RateLimiter(config);
|
|
4198
|
-
limiters.set(category, limiter);
|
|
4199
|
-
}
|
|
4200
|
-
return limiter;
|
|
4201
|
-
}
|
|
4202
|
-
function checkRateLimit(category, key) {
|
|
4203
|
-
const bucketKey = key ? `${category}:${key}` : category;
|
|
4204
|
-
const limiter = getRateLimiter(bucketKey);
|
|
4205
|
-
const allowed = limiter.consume();
|
|
4206
|
-
return {
|
|
4207
|
-
allowed,
|
|
4208
|
-
retryAfter: allowed ? 0 : limiter.retryAfter()
|
|
4209
|
-
};
|
|
4210
|
-
}
|
|
4211
|
-
|
|
4212
|
-
// src/tools/content.ts
|
|
4213
4279
|
init_supabase();
|
|
4214
4280
|
|
|
4215
4281
|
// src/lib/sanitize-error.ts
|
|
@@ -4263,8 +4329,15 @@ function sanitizeDbError(error) {
|
|
|
4263
4329
|
|
|
4264
4330
|
// src/tools/content.ts
|
|
4265
4331
|
init_request_context();
|
|
4266
|
-
|
|
4267
|
-
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
|
+
);
|
|
4268
4341
|
var _globalCreditsUsed = 0;
|
|
4269
4342
|
var _globalAssetsGenerated = 0;
|
|
4270
4343
|
function getCreditsUsed() {
|
|
@@ -4306,7 +4379,7 @@ function getCurrentBudgetStatus() {
|
|
|
4306
4379
|
function asEnvelope(data) {
|
|
4307
4380
|
return {
|
|
4308
4381
|
_meta: {
|
|
4309
|
-
version:
|
|
4382
|
+
version: MCP_VERSION,
|
|
4310
4383
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4311
4384
|
},
|
|
4312
4385
|
data
|
|
@@ -4388,8 +4461,12 @@ function registerContentTools(server2) {
|
|
|
4388
4461
|
enable_audio: z2.boolean().optional().describe(
|
|
4389
4462
|
"Enable native audio generation. Kling 2.6: doubles cost. Kling 3.0: 50% more (std 30/sec, pro 40/sec). 5+ languages."
|
|
4390
4463
|
),
|
|
4391
|
-
image_url: z2.string().optional().describe(
|
|
4392
|
-
|
|
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
|
+
),
|
|
4393
4470
|
response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
4394
4471
|
},
|
|
4395
4472
|
async ({
|
|
@@ -4827,10 +4904,13 @@ function registerContentTools(server2) {
|
|
|
4827
4904
|
};
|
|
4828
4905
|
}
|
|
4829
4906
|
if (job.external_id && (job.status === "pending" || job.status === "processing")) {
|
|
4830
|
-
const { data: liveStatus } = await callEdgeFunction(
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4907
|
+
const { data: liveStatus } = await callEdgeFunction(
|
|
4908
|
+
"kie-task-status",
|
|
4909
|
+
{
|
|
4910
|
+
taskId: job.external_id,
|
|
4911
|
+
model: job.model
|
|
4912
|
+
}
|
|
4913
|
+
);
|
|
4834
4914
|
if (liveStatus) {
|
|
4835
4915
|
const lines2 = [
|
|
4836
4916
|
`Job: ${job.id}`,
|
|
@@ -4904,7 +4984,12 @@ function registerContentTools(server2) {
|
|
|
4904
4984
|
});
|
|
4905
4985
|
if (format === "json") {
|
|
4906
4986
|
return {
|
|
4907
|
-
content: [
|
|
4987
|
+
content: [
|
|
4988
|
+
{
|
|
4989
|
+
type: "text",
|
|
4990
|
+
text: JSON.stringify(asEnvelope(job), null, 2)
|
|
4991
|
+
}
|
|
4992
|
+
]
|
|
4908
4993
|
};
|
|
4909
4994
|
}
|
|
4910
4995
|
return {
|
|
@@ -4922,7 +5007,15 @@ function registerContentTools(server2) {
|
|
|
4922
5007
|
brand_context: z2.string().max(3e3).optional().describe(
|
|
4923
5008
|
"Brand context JSON from extract_brand. Include colors, voice tone, visual style keywords for consistent branding across frames."
|
|
4924
5009
|
),
|
|
4925
|
-
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
|
+
),
|
|
4926
5019
|
target_duration: z2.number().min(5).max(120).optional().describe(
|
|
4927
5020
|
"Target total duration in seconds. Defaults to 30s for short-form, 60s for YouTube."
|
|
4928
5021
|
),
|
|
@@ -4930,7 +5023,9 @@ function registerContentTools(server2) {
|
|
|
4930
5023
|
style: z2.string().optional().describe(
|
|
4931
5024
|
'Visual style direction (e.g., "cinematic", "anime", "documentary", "motion graphics").'
|
|
4932
5025
|
),
|
|
4933
|
-
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
|
+
)
|
|
4934
5029
|
},
|
|
4935
5030
|
async ({
|
|
4936
5031
|
concept,
|
|
@@ -4943,7 +5038,11 @@ function registerContentTools(server2) {
|
|
|
4943
5038
|
}) => {
|
|
4944
5039
|
const format = response_format ?? "json";
|
|
4945
5040
|
const startedAt = Date.now();
|
|
4946
|
-
const isShortForm = [
|
|
5041
|
+
const isShortForm = [
|
|
5042
|
+
"tiktok",
|
|
5043
|
+
"instagram-reels",
|
|
5044
|
+
"youtube-shorts"
|
|
5045
|
+
].includes(platform3);
|
|
4947
5046
|
const duration = target_duration ?? (isShortForm ? 30 : 60);
|
|
4948
5047
|
const scenes = num_scenes ?? (isShortForm ? 7 : 10);
|
|
4949
5048
|
const aspectRatio = isShortForm ? "9:16" : "16:9";
|
|
@@ -5036,7 +5135,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
5036
5135
|
details: { error }
|
|
5037
5136
|
});
|
|
5038
5137
|
return {
|
|
5039
|
-
content: [
|
|
5138
|
+
content: [
|
|
5139
|
+
{
|
|
5140
|
+
type: "text",
|
|
5141
|
+
text: `Storyboard generation failed: ${error}`
|
|
5142
|
+
}
|
|
5143
|
+
],
|
|
5040
5144
|
isError: true
|
|
5041
5145
|
};
|
|
5042
5146
|
}
|
|
@@ -5052,7 +5156,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
5052
5156
|
try {
|
|
5053
5157
|
const parsed = JSON.parse(rawContent);
|
|
5054
5158
|
return {
|
|
5055
|
-
content: [
|
|
5159
|
+
content: [
|
|
5160
|
+
{
|
|
5161
|
+
type: "text",
|
|
5162
|
+
text: JSON.stringify(asEnvelope(parsed), null, 2)
|
|
5163
|
+
}
|
|
5164
|
+
]
|
|
5056
5165
|
};
|
|
5057
5166
|
} catch {
|
|
5058
5167
|
return {
|
|
@@ -5116,7 +5225,10 @@ Return ONLY valid JSON in this exact format:
|
|
|
5116
5225
|
isError: true
|
|
5117
5226
|
};
|
|
5118
5227
|
}
|
|
5119
|
-
const rateLimit = checkRateLimit(
|
|
5228
|
+
const rateLimit = checkRateLimit(
|
|
5229
|
+
"posting",
|
|
5230
|
+
`generate_voiceover:${userId}`
|
|
5231
|
+
);
|
|
5120
5232
|
if (!rateLimit.allowed) {
|
|
5121
5233
|
await logMcpToolInvocation({
|
|
5122
5234
|
toolName: "generate_voiceover",
|
|
@@ -5151,7 +5263,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
5151
5263
|
details: { error }
|
|
5152
5264
|
});
|
|
5153
5265
|
return {
|
|
5154
|
-
content: [
|
|
5266
|
+
content: [
|
|
5267
|
+
{
|
|
5268
|
+
type: "text",
|
|
5269
|
+
text: `Voiceover generation failed: ${error}`
|
|
5270
|
+
}
|
|
5271
|
+
],
|
|
5155
5272
|
isError: true
|
|
5156
5273
|
};
|
|
5157
5274
|
}
|
|
@@ -5164,7 +5281,10 @@ Return ONLY valid JSON in this exact format:
|
|
|
5164
5281
|
});
|
|
5165
5282
|
return {
|
|
5166
5283
|
content: [
|
|
5167
|
-
{
|
|
5284
|
+
{
|
|
5285
|
+
type: "text",
|
|
5286
|
+
text: "Voiceover generation failed: no audio URL returned."
|
|
5287
|
+
}
|
|
5168
5288
|
],
|
|
5169
5289
|
isError: true
|
|
5170
5290
|
};
|
|
@@ -5236,7 +5356,9 @@ Return ONLY valid JSON in this exact format:
|
|
|
5236
5356
|
"Carousel template. hormozi-authority: bold typography, one idea per slide, dark backgrounds. educational-series: numbered tips. Default: hormozi-authority."
|
|
5237
5357
|
),
|
|
5238
5358
|
slide_count: z2.number().min(3).max(10).optional().describe("Number of slides (3-10). Default: 7."),
|
|
5239
|
-
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
|
+
),
|
|
5240
5362
|
style: z2.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
|
|
5241
5363
|
"Visual style. hormozi: black bg, bold white text, gold accents. Default: hormozi (when using hormozi-authority template)."
|
|
5242
5364
|
),
|
|
@@ -5273,7 +5395,10 @@ Return ONLY valid JSON in this exact format:
|
|
|
5273
5395
|
};
|
|
5274
5396
|
}
|
|
5275
5397
|
const userId = await getDefaultUserId();
|
|
5276
|
-
const rateLimit = checkRateLimit(
|
|
5398
|
+
const rateLimit = checkRateLimit(
|
|
5399
|
+
"posting",
|
|
5400
|
+
`generate_carousel:${userId}`
|
|
5401
|
+
);
|
|
5277
5402
|
if (!rateLimit.allowed) {
|
|
5278
5403
|
await logMcpToolInvocation({
|
|
5279
5404
|
toolName: "generate_carousel",
|
|
@@ -5311,7 +5436,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
5311
5436
|
details: { error }
|
|
5312
5437
|
});
|
|
5313
5438
|
return {
|
|
5314
|
-
content: [
|
|
5439
|
+
content: [
|
|
5440
|
+
{
|
|
5441
|
+
type: "text",
|
|
5442
|
+
text: `Carousel generation failed: ${error}`
|
|
5443
|
+
}
|
|
5444
|
+
],
|
|
5315
5445
|
isError: true
|
|
5316
5446
|
};
|
|
5317
5447
|
}
|
|
@@ -5323,7 +5453,12 @@ Return ONLY valid JSON in this exact format:
|
|
|
5323
5453
|
details: { error: "No carousel data returned" }
|
|
5324
5454
|
});
|
|
5325
5455
|
return {
|
|
5326
|
-
content: [
|
|
5456
|
+
content: [
|
|
5457
|
+
{
|
|
5458
|
+
type: "text",
|
|
5459
|
+
text: "Carousel generation returned no data."
|
|
5460
|
+
}
|
|
5461
|
+
],
|
|
5327
5462
|
isError: true
|
|
5328
5463
|
};
|
|
5329
5464
|
}
|
|
@@ -5389,6 +5524,7 @@ import { z as z3 } from "zod";
|
|
|
5389
5524
|
import { createHash as createHash2 } from "node:crypto";
|
|
5390
5525
|
init_supabase();
|
|
5391
5526
|
init_quality();
|
|
5527
|
+
init_version();
|
|
5392
5528
|
var PLATFORM_CASE_MAP = {
|
|
5393
5529
|
youtube: "YouTube",
|
|
5394
5530
|
tiktok: "TikTok",
|
|
@@ -5402,7 +5538,7 @@ var PLATFORM_CASE_MAP = {
|
|
|
5402
5538
|
function asEnvelope2(data) {
|
|
5403
5539
|
return {
|
|
5404
5540
|
_meta: {
|
|
5405
|
-
version:
|
|
5541
|
+
version: MCP_VERSION,
|
|
5406
5542
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5407
5543
|
},
|
|
5408
5544
|
data
|
|
@@ -5434,15 +5570,21 @@ function registerDistributionTools(server2) {
|
|
|
5434
5570
|
"threads",
|
|
5435
5571
|
"bluesky"
|
|
5436
5572
|
])
|
|
5437
|
-
).min(1).describe(
|
|
5573
|
+
).min(1).describe(
|
|
5574
|
+
"Target platforms to post to. Each must have an active OAuth connection."
|
|
5575
|
+
),
|
|
5438
5576
|
title: z3.string().optional().describe("Post title (used by YouTube and some other platforms)."),
|
|
5439
|
-
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
|
+
),
|
|
5440
5580
|
schedule_at: z3.string().optional().describe(
|
|
5441
5581
|
'ISO 8601 datetime for scheduled posting (e.g. "2026-03-15T14:00:00Z"). Omit for immediate posting.'
|
|
5442
5582
|
),
|
|
5443
5583
|
project_id: z3.string().optional().describe("Social Neuron project ID to associate this post with."),
|
|
5444
5584
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
|
|
5445
|
-
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
|
+
)
|
|
5446
5588
|
},
|
|
5447
5589
|
async ({
|
|
5448
5590
|
media_url,
|
|
@@ -5461,7 +5603,12 @@ function registerDistributionTools(server2) {
|
|
|
5461
5603
|
const startedAt = Date.now();
|
|
5462
5604
|
if ((!caption || caption.trim().length === 0) && (!title || title.trim().length === 0)) {
|
|
5463
5605
|
return {
|
|
5464
|
-
content: [
|
|
5606
|
+
content: [
|
|
5607
|
+
{
|
|
5608
|
+
type: "text",
|
|
5609
|
+
text: "Either caption or title is required."
|
|
5610
|
+
}
|
|
5611
|
+
],
|
|
5465
5612
|
isError: true
|
|
5466
5613
|
};
|
|
5467
5614
|
}
|
|
@@ -5484,7 +5631,9 @@ function registerDistributionTools(server2) {
|
|
|
5484
5631
|
isError: true
|
|
5485
5632
|
};
|
|
5486
5633
|
}
|
|
5487
|
-
const normalizedPlatforms = platforms.map(
|
|
5634
|
+
const normalizedPlatforms = platforms.map(
|
|
5635
|
+
(p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
|
|
5636
|
+
);
|
|
5488
5637
|
let finalCaption = caption;
|
|
5489
5638
|
if (attribution && finalCaption) {
|
|
5490
5639
|
finalCaption = `${finalCaption}
|
|
@@ -5548,7 +5697,9 @@ Created with Social Neuron`;
|
|
|
5548
5697
|
];
|
|
5549
5698
|
for (const [platform3, result] of Object.entries(data.results)) {
|
|
5550
5699
|
if (result.success) {
|
|
5551
|
-
lines.push(
|
|
5700
|
+
lines.push(
|
|
5701
|
+
` ${platform3}: OK (jobId=${result.jobId}, postId=${result.postId})`
|
|
5702
|
+
);
|
|
5552
5703
|
} else {
|
|
5553
5704
|
lines.push(` ${platform3}: FAILED - ${result.error}`);
|
|
5554
5705
|
}
|
|
@@ -5565,7 +5716,12 @@ Created with Social Neuron`;
|
|
|
5565
5716
|
});
|
|
5566
5717
|
if (format === "json") {
|
|
5567
5718
|
return {
|
|
5568
|
-
content: [
|
|
5719
|
+
content: [
|
|
5720
|
+
{
|
|
5721
|
+
type: "text",
|
|
5722
|
+
text: JSON.stringify(asEnvelope2(data), null, 2)
|
|
5723
|
+
}
|
|
5724
|
+
],
|
|
5569
5725
|
isError: !data.success
|
|
5570
5726
|
};
|
|
5571
5727
|
}
|
|
@@ -5621,12 +5777,17 @@ Created with Social Neuron`;
|
|
|
5621
5777
|
for (const account of accounts) {
|
|
5622
5778
|
const name = account.username || "(unnamed)";
|
|
5623
5779
|
const platformLower = account.platform.toLowerCase();
|
|
5624
|
-
lines.push(
|
|
5780
|
+
lines.push(
|
|
5781
|
+
` ${platformLower}: ${name} (connected ${account.created_at.split("T")[0]})`
|
|
5782
|
+
);
|
|
5625
5783
|
}
|
|
5626
5784
|
if (format === "json") {
|
|
5627
5785
|
return {
|
|
5628
5786
|
content: [
|
|
5629
|
-
{
|
|
5787
|
+
{
|
|
5788
|
+
type: "text",
|
|
5789
|
+
text: JSON.stringify(asEnvelope2({ accounts }), null, 2)
|
|
5790
|
+
}
|
|
5630
5791
|
]
|
|
5631
5792
|
};
|
|
5632
5793
|
}
|
|
@@ -5688,7 +5849,10 @@ Created with Social Neuron`;
|
|
|
5688
5849
|
if (format === "json") {
|
|
5689
5850
|
return {
|
|
5690
5851
|
content: [
|
|
5691
|
-
{
|
|
5852
|
+
{
|
|
5853
|
+
type: "text",
|
|
5854
|
+
text: JSON.stringify(asEnvelope2({ posts: [] }), null, 2)
|
|
5855
|
+
}
|
|
5692
5856
|
]
|
|
5693
5857
|
};
|
|
5694
5858
|
}
|
|
@@ -5705,7 +5869,10 @@ Created with Social Neuron`;
|
|
|
5705
5869
|
if (format === "json") {
|
|
5706
5870
|
return {
|
|
5707
5871
|
content: [
|
|
5708
|
-
{
|
|
5872
|
+
{
|
|
5873
|
+
type: "text",
|
|
5874
|
+
text: JSON.stringify(asEnvelope2({ posts }), null, 2)
|
|
5875
|
+
}
|
|
5709
5876
|
]
|
|
5710
5877
|
};
|
|
5711
5878
|
}
|
|
@@ -5766,7 +5933,13 @@ Created with Social Neuron`;
|
|
|
5766
5933
|
min_gap_hours: z3.number().min(1).max(24).default(4).describe("Minimum gap between posts on same platform"),
|
|
5767
5934
|
response_format: z3.enum(["text", "json"]).default("text")
|
|
5768
5935
|
},
|
|
5769
|
-
async ({
|
|
5936
|
+
async ({
|
|
5937
|
+
platforms,
|
|
5938
|
+
count,
|
|
5939
|
+
start_after,
|
|
5940
|
+
min_gap_hours,
|
|
5941
|
+
response_format
|
|
5942
|
+
}) => {
|
|
5770
5943
|
const startedAt = Date.now();
|
|
5771
5944
|
try {
|
|
5772
5945
|
const userId = await getDefaultUserId();
|
|
@@ -5777,7 +5950,9 @@ Created with Social Neuron`;
|
|
|
5777
5950
|
const gapMs = min_gap_hours * 60 * 60 * 1e3;
|
|
5778
5951
|
const candidates = [];
|
|
5779
5952
|
for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
|
|
5780
|
-
const date = new Date(
|
|
5953
|
+
const date = new Date(
|
|
5954
|
+
startDate.getTime() + dayOffset * 24 * 60 * 60 * 1e3
|
|
5955
|
+
);
|
|
5781
5956
|
const dayOfWeek = date.getUTCDay();
|
|
5782
5957
|
for (const platform3 of platforms) {
|
|
5783
5958
|
const hours = PREFERRED_HOURS[platform3] ?? [12, 16];
|
|
@@ -5786,8 +5961,11 @@ Created with Social Neuron`;
|
|
|
5786
5961
|
slotDate.setUTCHours(hours[hourIdx], 0, 0, 0);
|
|
5787
5962
|
if (slotDate <= startDate) continue;
|
|
5788
5963
|
const hasConflict = (existingPosts ?? []).some((post) => {
|
|
5789
|
-
if (String(post.platform).toLowerCase() !== platform3)
|
|
5790
|
-
|
|
5964
|
+
if (String(post.platform).toLowerCase() !== platform3)
|
|
5965
|
+
return false;
|
|
5966
|
+
const postTime = new Date(
|
|
5967
|
+
post.scheduled_at ?? post.published_at
|
|
5968
|
+
).getTime();
|
|
5791
5969
|
return Math.abs(postTime - slotDate.getTime()) < gapMs;
|
|
5792
5970
|
});
|
|
5793
5971
|
let engagementScore = hours.length - hourIdx;
|
|
@@ -5832,15 +6010,22 @@ Created with Social Neuron`;
|
|
|
5832
6010
|
};
|
|
5833
6011
|
}
|
|
5834
6012
|
const lines = [];
|
|
5835
|
-
lines.push(
|
|
6013
|
+
lines.push(
|
|
6014
|
+
`Found ${slots.length} optimal slots (${conflictsAvoided} conflicts avoided):`
|
|
6015
|
+
);
|
|
5836
6016
|
lines.push("");
|
|
5837
6017
|
lines.push("Datetime (UTC) | Platform | Score");
|
|
5838
6018
|
lines.push("-------------------------+------------+------");
|
|
5839
6019
|
for (const s of slots) {
|
|
5840
6020
|
const dt = s.datetime.replace("T", " ").slice(0, 19);
|
|
5841
|
-
lines.push(
|
|
6021
|
+
lines.push(
|
|
6022
|
+
`${dt.padEnd(25)}| ${s.platform.padEnd(11)}| ${s.engagement_score}`
|
|
6023
|
+
);
|
|
5842
6024
|
}
|
|
5843
|
-
return {
|
|
6025
|
+
return {
|
|
6026
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
6027
|
+
isError: false
|
|
6028
|
+
};
|
|
5844
6029
|
} catch (err) {
|
|
5845
6030
|
const durationMs = Date.now() - startedAt;
|
|
5846
6031
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -5851,7 +6036,9 @@ Created with Social Neuron`;
|
|
|
5851
6036
|
details: { error: message }
|
|
5852
6037
|
});
|
|
5853
6038
|
return {
|
|
5854
|
-
content: [
|
|
6039
|
+
content: [
|
|
6040
|
+
{ type: "text", text: `Failed to find slots: ${message}` }
|
|
6041
|
+
],
|
|
5855
6042
|
isError: true
|
|
5856
6043
|
};
|
|
5857
6044
|
}
|
|
@@ -5878,8 +6065,12 @@ Created with Social Neuron`;
|
|
|
5878
6065
|
auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
|
|
5879
6066
|
dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
|
|
5880
6067
|
response_format: z3.enum(["text", "json"]).default("text"),
|
|
5881
|
-
enforce_quality: z3.boolean().default(true).describe(
|
|
5882
|
-
|
|
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
|
+
),
|
|
5883
6074
|
batch_size: z3.number().int().min(1).max(10).default(4).describe("Concurrent schedule calls per platform batch."),
|
|
5884
6075
|
idempotency_seed: z3.string().max(128).optional().describe("Optional stable seed used when building idempotency keys.")
|
|
5885
6076
|
},
|
|
@@ -5918,17 +6109,25 @@ Created with Social Neuron`;
|
|
|
5918
6109
|
if (!stored?.plan_payload) {
|
|
5919
6110
|
return {
|
|
5920
6111
|
content: [
|
|
5921
|
-
{
|
|
6112
|
+
{
|
|
6113
|
+
type: "text",
|
|
6114
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
6115
|
+
}
|
|
5922
6116
|
],
|
|
5923
6117
|
isError: true
|
|
5924
6118
|
};
|
|
5925
6119
|
}
|
|
5926
6120
|
const payload = stored.plan_payload;
|
|
5927
|
-
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;
|
|
5928
6124
|
if (!postsFromPayload) {
|
|
5929
6125
|
return {
|
|
5930
6126
|
content: [
|
|
5931
|
-
{
|
|
6127
|
+
{
|
|
6128
|
+
type: "text",
|
|
6129
|
+
text: `Stored plan ${plan_id} has no posts array.`
|
|
6130
|
+
}
|
|
5932
6131
|
],
|
|
5933
6132
|
isError: true
|
|
5934
6133
|
};
|
|
@@ -6020,7 +6219,10 @@ Created with Social Neuron`;
|
|
|
6020
6219
|
approvalSummary = {
|
|
6021
6220
|
total: approvals.length,
|
|
6022
6221
|
eligible: approvedPosts.length,
|
|
6023
|
-
skipped: Math.max(
|
|
6222
|
+
skipped: Math.max(
|
|
6223
|
+
0,
|
|
6224
|
+
workingPlan.posts.length - approvedPosts.length
|
|
6225
|
+
)
|
|
6024
6226
|
};
|
|
6025
6227
|
workingPlan = {
|
|
6026
6228
|
...workingPlan,
|
|
@@ -6054,9 +6256,14 @@ Created with Social Neuron`;
|
|
|
6054
6256
|
try {
|
|
6055
6257
|
const { data: settingsData } = await supabase.from("system_settings").select("value").eq("key", "content_safety").maybeSingle();
|
|
6056
6258
|
if (settingsData?.value?.quality_threshold !== void 0) {
|
|
6057
|
-
const parsedThreshold = Number(
|
|
6259
|
+
const parsedThreshold = Number(
|
|
6260
|
+
settingsData.value.quality_threshold
|
|
6261
|
+
);
|
|
6058
6262
|
if (Number.isFinite(parsedThreshold)) {
|
|
6059
|
-
effectiveQualityThreshold = Math.max(
|
|
6263
|
+
effectiveQualityThreshold = Math.max(
|
|
6264
|
+
0,
|
|
6265
|
+
Math.min(35, Math.trunc(parsedThreshold))
|
|
6266
|
+
);
|
|
6060
6267
|
}
|
|
6061
6268
|
}
|
|
6062
6269
|
if (Array.isArray(settingsData?.value?.custom_banned_terms)) {
|
|
@@ -6092,13 +6299,18 @@ Created with Social Neuron`;
|
|
|
6092
6299
|
}
|
|
6093
6300
|
};
|
|
6094
6301
|
});
|
|
6095
|
-
const qualityPassed = postsWithResults.filter(
|
|
6302
|
+
const qualityPassed = postsWithResults.filter(
|
|
6303
|
+
(post) => post.quality.passed
|
|
6304
|
+
).length;
|
|
6096
6305
|
const qualitySummary = {
|
|
6097
6306
|
total_posts: postsWithResults.length,
|
|
6098
6307
|
passed: qualityPassed,
|
|
6099
6308
|
failed: postsWithResults.length - qualityPassed,
|
|
6100
6309
|
avg_score: postsWithResults.length > 0 ? Number(
|
|
6101
|
-
(postsWithResults.reduce(
|
|
6310
|
+
(postsWithResults.reduce(
|
|
6311
|
+
(sum, post) => sum + post.quality.score,
|
|
6312
|
+
0
|
|
6313
|
+
) / postsWithResults.length).toFixed(2)
|
|
6102
6314
|
) : 0
|
|
6103
6315
|
};
|
|
6104
6316
|
if (dry_run) {
|
|
@@ -6168,8 +6380,13 @@ Created with Social Neuron`;
|
|
|
6168
6380
|
}
|
|
6169
6381
|
}
|
|
6170
6382
|
lines2.push("");
|
|
6171
|
-
lines2.push(
|
|
6172
|
-
|
|
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
|
+
};
|
|
6173
6390
|
}
|
|
6174
6391
|
let scheduled = 0;
|
|
6175
6392
|
let failed = 0;
|
|
@@ -6261,7 +6478,8 @@ Created with Social Neuron`;
|
|
|
6261
6478
|
}
|
|
6262
6479
|
const chunk = (arr, size) => {
|
|
6263
6480
|
const out = [];
|
|
6264
|
-
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));
|
|
6265
6483
|
return out;
|
|
6266
6484
|
};
|
|
6267
6485
|
const platformBatches = Array.from(grouped.entries()).map(
|
|
@@ -6269,7 +6487,9 @@ Created with Social Neuron`;
|
|
|
6269
6487
|
const platformResults = [];
|
|
6270
6488
|
const batches = chunk(platformPosts, batch_size);
|
|
6271
6489
|
for (const batch of batches) {
|
|
6272
|
-
const settled = await Promise.allSettled(
|
|
6490
|
+
const settled = await Promise.allSettled(
|
|
6491
|
+
batch.map((post) => scheduleOne(post))
|
|
6492
|
+
);
|
|
6273
6493
|
for (const outcome of settled) {
|
|
6274
6494
|
if (outcome.status === "fulfilled") {
|
|
6275
6495
|
platformResults.push(outcome.value);
|
|
@@ -6335,7 +6555,11 @@ Created with Social Neuron`;
|
|
|
6335
6555
|
plan_id: effectivePlanId,
|
|
6336
6556
|
approvals: approvalSummary,
|
|
6337
6557
|
posts: results,
|
|
6338
|
-
summary: {
|
|
6558
|
+
summary: {
|
|
6559
|
+
total_posts: workingPlan.posts.length,
|
|
6560
|
+
scheduled,
|
|
6561
|
+
failed
|
|
6562
|
+
}
|
|
6339
6563
|
}),
|
|
6340
6564
|
null,
|
|
6341
6565
|
2
|
|
@@ -6373,7 +6597,12 @@ Created with Social Neuron`;
|
|
|
6373
6597
|
details: { error: message }
|
|
6374
6598
|
});
|
|
6375
6599
|
return {
|
|
6376
|
-
content: [
|
|
6600
|
+
content: [
|
|
6601
|
+
{
|
|
6602
|
+
type: "text",
|
|
6603
|
+
text: `Batch scheduling failed: ${message}`
|
|
6604
|
+
}
|
|
6605
|
+
],
|
|
6377
6606
|
isError: true
|
|
6378
6607
|
};
|
|
6379
6608
|
}
|
|
@@ -6385,10 +6614,11 @@ Created with Social Neuron`;
|
|
|
6385
6614
|
init_supabase();
|
|
6386
6615
|
init_edge_function();
|
|
6387
6616
|
import { z as z4 } from "zod";
|
|
6617
|
+
init_version();
|
|
6388
6618
|
function asEnvelope3(data) {
|
|
6389
6619
|
return {
|
|
6390
6620
|
_meta: {
|
|
6391
|
-
version:
|
|
6621
|
+
version: MCP_VERSION,
|
|
6392
6622
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6393
6623
|
},
|
|
6394
6624
|
data
|
|
@@ -6501,7 +6731,9 @@ function registerAnalyticsTools(server2) {
|
|
|
6501
6731
|
]
|
|
6502
6732
|
};
|
|
6503
6733
|
}
|
|
6504
|
-
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);
|
|
6505
6737
|
if (simpleError) {
|
|
6506
6738
|
return {
|
|
6507
6739
|
content: [
|
|
@@ -6513,7 +6745,12 @@ function registerAnalyticsTools(server2) {
|
|
|
6513
6745
|
isError: true
|
|
6514
6746
|
};
|
|
6515
6747
|
}
|
|
6516
|
-
return formatSimpleAnalytics(
|
|
6748
|
+
return formatSimpleAnalytics(
|
|
6749
|
+
simpleRows,
|
|
6750
|
+
platform3,
|
|
6751
|
+
lookbackDays,
|
|
6752
|
+
format
|
|
6753
|
+
);
|
|
6517
6754
|
}
|
|
6518
6755
|
if (!rows || rows.length === 0) {
|
|
6519
6756
|
if (format === "json") {
|
|
@@ -6590,7 +6827,10 @@ function registerAnalyticsTools(server2) {
|
|
|
6590
6827
|
const format = response_format ?? "text";
|
|
6591
6828
|
const startedAt = Date.now();
|
|
6592
6829
|
const userId = await getDefaultUserId();
|
|
6593
|
-
const rateLimit = checkRateLimit(
|
|
6830
|
+
const rateLimit = checkRateLimit(
|
|
6831
|
+
"posting",
|
|
6832
|
+
`refresh_platform_analytics:${userId}`
|
|
6833
|
+
);
|
|
6594
6834
|
if (!rateLimit.allowed) {
|
|
6595
6835
|
await logMcpToolInvocation({
|
|
6596
6836
|
toolName: "refresh_platform_analytics",
|
|
@@ -6608,7 +6848,9 @@ function registerAnalyticsTools(server2) {
|
|
|
6608
6848
|
isError: true
|
|
6609
6849
|
};
|
|
6610
6850
|
}
|
|
6611
|
-
const { data, error } = await callEdgeFunction("fetch-analytics", {
|
|
6851
|
+
const { data, error } = await callEdgeFunction("fetch-analytics", {
|
|
6852
|
+
userId
|
|
6853
|
+
});
|
|
6612
6854
|
if (error) {
|
|
6613
6855
|
await logMcpToolInvocation({
|
|
6614
6856
|
toolName: "refresh_platform_analytics",
|
|
@@ -6617,7 +6859,12 @@ function registerAnalyticsTools(server2) {
|
|
|
6617
6859
|
details: { error }
|
|
6618
6860
|
});
|
|
6619
6861
|
return {
|
|
6620
|
-
content: [
|
|
6862
|
+
content: [
|
|
6863
|
+
{
|
|
6864
|
+
type: "text",
|
|
6865
|
+
text: `Error refreshing analytics: ${error}`
|
|
6866
|
+
}
|
|
6867
|
+
],
|
|
6621
6868
|
isError: true
|
|
6622
6869
|
};
|
|
6623
6870
|
}
|
|
@@ -6630,12 +6877,18 @@ function registerAnalyticsTools(server2) {
|
|
|
6630
6877
|
details: { error: "Edge function returned success=false" }
|
|
6631
6878
|
});
|
|
6632
6879
|
return {
|
|
6633
|
-
content: [
|
|
6880
|
+
content: [
|
|
6881
|
+
{ type: "text", text: "Analytics refresh failed." }
|
|
6882
|
+
],
|
|
6634
6883
|
isError: true
|
|
6635
6884
|
};
|
|
6636
6885
|
}
|
|
6637
|
-
const queued = (result.results ?? []).filter(
|
|
6638
|
-
|
|
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;
|
|
6639
6892
|
const lines = [
|
|
6640
6893
|
`Analytics refresh triggered successfully.`,
|
|
6641
6894
|
` Posts processed: ${result.postsProcessed}`,
|
|
@@ -6681,7 +6934,10 @@ function formatAnalytics(summary, days, format) {
|
|
|
6681
6934
|
if (format === "json") {
|
|
6682
6935
|
return {
|
|
6683
6936
|
content: [
|
|
6684
|
-
{
|
|
6937
|
+
{
|
|
6938
|
+
type: "text",
|
|
6939
|
+
text: JSON.stringify(asEnvelope3({ ...summary, days }), null, 2)
|
|
6940
|
+
}
|
|
6685
6941
|
]
|
|
6686
6942
|
};
|
|
6687
6943
|
}
|
|
@@ -6799,10 +7055,160 @@ function formatSimpleAnalytics(rows, platform3, days, format) {
|
|
|
6799
7055
|
init_edge_function();
|
|
6800
7056
|
init_supabase();
|
|
6801
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();
|
|
6802
7208
|
function asEnvelope4(data) {
|
|
6803
7209
|
return {
|
|
6804
7210
|
_meta: {
|
|
6805
|
-
version:
|
|
7211
|
+
version: MCP_VERSION,
|
|
6806
7212
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6807
7213
|
},
|
|
6808
7214
|
data
|
|
@@ -6819,6 +7225,15 @@ function registerBrandTools(server2) {
|
|
|
6819
7225
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6820
7226
|
},
|
|
6821
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
|
+
}
|
|
6822
7237
|
const { data, error } = await callEdgeFunction(
|
|
6823
7238
|
"brand-extract",
|
|
6824
7239
|
{ url },
|
|
@@ -6848,7 +7263,12 @@ function registerBrandTools(server2) {
|
|
|
6848
7263
|
}
|
|
6849
7264
|
if ((response_format || "text") === "json") {
|
|
6850
7265
|
return {
|
|
6851
|
-
content: [
|
|
7266
|
+
content: [
|
|
7267
|
+
{
|
|
7268
|
+
type: "text",
|
|
7269
|
+
text: JSON.stringify(asEnvelope4(data), null, 2)
|
|
7270
|
+
}
|
|
7271
|
+
]
|
|
6852
7272
|
};
|
|
6853
7273
|
}
|
|
6854
7274
|
const lines = [
|
|
@@ -6912,7 +7332,12 @@ function registerBrandTools(server2) {
|
|
|
6912
7332
|
const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
|
|
6913
7333
|
if (!membership) {
|
|
6914
7334
|
return {
|
|
6915
|
-
content: [
|
|
7335
|
+
content: [
|
|
7336
|
+
{
|
|
7337
|
+
type: "text",
|
|
7338
|
+
text: "Project is not accessible to current user."
|
|
7339
|
+
}
|
|
7340
|
+
],
|
|
6916
7341
|
isError: true
|
|
6917
7342
|
};
|
|
6918
7343
|
}
|
|
@@ -6931,13 +7356,21 @@ function registerBrandTools(server2) {
|
|
|
6931
7356
|
if (!data) {
|
|
6932
7357
|
return {
|
|
6933
7358
|
content: [
|
|
6934
|
-
{
|
|
7359
|
+
{
|
|
7360
|
+
type: "text",
|
|
7361
|
+
text: "No active brand profile found for this project."
|
|
7362
|
+
}
|
|
6935
7363
|
]
|
|
6936
7364
|
};
|
|
6937
7365
|
}
|
|
6938
7366
|
if ((response_format || "text") === "json") {
|
|
6939
7367
|
return {
|
|
6940
|
-
content: [
|
|
7368
|
+
content: [
|
|
7369
|
+
{
|
|
7370
|
+
type: "text",
|
|
7371
|
+
text: JSON.stringify(asEnvelope4(data), null, 2)
|
|
7372
|
+
}
|
|
7373
|
+
]
|
|
6941
7374
|
};
|
|
6942
7375
|
}
|
|
6943
7376
|
const lines = [
|
|
@@ -6958,11 +7391,18 @@ function registerBrandTools(server2) {
|
|
|
6958
7391
|
"Persist a brand profile as the active profile for a project.",
|
|
6959
7392
|
{
|
|
6960
7393
|
project_id: z5.string().uuid().optional().describe("Project ID. Defaults to active project context."),
|
|
6961
|
-
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
|
+
),
|
|
6962
7397
|
change_summary: z5.string().max(500).optional().describe("Optional summary of changes."),
|
|
6963
7398
|
changed_paths: z5.array(z5.string()).optional().describe("Optional changed path list."),
|
|
6964
7399
|
source_url: z5.string().url().optional().describe("Optional source URL for provenance."),
|
|
6965
|
-
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."),
|
|
6966
7406
|
overall_confidence: z5.number().min(0).max(1).optional().describe("Optional overall confidence score in range 0..1."),
|
|
6967
7407
|
extraction_metadata: z5.record(z5.string(), z5.unknown()).optional(),
|
|
6968
7408
|
response_format: z5.enum(["text", "json"]).optional()
|
|
@@ -7002,20 +7442,28 @@ function registerBrandTools(server2) {
|
|
|
7002
7442
|
const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
|
|
7003
7443
|
if (!membership) {
|
|
7004
7444
|
return {
|
|
7005
|
-
content: [
|
|
7445
|
+
content: [
|
|
7446
|
+
{
|
|
7447
|
+
type: "text",
|
|
7448
|
+
text: "Project is not accessible to current user."
|
|
7449
|
+
}
|
|
7450
|
+
],
|
|
7006
7451
|
isError: true
|
|
7007
7452
|
};
|
|
7008
7453
|
}
|
|
7009
|
-
const { data: profileId, error } = await supabase.rpc(
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
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
|
+
);
|
|
7019
7467
|
if (error) {
|
|
7020
7468
|
return {
|
|
7021
7469
|
content: [
|
|
@@ -7036,7 +7484,12 @@ function registerBrandTools(server2) {
|
|
|
7036
7484
|
};
|
|
7037
7485
|
if ((response_format || "text") === "json") {
|
|
7038
7486
|
return {
|
|
7039
|
-
content: [
|
|
7487
|
+
content: [
|
|
7488
|
+
{
|
|
7489
|
+
type: "text",
|
|
7490
|
+
text: JSON.stringify(asEnvelope4(payload), null, 2)
|
|
7491
|
+
}
|
|
7492
|
+
]
|
|
7040
7493
|
};
|
|
7041
7494
|
}
|
|
7042
7495
|
return {
|
|
@@ -7092,7 +7545,10 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
7092
7545
|
if (!projectId) {
|
|
7093
7546
|
return {
|
|
7094
7547
|
content: [
|
|
7095
|
-
{
|
|
7548
|
+
{
|
|
7549
|
+
type: "text",
|
|
7550
|
+
text: "No project_id provided and no default project found."
|
|
7551
|
+
}
|
|
7096
7552
|
],
|
|
7097
7553
|
isError: true
|
|
7098
7554
|
};
|
|
@@ -7107,7 +7563,12 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
7107
7563
|
const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
|
|
7108
7564
|
if (!membership) {
|
|
7109
7565
|
return {
|
|
7110
|
-
content: [
|
|
7566
|
+
content: [
|
|
7567
|
+
{
|
|
7568
|
+
type: "text",
|
|
7569
|
+
text: "Project is not accessible to current user."
|
|
7570
|
+
}
|
|
7571
|
+
],
|
|
7111
7572
|
isError: true
|
|
7112
7573
|
};
|
|
7113
7574
|
}
|
|
@@ -7123,7 +7584,9 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
7123
7584
|
isError: true
|
|
7124
7585
|
};
|
|
7125
7586
|
}
|
|
7126
|
-
const brandContext = {
|
|
7587
|
+
const brandContext = {
|
|
7588
|
+
...existingProfile.brand_context
|
|
7589
|
+
};
|
|
7127
7590
|
const voiceProfile = brandContext.voiceProfile ?? {};
|
|
7128
7591
|
const platformOverrides = voiceProfile.platformOverrides ?? {};
|
|
7129
7592
|
const existingOverride = platformOverrides[platform3] ?? {};
|
|
@@ -7147,16 +7610,19 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
7147
7610
|
...brandContext,
|
|
7148
7611
|
voiceProfile: updatedVoiceProfile
|
|
7149
7612
|
};
|
|
7150
|
-
const { data: profileId, error: saveError } = await supabase.rpc(
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
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
|
+
);
|
|
7160
7626
|
if (saveError) {
|
|
7161
7627
|
return {
|
|
7162
7628
|
content: [
|
|
@@ -7177,7 +7643,12 @@ Version: ${payload.version ?? "N/A"}`
|
|
|
7177
7643
|
};
|
|
7178
7644
|
if ((response_format || "text") === "json") {
|
|
7179
7645
|
return {
|
|
7180
|
-
content: [
|
|
7646
|
+
content: [
|
|
7647
|
+
{
|
|
7648
|
+
type: "text",
|
|
7649
|
+
text: JSON.stringify(asEnvelope4(payload), null, 2)
|
|
7650
|
+
}
|
|
7651
|
+
],
|
|
7181
7652
|
isError: false
|
|
7182
7653
|
};
|
|
7183
7654
|
}
|
|
@@ -7275,155 +7746,6 @@ async function capturePageScreenshot(page, outputPath, selector) {
|
|
|
7275
7746
|
// src/tools/screenshot.ts
|
|
7276
7747
|
import { resolve, relative } from "node:path";
|
|
7277
7748
|
import { mkdir } from "node:fs/promises";
|
|
7278
|
-
|
|
7279
|
-
// src/lib/ssrf.ts
|
|
7280
|
-
var BLOCKED_IP_PATTERNS = [
|
|
7281
|
-
// IPv4 localhost/loopback
|
|
7282
|
-
/^127\./,
|
|
7283
|
-
/^0\./,
|
|
7284
|
-
// IPv4 private ranges (RFC 1918)
|
|
7285
|
-
/^10\./,
|
|
7286
|
-
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
|
7287
|
-
/^192\.168\./,
|
|
7288
|
-
// IPv4 link-local
|
|
7289
|
-
/^169\.254\./,
|
|
7290
|
-
// Cloud metadata endpoint (AWS, GCP, Azure)
|
|
7291
|
-
/^169\.254\.169\.254$/,
|
|
7292
|
-
// IPv4 broadcast
|
|
7293
|
-
/^255\./,
|
|
7294
|
-
// Shared address space (RFC 6598)
|
|
7295
|
-
/^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./
|
|
7296
|
-
];
|
|
7297
|
-
var BLOCKED_IPV6_PATTERNS = [
|
|
7298
|
-
/^::1$/i,
|
|
7299
|
-
// loopback
|
|
7300
|
-
/^::$/i,
|
|
7301
|
-
// unspecified
|
|
7302
|
-
/^fe[89ab][0-9a-f]:/i,
|
|
7303
|
-
// link-local fe80::/10
|
|
7304
|
-
/^fc[0-9a-f]:/i,
|
|
7305
|
-
// unique local fc00::/7
|
|
7306
|
-
/^fd[0-9a-f]:/i,
|
|
7307
|
-
// unique local fc00::/7
|
|
7308
|
-
/^::ffff:127\./i,
|
|
7309
|
-
// IPv4-mapped localhost
|
|
7310
|
-
/^::ffff:(0|10|127|169\.254|172\.(1[6-9]|2[0-9]|3[0-1])|192\.168)\./i
|
|
7311
|
-
// IPv4-mapped private
|
|
7312
|
-
];
|
|
7313
|
-
var BLOCKED_HOSTNAMES = [
|
|
7314
|
-
"localhost",
|
|
7315
|
-
"localhost.localdomain",
|
|
7316
|
-
"local",
|
|
7317
|
-
"127.0.0.1",
|
|
7318
|
-
"0.0.0.0",
|
|
7319
|
-
"[::1]",
|
|
7320
|
-
"[::ffff:127.0.0.1]",
|
|
7321
|
-
// Cloud metadata endpoints
|
|
7322
|
-
"metadata.google.internal",
|
|
7323
|
-
"metadata.goog",
|
|
7324
|
-
"instance-data",
|
|
7325
|
-
"instance-data.ec2.internal"
|
|
7326
|
-
];
|
|
7327
|
-
var ALLOWED_PROTOCOLS = ["http:", "https:"];
|
|
7328
|
-
var BLOCKED_PORTS = [22, 23, 25, 110, 143, 445, 3306, 5432, 6379, 27017, 11211];
|
|
7329
|
-
function isBlockedIP(ip) {
|
|
7330
|
-
const normalized = ip.replace(/^\[/, "").replace(/\]$/, "");
|
|
7331
|
-
if (normalized.includes(":")) {
|
|
7332
|
-
return BLOCKED_IPV6_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
7333
|
-
}
|
|
7334
|
-
return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
7335
|
-
}
|
|
7336
|
-
function isBlockedHostname(hostname) {
|
|
7337
|
-
return BLOCKED_HOSTNAMES.includes(hostname.toLowerCase());
|
|
7338
|
-
}
|
|
7339
|
-
function isIPAddress(hostname) {
|
|
7340
|
-
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
7341
|
-
const ipv6Pattern = /^\[?[a-fA-F0-9:]+\]?$/;
|
|
7342
|
-
return ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname);
|
|
7343
|
-
}
|
|
7344
|
-
async function validateUrlForSSRF(urlString) {
|
|
7345
|
-
try {
|
|
7346
|
-
const url = new URL(urlString);
|
|
7347
|
-
if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
|
|
7348
|
-
return {
|
|
7349
|
-
isValid: false,
|
|
7350
|
-
error: `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are allowed.`
|
|
7351
|
-
};
|
|
7352
|
-
}
|
|
7353
|
-
if (url.username || url.password) {
|
|
7354
|
-
return {
|
|
7355
|
-
isValid: false,
|
|
7356
|
-
error: "URLs with embedded credentials are not allowed."
|
|
7357
|
-
};
|
|
7358
|
-
}
|
|
7359
|
-
const hostname = url.hostname.toLowerCase();
|
|
7360
|
-
if (isBlockedHostname(hostname)) {
|
|
7361
|
-
return {
|
|
7362
|
-
isValid: false,
|
|
7363
|
-
error: "Access to internal/localhost addresses is not allowed."
|
|
7364
|
-
};
|
|
7365
|
-
}
|
|
7366
|
-
if (isIPAddress(hostname) && isBlockedIP(hostname)) {
|
|
7367
|
-
return {
|
|
7368
|
-
isValid: false,
|
|
7369
|
-
error: "Access to private/internal IP addresses is not allowed."
|
|
7370
|
-
};
|
|
7371
|
-
}
|
|
7372
|
-
const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80;
|
|
7373
|
-
if (BLOCKED_PORTS.includes(port)) {
|
|
7374
|
-
return {
|
|
7375
|
-
isValid: false,
|
|
7376
|
-
error: `Access to port ${port} is not allowed.`
|
|
7377
|
-
};
|
|
7378
|
-
}
|
|
7379
|
-
let resolvedIP;
|
|
7380
|
-
if (!isIPAddress(hostname)) {
|
|
7381
|
-
try {
|
|
7382
|
-
const dns = await import("node:dns");
|
|
7383
|
-
const resolver = new dns.promises.Resolver();
|
|
7384
|
-
const resolvedIPs = [];
|
|
7385
|
-
try {
|
|
7386
|
-
const aRecords = await resolver.resolve4(hostname);
|
|
7387
|
-
resolvedIPs.push(...aRecords);
|
|
7388
|
-
} catch {
|
|
7389
|
-
}
|
|
7390
|
-
try {
|
|
7391
|
-
const aaaaRecords = await resolver.resolve6(hostname);
|
|
7392
|
-
resolvedIPs.push(...aaaaRecords);
|
|
7393
|
-
} catch {
|
|
7394
|
-
}
|
|
7395
|
-
if (resolvedIPs.length === 0) {
|
|
7396
|
-
return {
|
|
7397
|
-
isValid: false,
|
|
7398
|
-
error: "DNS resolution failed: hostname did not resolve to any address."
|
|
7399
|
-
};
|
|
7400
|
-
}
|
|
7401
|
-
for (const ip of resolvedIPs) {
|
|
7402
|
-
if (isBlockedIP(ip)) {
|
|
7403
|
-
return {
|
|
7404
|
-
isValid: false,
|
|
7405
|
-
error: "Hostname resolves to a private/internal IP address."
|
|
7406
|
-
};
|
|
7407
|
-
}
|
|
7408
|
-
}
|
|
7409
|
-
resolvedIP = resolvedIPs[0];
|
|
7410
|
-
} catch {
|
|
7411
|
-
return {
|
|
7412
|
-
isValid: false,
|
|
7413
|
-
error: "DNS resolution failed. Cannot verify hostname safety."
|
|
7414
|
-
};
|
|
7415
|
-
}
|
|
7416
|
-
}
|
|
7417
|
-
return { isValid: true, sanitizedUrl: url.toString(), resolvedIP };
|
|
7418
|
-
} catch (error) {
|
|
7419
|
-
return {
|
|
7420
|
-
isValid: false,
|
|
7421
|
-
error: `Invalid URL format: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
7422
|
-
};
|
|
7423
|
-
}
|
|
7424
|
-
}
|
|
7425
|
-
|
|
7426
|
-
// src/tools/screenshot.ts
|
|
7427
7749
|
init_supabase();
|
|
7428
7750
|
function registerScreenshotTools(server2) {
|
|
7429
7751
|
server2.tool(
|
|
@@ -7999,6 +8321,8 @@ function registerRemotionTools(server2) {
|
|
|
7999
8321
|
// src/tools/insights.ts
|
|
8000
8322
|
init_supabase();
|
|
8001
8323
|
import { z as z8 } from "zod";
|
|
8324
|
+
init_version();
|
|
8325
|
+
var MAX_INSIGHT_AGE_DAYS = 30;
|
|
8002
8326
|
var PLATFORM_ENUM = [
|
|
8003
8327
|
"youtube",
|
|
8004
8328
|
"tiktok",
|
|
@@ -8009,11 +8333,19 @@ var PLATFORM_ENUM = [
|
|
|
8009
8333
|
"threads",
|
|
8010
8334
|
"bluesky"
|
|
8011
8335
|
];
|
|
8012
|
-
var DAY_NAMES = [
|
|
8336
|
+
var DAY_NAMES = [
|
|
8337
|
+
"Sunday",
|
|
8338
|
+
"Monday",
|
|
8339
|
+
"Tuesday",
|
|
8340
|
+
"Wednesday",
|
|
8341
|
+
"Thursday",
|
|
8342
|
+
"Friday",
|
|
8343
|
+
"Saturday"
|
|
8344
|
+
];
|
|
8013
8345
|
function asEnvelope5(data) {
|
|
8014
8346
|
return {
|
|
8015
8347
|
_meta: {
|
|
8016
|
-
version:
|
|
8348
|
+
version: MCP_VERSION,
|
|
8017
8349
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8018
8350
|
},
|
|
8019
8351
|
data
|
|
@@ -8024,7 +8356,12 @@ function registerInsightsTools(server2) {
|
|
|
8024
8356
|
"get_performance_insights",
|
|
8025
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.",
|
|
8026
8358
|
{
|
|
8027
|
-
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."),
|
|
8028
8365
|
days: z8.number().min(1).max(90).optional().describe("Number of days to look back. Defaults to 30. Max 90."),
|
|
8029
8366
|
limit: z8.number().min(1).max(50).optional().describe("Maximum number of insights to return. Defaults to 10."),
|
|
8030
8367
|
response_format: z8.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
@@ -8043,10 +8380,13 @@ function registerInsightsTools(server2) {
|
|
|
8043
8380
|
projectIds.push(...projects.map((p) => p.id));
|
|
8044
8381
|
}
|
|
8045
8382
|
}
|
|
8383
|
+
const effectiveDays = Math.min(lookbackDays, MAX_INSIGHT_AGE_DAYS);
|
|
8046
8384
|
const since = /* @__PURE__ */ new Date();
|
|
8047
|
-
since.setDate(since.getDate() -
|
|
8385
|
+
since.setDate(since.getDate() - effectiveDays);
|
|
8048
8386
|
const sinceIso = since.toISOString();
|
|
8049
|
-
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);
|
|
8050
8390
|
if (projectIds.length > 0) {
|
|
8051
8391
|
query = query.in("project_id", projectIds);
|
|
8052
8392
|
} else {
|
|
@@ -8402,10 +8742,11 @@ function registerYouTubeAnalyticsTools(server2) {
|
|
|
8402
8742
|
init_edge_function();
|
|
8403
8743
|
import { z as z10 } from "zod";
|
|
8404
8744
|
init_supabase();
|
|
8745
|
+
init_version();
|
|
8405
8746
|
function asEnvelope6(data) {
|
|
8406
8747
|
return {
|
|
8407
8748
|
_meta: {
|
|
8408
|
-
version:
|
|
8749
|
+
version: MCP_VERSION,
|
|
8409
8750
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8410
8751
|
},
|
|
8411
8752
|
data
|
|
@@ -8416,7 +8757,9 @@ function registerCommentsTools(server2) {
|
|
|
8416
8757
|
"list_comments",
|
|
8417
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.",
|
|
8418
8759
|
{
|
|
8419
|
-
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
|
+
),
|
|
8420
8763
|
max_results: z10.number().min(1).max(100).optional().describe("Maximum number of comments to return. Defaults to 50."),
|
|
8421
8764
|
page_token: z10.string().optional().describe("Pagination token from a previous list_comments call."),
|
|
8422
8765
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
@@ -8431,7 +8774,9 @@ function registerCommentsTools(server2) {
|
|
|
8431
8774
|
});
|
|
8432
8775
|
if (error) {
|
|
8433
8776
|
return {
|
|
8434
|
-
content: [
|
|
8777
|
+
content: [
|
|
8778
|
+
{ type: "text", text: `Error listing comments: ${error}` }
|
|
8779
|
+
],
|
|
8435
8780
|
isError: true
|
|
8436
8781
|
};
|
|
8437
8782
|
}
|
|
@@ -8443,7 +8788,10 @@ function registerCommentsTools(server2) {
|
|
|
8443
8788
|
{
|
|
8444
8789
|
type: "text",
|
|
8445
8790
|
text: JSON.stringify(
|
|
8446
|
-
asEnvelope6({
|
|
8791
|
+
asEnvelope6({
|
|
8792
|
+
comments,
|
|
8793
|
+
nextPageToken: result.nextPageToken ?? null
|
|
8794
|
+
}),
|
|
8447
8795
|
null,
|
|
8448
8796
|
2
|
|
8449
8797
|
)
|
|
@@ -8480,7 +8828,9 @@ function registerCommentsTools(server2) {
|
|
|
8480
8828
|
"reply_to_comment",
|
|
8481
8829
|
"Reply to a YouTube comment. Requires the parent comment ID and reply text.",
|
|
8482
8830
|
{
|
|
8483
|
-
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
|
+
),
|
|
8484
8834
|
text: z10.string().min(1).describe("The reply text."),
|
|
8485
8835
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8486
8836
|
},
|
|
@@ -8519,7 +8869,12 @@ function registerCommentsTools(server2) {
|
|
|
8519
8869
|
details: { error }
|
|
8520
8870
|
});
|
|
8521
8871
|
return {
|
|
8522
|
-
content: [
|
|
8872
|
+
content: [
|
|
8873
|
+
{
|
|
8874
|
+
type: "text",
|
|
8875
|
+
text: `Error replying to comment: ${error}`
|
|
8876
|
+
}
|
|
8877
|
+
],
|
|
8523
8878
|
isError: true
|
|
8524
8879
|
};
|
|
8525
8880
|
}
|
|
@@ -8532,7 +8887,12 @@ function registerCommentsTools(server2) {
|
|
|
8532
8887
|
});
|
|
8533
8888
|
if (format === "json") {
|
|
8534
8889
|
return {
|
|
8535
|
-
content: [
|
|
8890
|
+
content: [
|
|
8891
|
+
{
|
|
8892
|
+
type: "text",
|
|
8893
|
+
text: JSON.stringify(asEnvelope6(result), null, 2)
|
|
8894
|
+
}
|
|
8895
|
+
]
|
|
8536
8896
|
};
|
|
8537
8897
|
}
|
|
8538
8898
|
return {
|
|
@@ -8590,7 +8950,9 @@ function registerCommentsTools(server2) {
|
|
|
8590
8950
|
details: { error }
|
|
8591
8951
|
});
|
|
8592
8952
|
return {
|
|
8593
|
-
content: [
|
|
8953
|
+
content: [
|
|
8954
|
+
{ type: "text", text: `Error posting comment: ${error}` }
|
|
8955
|
+
],
|
|
8594
8956
|
isError: true
|
|
8595
8957
|
};
|
|
8596
8958
|
}
|
|
@@ -8603,7 +8965,12 @@ function registerCommentsTools(server2) {
|
|
|
8603
8965
|
});
|
|
8604
8966
|
if (format === "json") {
|
|
8605
8967
|
return {
|
|
8606
|
-
content: [
|
|
8968
|
+
content: [
|
|
8969
|
+
{
|
|
8970
|
+
type: "text",
|
|
8971
|
+
text: JSON.stringify(asEnvelope6(result), null, 2)
|
|
8972
|
+
}
|
|
8973
|
+
]
|
|
8607
8974
|
};
|
|
8608
8975
|
}
|
|
8609
8976
|
return {
|
|
@@ -8661,7 +9028,12 @@ function registerCommentsTools(server2) {
|
|
|
8661
9028
|
details: { error }
|
|
8662
9029
|
});
|
|
8663
9030
|
return {
|
|
8664
|
-
content: [
|
|
9031
|
+
content: [
|
|
9032
|
+
{
|
|
9033
|
+
type: "text",
|
|
9034
|
+
text: `Error moderating comment: ${error}`
|
|
9035
|
+
}
|
|
9036
|
+
],
|
|
8665
9037
|
isError: true
|
|
8666
9038
|
};
|
|
8667
9039
|
}
|
|
@@ -8740,7 +9112,9 @@ function registerCommentsTools(server2) {
|
|
|
8740
9112
|
details: { error }
|
|
8741
9113
|
});
|
|
8742
9114
|
return {
|
|
8743
|
-
content: [
|
|
9115
|
+
content: [
|
|
9116
|
+
{ type: "text", text: `Error deleting comment: ${error}` }
|
|
9117
|
+
],
|
|
8744
9118
|
isError: true
|
|
8745
9119
|
};
|
|
8746
9120
|
}
|
|
@@ -8755,13 +9129,22 @@ function registerCommentsTools(server2) {
|
|
|
8755
9129
|
content: [
|
|
8756
9130
|
{
|
|
8757
9131
|
type: "text",
|
|
8758
|
-
text: JSON.stringify(
|
|
9132
|
+
text: JSON.stringify(
|
|
9133
|
+
asEnvelope6({ success: true, commentId: comment_id }),
|
|
9134
|
+
null,
|
|
9135
|
+
2
|
|
9136
|
+
)
|
|
8759
9137
|
}
|
|
8760
9138
|
]
|
|
8761
9139
|
};
|
|
8762
9140
|
}
|
|
8763
9141
|
return {
|
|
8764
|
-
content: [
|
|
9142
|
+
content: [
|
|
9143
|
+
{
|
|
9144
|
+
type: "text",
|
|
9145
|
+
text: `Comment ${comment_id} deleted successfully.`
|
|
9146
|
+
}
|
|
9147
|
+
]
|
|
8765
9148
|
};
|
|
8766
9149
|
}
|
|
8767
9150
|
);
|
|
@@ -8770,6 +9153,7 @@ function registerCommentsTools(server2) {
|
|
|
8770
9153
|
// src/tools/ideation-context.ts
|
|
8771
9154
|
init_supabase();
|
|
8772
9155
|
import { z as z11 } from "zod";
|
|
9156
|
+
init_version();
|
|
8773
9157
|
function transformInsightsToPerformanceContext(projectId, insights) {
|
|
8774
9158
|
if (!insights.length) {
|
|
8775
9159
|
return {
|
|
@@ -8789,8 +9173,12 @@ function transformInsightsToPerformanceContext(projectId, insights) {
|
|
|
8789
9173
|
};
|
|
8790
9174
|
}
|
|
8791
9175
|
const topHooksInsight = insights.find((i) => i.insight_type === "top_hooks");
|
|
8792
|
-
const optimalTimingInsight = insights.find(
|
|
8793
|
-
|
|
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
|
+
);
|
|
8794
9182
|
const topHooks = topHooksInsight?.insight_data?.hooks || [];
|
|
8795
9183
|
const hooksSummary = topHooksInsight?.insight_data?.summary || "";
|
|
8796
9184
|
const timingSummary = optimalTimingInsight?.insight_data?.summary || "";
|
|
@@ -8801,7 +9189,10 @@ function transformInsightsToPerformanceContext(projectId, insights) {
|
|
|
8801
9189
|
if (hooksSummary) promptParts.push(hooksSummary);
|
|
8802
9190
|
if (timingSummary) promptParts.push(timingSummary);
|
|
8803
9191
|
if (modelSummary) promptParts.push(modelSummary);
|
|
8804
|
-
if (topHooks.length)
|
|
9192
|
+
if (topHooks.length)
|
|
9193
|
+
promptParts.push(
|
|
9194
|
+
`Top performing hooks: ${topHooks.slice(0, 3).join(", ")}`
|
|
9195
|
+
);
|
|
8805
9196
|
return {
|
|
8806
9197
|
projectId,
|
|
8807
9198
|
hasHistoricalData: true,
|
|
@@ -8827,7 +9218,7 @@ function transformInsightsToPerformanceContext(projectId, insights) {
|
|
|
8827
9218
|
function asEnvelope7(data) {
|
|
8828
9219
|
return {
|
|
8829
9220
|
_meta: {
|
|
8830
|
-
version:
|
|
9221
|
+
version: MCP_VERSION,
|
|
8831
9222
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8832
9223
|
},
|
|
8833
9224
|
data
|
|
@@ -8850,7 +9241,12 @@ function registerIdeationContextTools(server2) {
|
|
|
8850
9241
|
const { data: member } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).limit(1).single();
|
|
8851
9242
|
if (!member?.organization_id) {
|
|
8852
9243
|
return {
|
|
8853
|
-
content: [
|
|
9244
|
+
content: [
|
|
9245
|
+
{
|
|
9246
|
+
type: "text",
|
|
9247
|
+
text: "No organization found for current user."
|
|
9248
|
+
}
|
|
9249
|
+
],
|
|
8854
9250
|
isError: true
|
|
8855
9251
|
};
|
|
8856
9252
|
}
|
|
@@ -8858,7 +9254,10 @@ function registerIdeationContextTools(server2) {
|
|
|
8858
9254
|
if (projectsError) {
|
|
8859
9255
|
return {
|
|
8860
9256
|
content: [
|
|
8861
|
-
{
|
|
9257
|
+
{
|
|
9258
|
+
type: "text",
|
|
9259
|
+
text: `Failed to resolve projects: ${projectsError.message}`
|
|
9260
|
+
}
|
|
8862
9261
|
],
|
|
8863
9262
|
isError: true
|
|
8864
9263
|
};
|
|
@@ -8866,7 +9265,12 @@ function registerIdeationContextTools(server2) {
|
|
|
8866
9265
|
const projectIds = (projects || []).map((p) => p.id);
|
|
8867
9266
|
if (projectIds.length === 0) {
|
|
8868
9267
|
return {
|
|
8869
|
-
content: [
|
|
9268
|
+
content: [
|
|
9269
|
+
{
|
|
9270
|
+
type: "text",
|
|
9271
|
+
text: "No projects found for current user."
|
|
9272
|
+
}
|
|
9273
|
+
],
|
|
8870
9274
|
isError: true
|
|
8871
9275
|
};
|
|
8872
9276
|
}
|
|
@@ -8875,7 +9279,10 @@ function registerIdeationContextTools(server2) {
|
|
|
8875
9279
|
if (!selectedProjectId) {
|
|
8876
9280
|
return {
|
|
8877
9281
|
content: [
|
|
8878
|
-
{
|
|
9282
|
+
{
|
|
9283
|
+
type: "text",
|
|
9284
|
+
text: "No accessible project found for current user."
|
|
9285
|
+
}
|
|
8879
9286
|
],
|
|
8880
9287
|
isError: true
|
|
8881
9288
|
};
|
|
@@ -8893,7 +9300,9 @@ function registerIdeationContextTools(server2) {
|
|
|
8893
9300
|
}
|
|
8894
9301
|
const since = /* @__PURE__ */ new Date();
|
|
8895
9302
|
since.setDate(since.getDate() - lookbackDays);
|
|
8896
|
-
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);
|
|
8897
9306
|
if (error) {
|
|
8898
9307
|
return {
|
|
8899
9308
|
content: [
|
|
@@ -8911,7 +9320,12 @@ function registerIdeationContextTools(server2) {
|
|
|
8911
9320
|
);
|
|
8912
9321
|
if (format === "json") {
|
|
8913
9322
|
return {
|
|
8914
|
-
content: [
|
|
9323
|
+
content: [
|
|
9324
|
+
{
|
|
9325
|
+
type: "text",
|
|
9326
|
+
text: JSON.stringify(asEnvelope7(context), null, 2)
|
|
9327
|
+
}
|
|
9328
|
+
]
|
|
8915
9329
|
};
|
|
8916
9330
|
}
|
|
8917
9331
|
const lines = [
|
|
@@ -8932,10 +9346,11 @@ function registerIdeationContextTools(server2) {
|
|
|
8932
9346
|
// src/tools/credits.ts
|
|
8933
9347
|
init_supabase();
|
|
8934
9348
|
import { z as z12 } from "zod";
|
|
9349
|
+
init_version();
|
|
8935
9350
|
function asEnvelope8(data) {
|
|
8936
9351
|
return {
|
|
8937
9352
|
_meta: {
|
|
8938
|
-
version:
|
|
9353
|
+
version: MCP_VERSION,
|
|
8939
9354
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8940
9355
|
},
|
|
8941
9356
|
data
|
|
@@ -8974,7 +9389,12 @@ function registerCreditsTools(server2) {
|
|
|
8974
9389
|
};
|
|
8975
9390
|
if ((response_format || "text") === "json") {
|
|
8976
9391
|
return {
|
|
8977
|
-
content: [
|
|
9392
|
+
content: [
|
|
9393
|
+
{
|
|
9394
|
+
type: "text",
|
|
9395
|
+
text: JSON.stringify(asEnvelope8(payload), null, 2)
|
|
9396
|
+
}
|
|
9397
|
+
]
|
|
8978
9398
|
};
|
|
8979
9399
|
}
|
|
8980
9400
|
return {
|
|
@@ -9008,7 +9428,12 @@ Monthly used: ${payload.monthlyUsed}` + (payload.monthlyLimit ? ` / ${payload.mo
|
|
|
9008
9428
|
};
|
|
9009
9429
|
if ((response_format || "text") === "json") {
|
|
9010
9430
|
return {
|
|
9011
|
-
content: [
|
|
9431
|
+
content: [
|
|
9432
|
+
{
|
|
9433
|
+
type: "text",
|
|
9434
|
+
text: JSON.stringify(asEnvelope8(payload), null, 2)
|
|
9435
|
+
}
|
|
9436
|
+
]
|
|
9012
9437
|
};
|
|
9013
9438
|
}
|
|
9014
9439
|
return {
|
|
@@ -9031,11 +9456,12 @@ Assets remaining: ${payload.remainingAssets ?? "unlimited"}`
|
|
|
9031
9456
|
|
|
9032
9457
|
// src/tools/loop-summary.ts
|
|
9033
9458
|
init_supabase();
|
|
9459
|
+
init_version();
|
|
9034
9460
|
import { z as z13 } from "zod";
|
|
9035
9461
|
function asEnvelope9(data) {
|
|
9036
9462
|
return {
|
|
9037
9463
|
_meta: {
|
|
9038
|
-
version:
|
|
9464
|
+
version: MCP_VERSION,
|
|
9039
9465
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9040
9466
|
},
|
|
9041
9467
|
data
|
|
@@ -9074,7 +9500,12 @@ function registerLoopSummaryTools(server2) {
|
|
|
9074
9500
|
const { data: membership } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId).eq("organization_id", project.organization_id).maybeSingle();
|
|
9075
9501
|
if (!membership) {
|
|
9076
9502
|
return {
|
|
9077
|
-
content: [
|
|
9503
|
+
content: [
|
|
9504
|
+
{
|
|
9505
|
+
type: "text",
|
|
9506
|
+
text: "Project is not accessible to current user."
|
|
9507
|
+
}
|
|
9508
|
+
],
|
|
9078
9509
|
isError: true
|
|
9079
9510
|
};
|
|
9080
9511
|
}
|
|
@@ -9097,7 +9528,12 @@ function registerLoopSummaryTools(server2) {
|
|
|
9097
9528
|
};
|
|
9098
9529
|
if ((response_format || "text") === "json") {
|
|
9099
9530
|
return {
|
|
9100
|
-
content: [
|
|
9531
|
+
content: [
|
|
9532
|
+
{
|
|
9533
|
+
type: "text",
|
|
9534
|
+
text: JSON.stringify(asEnvelope9(payload), null, 2)
|
|
9535
|
+
}
|
|
9536
|
+
]
|
|
9101
9537
|
};
|
|
9102
9538
|
}
|
|
9103
9539
|
return {
|
|
@@ -9427,8 +9863,12 @@ ${"=".repeat(40)}
|
|
|
9427
9863
|
init_edge_function();
|
|
9428
9864
|
init_supabase();
|
|
9429
9865
|
import { z as z16 } from "zod";
|
|
9866
|
+
init_version();
|
|
9430
9867
|
function asEnvelope12(data) {
|
|
9431
|
-
return {
|
|
9868
|
+
return {
|
|
9869
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
9870
|
+
data
|
|
9871
|
+
};
|
|
9432
9872
|
}
|
|
9433
9873
|
function isYouTubeUrl(url) {
|
|
9434
9874
|
if (/youtube\.com\/watch|youtu\.be\//.test(url)) return "video";
|
|
@@ -9459,13 +9899,17 @@ Metadata:`);
|
|
|
9459
9899
|
if (m.tags?.length) lines.push(` Tags: ${m.tags.join(", ")}`);
|
|
9460
9900
|
}
|
|
9461
9901
|
if (content.features?.length)
|
|
9462
|
-
lines.push(
|
|
9902
|
+
lines.push(
|
|
9903
|
+
`
|
|
9463
9904
|
Features:
|
|
9464
|
-
${content.features.map((f) => ` - ${f}`).join("\n")}`
|
|
9905
|
+
${content.features.map((f) => ` - ${f}`).join("\n")}`
|
|
9906
|
+
);
|
|
9465
9907
|
if (content.benefits?.length)
|
|
9466
|
-
lines.push(
|
|
9908
|
+
lines.push(
|
|
9909
|
+
`
|
|
9467
9910
|
Benefits:
|
|
9468
|
-
${content.benefits.map((b) => ` - ${b}`).join("\n")}`
|
|
9911
|
+
${content.benefits.map((b) => ` - ${b}`).join("\n")}`
|
|
9912
|
+
);
|
|
9469
9913
|
if (content.usp) lines.push(`
|
|
9470
9914
|
USP: ${content.usp}`);
|
|
9471
9915
|
if (content.suggested_hooks?.length)
|
|
@@ -9487,12 +9931,20 @@ function registerExtractionTools(server2) {
|
|
|
9487
9931
|
max_results: z16.number().min(1).max(100).default(10).describe("Max comments to include"),
|
|
9488
9932
|
response_format: z16.enum(["text", "json"]).default("text")
|
|
9489
9933
|
},
|
|
9490
|
-
async ({
|
|
9934
|
+
async ({
|
|
9935
|
+
url,
|
|
9936
|
+
extract_type,
|
|
9937
|
+
include_comments,
|
|
9938
|
+
max_results,
|
|
9939
|
+
response_format
|
|
9940
|
+
}) => {
|
|
9491
9941
|
const startedAt = Date.now();
|
|
9492
9942
|
const ssrfCheck = await validateUrlForSSRF(url);
|
|
9493
9943
|
if (!ssrfCheck.isValid) {
|
|
9494
9944
|
return {
|
|
9495
|
-
content: [
|
|
9945
|
+
content: [
|
|
9946
|
+
{ type: "text", text: `URL blocked: ${ssrfCheck.error}` }
|
|
9947
|
+
],
|
|
9496
9948
|
isError: true
|
|
9497
9949
|
};
|
|
9498
9950
|
}
|
|
@@ -9620,13 +10072,21 @@ function registerExtractionTools(server2) {
|
|
|
9620
10072
|
if (response_format === "json") {
|
|
9621
10073
|
return {
|
|
9622
10074
|
content: [
|
|
9623
|
-
{
|
|
10075
|
+
{
|
|
10076
|
+
type: "text",
|
|
10077
|
+
text: JSON.stringify(asEnvelope12(extracted), null, 2)
|
|
10078
|
+
}
|
|
9624
10079
|
],
|
|
9625
10080
|
isError: false
|
|
9626
10081
|
};
|
|
9627
10082
|
}
|
|
9628
10083
|
return {
|
|
9629
|
-
content: [
|
|
10084
|
+
content: [
|
|
10085
|
+
{
|
|
10086
|
+
type: "text",
|
|
10087
|
+
text: formatExtractedContentAsText(extracted)
|
|
10088
|
+
}
|
|
10089
|
+
],
|
|
9630
10090
|
isError: false
|
|
9631
10091
|
};
|
|
9632
10092
|
} catch (err) {
|
|
@@ -9639,7 +10099,9 @@ function registerExtractionTools(server2) {
|
|
|
9639
10099
|
details: { url, error: message }
|
|
9640
10100
|
});
|
|
9641
10101
|
return {
|
|
9642
|
-
content: [
|
|
10102
|
+
content: [
|
|
10103
|
+
{ type: "text", text: `Extraction failed: ${message}` }
|
|
10104
|
+
],
|
|
9643
10105
|
isError: true
|
|
9644
10106
|
};
|
|
9645
10107
|
}
|
|
@@ -9650,9 +10112,13 @@ function registerExtractionTools(server2) {
|
|
|
9650
10112
|
// src/tools/quality.ts
|
|
9651
10113
|
init_quality();
|
|
9652
10114
|
init_supabase();
|
|
10115
|
+
init_version();
|
|
9653
10116
|
import { z as z17 } from "zod";
|
|
9654
10117
|
function asEnvelope13(data) {
|
|
9655
|
-
return {
|
|
10118
|
+
return {
|
|
10119
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
10120
|
+
data
|
|
10121
|
+
};
|
|
9656
10122
|
}
|
|
9657
10123
|
function registerQualityTools(server2) {
|
|
9658
10124
|
server2.tool(
|
|
@@ -9708,7 +10174,12 @@ function registerQualityTools(server2) {
|
|
|
9708
10174
|
});
|
|
9709
10175
|
if (response_format === "json") {
|
|
9710
10176
|
return {
|
|
9711
|
-
content: [
|
|
10177
|
+
content: [
|
|
10178
|
+
{
|
|
10179
|
+
type: "text",
|
|
10180
|
+
text: JSON.stringify(asEnvelope13(result), null, 2)
|
|
10181
|
+
}
|
|
10182
|
+
],
|
|
9712
10183
|
isError: false
|
|
9713
10184
|
};
|
|
9714
10185
|
}
|
|
@@ -9718,7 +10189,9 @@ function registerQualityTools(server2) {
|
|
|
9718
10189
|
);
|
|
9719
10190
|
lines.push("");
|
|
9720
10191
|
for (const cat of result.categories) {
|
|
9721
|
-
lines.push(
|
|
10192
|
+
lines.push(
|
|
10193
|
+
` ${cat.name}: ${cat.score}/${cat.maxScore} \u2014 ${cat.detail}`
|
|
10194
|
+
);
|
|
9722
10195
|
}
|
|
9723
10196
|
if (result.blockers.length > 0) {
|
|
9724
10197
|
lines.push("");
|
|
@@ -9729,7 +10202,10 @@ function registerQualityTools(server2) {
|
|
|
9729
10202
|
}
|
|
9730
10203
|
lines.push("");
|
|
9731
10204
|
lines.push(`Threshold: ${result.threshold}/${result.maxTotal}`);
|
|
9732
|
-
return {
|
|
10205
|
+
return {
|
|
10206
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
10207
|
+
isError: false
|
|
10208
|
+
};
|
|
9733
10209
|
}
|
|
9734
10210
|
);
|
|
9735
10211
|
server2.tool(
|
|
@@ -9770,7 +10246,9 @@ function registerQualityTools(server2) {
|
|
|
9770
10246
|
});
|
|
9771
10247
|
const scores = postsWithQuality.map((p) => p.quality.score);
|
|
9772
10248
|
const passed = postsWithQuality.filter((p) => p.quality.passed).length;
|
|
9773
|
-
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;
|
|
9774
10252
|
const summary = {
|
|
9775
10253
|
total_posts: plan.posts.length,
|
|
9776
10254
|
passed,
|
|
@@ -9789,25 +10267,36 @@ function registerQualityTools(server2) {
|
|
|
9789
10267
|
content: [
|
|
9790
10268
|
{
|
|
9791
10269
|
type: "text",
|
|
9792
|
-
text: JSON.stringify(
|
|
10270
|
+
text: JSON.stringify(
|
|
10271
|
+
asEnvelope13({ posts: postsWithQuality, summary }),
|
|
10272
|
+
null,
|
|
10273
|
+
2
|
|
10274
|
+
)
|
|
9793
10275
|
}
|
|
9794
10276
|
],
|
|
9795
10277
|
isError: false
|
|
9796
10278
|
};
|
|
9797
10279
|
}
|
|
9798
10280
|
const lines = [];
|
|
9799
|
-
lines.push(
|
|
10281
|
+
lines.push(
|
|
10282
|
+
`PLAN QUALITY: ${passed}/${plan.posts.length} passed (avg: ${avgScore}/35)`
|
|
10283
|
+
);
|
|
9800
10284
|
lines.push("");
|
|
9801
10285
|
for (const post of postsWithQuality) {
|
|
9802
10286
|
const icon = post.quality.passed ? "[PASS]" : "[FAIL]";
|
|
9803
|
-
lines.push(
|
|
10287
|
+
lines.push(
|
|
10288
|
+
`${icon} ${post.id} | ${post.platform} | ${post.quality.score}/35`
|
|
10289
|
+
);
|
|
9804
10290
|
if (post.quality.blockers.length > 0) {
|
|
9805
10291
|
for (const b of post.quality.blockers) {
|
|
9806
10292
|
lines.push(` - ${b}`);
|
|
9807
10293
|
}
|
|
9808
10294
|
}
|
|
9809
10295
|
}
|
|
9810
|
-
return {
|
|
10296
|
+
return {
|
|
10297
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
10298
|
+
isError: false
|
|
10299
|
+
};
|
|
9811
10300
|
}
|
|
9812
10301
|
);
|
|
9813
10302
|
}
|
|
@@ -9817,11 +10306,15 @@ init_edge_function();
|
|
|
9817
10306
|
init_supabase();
|
|
9818
10307
|
import { z as z18 } from "zod";
|
|
9819
10308
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10309
|
+
init_version();
|
|
9820
10310
|
function toRecord(value) {
|
|
9821
10311
|
return value && typeof value === "object" ? value : void 0;
|
|
9822
10312
|
}
|
|
9823
10313
|
function asEnvelope14(data) {
|
|
9824
|
-
return {
|
|
10314
|
+
return {
|
|
10315
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
10316
|
+
data
|
|
10317
|
+
};
|
|
9825
10318
|
}
|
|
9826
10319
|
function tomorrowIsoDate() {
|
|
9827
10320
|
const d = /* @__PURE__ */ new Date();
|
|
@@ -9859,7 +10352,9 @@ function formatPlanAsText(plan) {
|
|
|
9859
10352
|
lines.push(`WEEKLY CONTENT PLAN: "${plan.topic}"`);
|
|
9860
10353
|
lines.push(`Period: ${plan.start_date} to ${plan.end_date}`);
|
|
9861
10354
|
lines.push(`Platforms: ${plan.platforms.join(", ")}`);
|
|
9862
|
-
lines.push(
|
|
10355
|
+
lines.push(
|
|
10356
|
+
`Posts: ${plan.posts.length} | Estimated credits: ~${plan.estimated_credits}`
|
|
10357
|
+
);
|
|
9863
10358
|
if (plan.plan_id) lines.push(`Plan ID: ${plan.plan_id}`);
|
|
9864
10359
|
if (plan.insights_applied?.has_historical_data) {
|
|
9865
10360
|
lines.push("");
|
|
@@ -9874,7 +10369,9 @@ function formatPlanAsText(plan) {
|
|
|
9874
10369
|
`- Best posting time: ${days[timing.dayOfWeek] ?? timing.dayOfWeek} ${timing.hourOfDay}:00`
|
|
9875
10370
|
);
|
|
9876
10371
|
}
|
|
9877
|
-
lines.push(
|
|
10372
|
+
lines.push(
|
|
10373
|
+
`- Recommended model: ${plan.insights_applied.recommended_model ?? "N/A"}`
|
|
10374
|
+
);
|
|
9878
10375
|
lines.push(`- Insights count: ${plan.insights_applied.insights_count}`);
|
|
9879
10376
|
}
|
|
9880
10377
|
lines.push("");
|
|
@@ -9895,9 +10392,11 @@ function formatPlanAsText(plan) {
|
|
|
9895
10392
|
` Caption: ${post.caption.slice(0, 200)}${post.caption.length > 200 ? "..." : ""}`
|
|
9896
10393
|
);
|
|
9897
10394
|
if (post.title) lines.push(` Title: ${post.title}`);
|
|
9898
|
-
if (post.visual_direction)
|
|
10395
|
+
if (post.visual_direction)
|
|
10396
|
+
lines.push(` Visual: ${post.visual_direction}`);
|
|
9899
10397
|
if (post.media_type) lines.push(` Media: ${post.media_type}`);
|
|
9900
|
-
if (post.hashtags?.length)
|
|
10398
|
+
if (post.hashtags?.length)
|
|
10399
|
+
lines.push(` Hashtags: ${post.hashtags.join(" ")}`);
|
|
9901
10400
|
lines.push("");
|
|
9902
10401
|
}
|
|
9903
10402
|
}
|
|
@@ -9980,7 +10479,10 @@ function registerPlanningTools(server2) {
|
|
|
9980
10479
|
"mcp-data",
|
|
9981
10480
|
{
|
|
9982
10481
|
action: "brand-profile",
|
|
9983
|
-
...resolvedProjectId ? {
|
|
10482
|
+
...resolvedProjectId ? {
|
|
10483
|
+
projectId: resolvedProjectId,
|
|
10484
|
+
project_id: resolvedProjectId
|
|
10485
|
+
} : {}
|
|
9984
10486
|
},
|
|
9985
10487
|
{ timeoutMs: 15e3 }
|
|
9986
10488
|
);
|
|
@@ -10184,7 +10686,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10184
10686
|
details: { topic, error: `plan persistence failed: ${message}` }
|
|
10185
10687
|
});
|
|
10186
10688
|
return {
|
|
10187
|
-
content: [
|
|
10689
|
+
content: [
|
|
10690
|
+
{
|
|
10691
|
+
type: "text",
|
|
10692
|
+
text: `Plan persistence failed: ${message}`
|
|
10693
|
+
}
|
|
10694
|
+
],
|
|
10188
10695
|
isError: true
|
|
10189
10696
|
};
|
|
10190
10697
|
}
|
|
@@ -10198,7 +10705,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10198
10705
|
});
|
|
10199
10706
|
if (response_format === "json") {
|
|
10200
10707
|
return {
|
|
10201
|
-
content: [
|
|
10708
|
+
content: [
|
|
10709
|
+
{
|
|
10710
|
+
type: "text",
|
|
10711
|
+
text: JSON.stringify(asEnvelope14(plan), null, 2)
|
|
10712
|
+
}
|
|
10713
|
+
],
|
|
10202
10714
|
isError: false
|
|
10203
10715
|
};
|
|
10204
10716
|
}
|
|
@@ -10216,7 +10728,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10216
10728
|
details: { topic, error: message }
|
|
10217
10729
|
});
|
|
10218
10730
|
return {
|
|
10219
|
-
content: [
|
|
10731
|
+
content: [
|
|
10732
|
+
{
|
|
10733
|
+
type: "text",
|
|
10734
|
+
text: `Plan generation failed: ${message}`
|
|
10735
|
+
}
|
|
10736
|
+
],
|
|
10220
10737
|
isError: true
|
|
10221
10738
|
};
|
|
10222
10739
|
}
|
|
@@ -10276,7 +10793,11 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10276
10793
|
toolName: "save_content_plan",
|
|
10277
10794
|
status: "success",
|
|
10278
10795
|
durationMs,
|
|
10279
|
-
details: {
|
|
10796
|
+
details: {
|
|
10797
|
+
plan_id: planId,
|
|
10798
|
+
project_id: resolvedProjectId,
|
|
10799
|
+
status: normalizedStatus
|
|
10800
|
+
}
|
|
10280
10801
|
});
|
|
10281
10802
|
const result = {
|
|
10282
10803
|
plan_id: planId,
|
|
@@ -10285,13 +10806,21 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10285
10806
|
};
|
|
10286
10807
|
if (response_format === "json") {
|
|
10287
10808
|
return {
|
|
10288
|
-
content: [
|
|
10809
|
+
content: [
|
|
10810
|
+
{
|
|
10811
|
+
type: "text",
|
|
10812
|
+
text: JSON.stringify(asEnvelope14(result), null, 2)
|
|
10813
|
+
}
|
|
10814
|
+
],
|
|
10289
10815
|
isError: false
|
|
10290
10816
|
};
|
|
10291
10817
|
}
|
|
10292
10818
|
return {
|
|
10293
10819
|
content: [
|
|
10294
|
-
{
|
|
10820
|
+
{
|
|
10821
|
+
type: "text",
|
|
10822
|
+
text: `Saved content plan ${planId} (${normalizedStatus}).`
|
|
10823
|
+
}
|
|
10295
10824
|
],
|
|
10296
10825
|
isError: false
|
|
10297
10826
|
};
|
|
@@ -10305,7 +10834,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10305
10834
|
details: { error: message }
|
|
10306
10835
|
});
|
|
10307
10836
|
return {
|
|
10308
|
-
content: [
|
|
10837
|
+
content: [
|
|
10838
|
+
{
|
|
10839
|
+
type: "text",
|
|
10840
|
+
text: `Failed to save content plan: ${message}`
|
|
10841
|
+
}
|
|
10842
|
+
],
|
|
10309
10843
|
isError: true
|
|
10310
10844
|
};
|
|
10311
10845
|
}
|
|
@@ -10321,7 +10855,9 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10321
10855
|
async ({ plan_id, response_format }) => {
|
|
10322
10856
|
const supabase = getSupabaseClient();
|
|
10323
10857
|
const userId = await getDefaultUserId();
|
|
10324
|
-
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();
|
|
10325
10861
|
if (error) {
|
|
10326
10862
|
return {
|
|
10327
10863
|
content: [
|
|
@@ -10336,7 +10872,10 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10336
10872
|
if (!data) {
|
|
10337
10873
|
return {
|
|
10338
10874
|
content: [
|
|
10339
|
-
{
|
|
10875
|
+
{
|
|
10876
|
+
type: "text",
|
|
10877
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
10878
|
+
}
|
|
10340
10879
|
],
|
|
10341
10880
|
isError: true
|
|
10342
10881
|
};
|
|
@@ -10352,7 +10891,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10352
10891
|
};
|
|
10353
10892
|
if (response_format === "json") {
|
|
10354
10893
|
return {
|
|
10355
|
-
content: [
|
|
10894
|
+
content: [
|
|
10895
|
+
{
|
|
10896
|
+
type: "text",
|
|
10897
|
+
text: JSON.stringify(asEnvelope14(payload), null, 2)
|
|
10898
|
+
}
|
|
10899
|
+
],
|
|
10356
10900
|
isError: false
|
|
10357
10901
|
};
|
|
10358
10902
|
}
|
|
@@ -10363,7 +10907,10 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10363
10907
|
`Status: ${data.status}`,
|
|
10364
10908
|
`Posts: ${Array.isArray(plan?.posts) ? plan.posts.length : 0}`
|
|
10365
10909
|
];
|
|
10366
|
-
return {
|
|
10910
|
+
return {
|
|
10911
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
10912
|
+
isError: false
|
|
10913
|
+
};
|
|
10367
10914
|
}
|
|
10368
10915
|
);
|
|
10369
10916
|
server2.tool(
|
|
@@ -10406,14 +10953,19 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10406
10953
|
if (!stored?.plan_payload) {
|
|
10407
10954
|
return {
|
|
10408
10955
|
content: [
|
|
10409
|
-
{
|
|
10956
|
+
{
|
|
10957
|
+
type: "text",
|
|
10958
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
10959
|
+
}
|
|
10410
10960
|
],
|
|
10411
10961
|
isError: true
|
|
10412
10962
|
};
|
|
10413
10963
|
}
|
|
10414
10964
|
const plan = stored.plan_payload;
|
|
10415
10965
|
const existingPosts = Array.isArray(plan.posts) ? plan.posts : [];
|
|
10416
|
-
const updatesById = new Map(
|
|
10966
|
+
const updatesById = new Map(
|
|
10967
|
+
post_updates.map((update) => [update.post_id, update])
|
|
10968
|
+
);
|
|
10417
10969
|
const updatedPosts = existingPosts.map((post) => {
|
|
10418
10970
|
const update = updatesById.get(post.id);
|
|
10419
10971
|
if (!update) return post;
|
|
@@ -10431,7 +10983,9 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10431
10983
|
...update.status !== void 0 ? { status: update.status } : {}
|
|
10432
10984
|
};
|
|
10433
10985
|
});
|
|
10434
|
-
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";
|
|
10435
10989
|
const updatedPlan = {
|
|
10436
10990
|
...plan,
|
|
10437
10991
|
posts: updatedPosts
|
|
@@ -10458,7 +11012,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10458
11012
|
};
|
|
10459
11013
|
if (response_format === "json") {
|
|
10460
11014
|
return {
|
|
10461
|
-
content: [
|
|
11015
|
+
content: [
|
|
11016
|
+
{
|
|
11017
|
+
type: "text",
|
|
11018
|
+
text: JSON.stringify(asEnvelope14(payload), null, 2)
|
|
11019
|
+
}
|
|
11020
|
+
],
|
|
10462
11021
|
isError: false
|
|
10463
11022
|
};
|
|
10464
11023
|
}
|
|
@@ -10498,7 +11057,10 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10498
11057
|
if (!stored?.plan_payload || !stored.project_id) {
|
|
10499
11058
|
return {
|
|
10500
11059
|
content: [
|
|
10501
|
-
{
|
|
11060
|
+
{
|
|
11061
|
+
type: "text",
|
|
11062
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
11063
|
+
}
|
|
10502
11064
|
],
|
|
10503
11065
|
isError: true
|
|
10504
11066
|
};
|
|
@@ -10507,7 +11069,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10507
11069
|
const posts = Array.isArray(plan.posts) ? plan.posts : [];
|
|
10508
11070
|
if (posts.length === 0) {
|
|
10509
11071
|
return {
|
|
10510
|
-
content: [
|
|
11072
|
+
content: [
|
|
11073
|
+
{
|
|
11074
|
+
type: "text",
|
|
11075
|
+
text: `Plan ${plan_id} has no posts to submit.`
|
|
11076
|
+
}
|
|
11077
|
+
],
|
|
10511
11078
|
isError: true
|
|
10512
11079
|
};
|
|
10513
11080
|
}
|
|
@@ -10550,7 +11117,12 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10550
11117
|
};
|
|
10551
11118
|
if (response_format === "json") {
|
|
10552
11119
|
return {
|
|
10553
|
-
content: [
|
|
11120
|
+
content: [
|
|
11121
|
+
{
|
|
11122
|
+
type: "text",
|
|
11123
|
+
text: JSON.stringify(asEnvelope14(payload), null, 2)
|
|
11124
|
+
}
|
|
11125
|
+
],
|
|
10554
11126
|
isError: false
|
|
10555
11127
|
};
|
|
10556
11128
|
}
|
|
@@ -10570,10 +11142,11 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10570
11142
|
// src/tools/plan-approvals.ts
|
|
10571
11143
|
init_supabase();
|
|
10572
11144
|
import { z as z19 } from "zod";
|
|
11145
|
+
init_version();
|
|
10573
11146
|
function asEnvelope15(data) {
|
|
10574
11147
|
return {
|
|
10575
11148
|
_meta: {
|
|
10576
|
-
version:
|
|
11149
|
+
version: MCP_VERSION,
|
|
10577
11150
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10578
11151
|
},
|
|
10579
11152
|
data
|
|
@@ -10612,14 +11185,24 @@ function registerPlanApprovalTools(server2) {
|
|
|
10612
11185
|
if (!projectId) {
|
|
10613
11186
|
return {
|
|
10614
11187
|
content: [
|
|
10615
|
-
{
|
|
11188
|
+
{
|
|
11189
|
+
type: "text",
|
|
11190
|
+
text: "No project_id provided and no default project found."
|
|
11191
|
+
}
|
|
10616
11192
|
],
|
|
10617
11193
|
isError: true
|
|
10618
11194
|
};
|
|
10619
11195
|
}
|
|
10620
|
-
const accessError = await assertProjectAccess(
|
|
11196
|
+
const accessError = await assertProjectAccess(
|
|
11197
|
+
supabase,
|
|
11198
|
+
userId,
|
|
11199
|
+
projectId
|
|
11200
|
+
);
|
|
10621
11201
|
if (accessError) {
|
|
10622
|
-
return {
|
|
11202
|
+
return {
|
|
11203
|
+
content: [{ type: "text", text: accessError }],
|
|
11204
|
+
isError: true
|
|
11205
|
+
};
|
|
10623
11206
|
}
|
|
10624
11207
|
const rows = posts.map((post) => ({
|
|
10625
11208
|
plan_id,
|
|
@@ -10648,7 +11231,12 @@ function registerPlanApprovalTools(server2) {
|
|
|
10648
11231
|
};
|
|
10649
11232
|
if ((response_format || "text") === "json") {
|
|
10650
11233
|
return {
|
|
10651
|
-
content: [
|
|
11234
|
+
content: [
|
|
11235
|
+
{
|
|
11236
|
+
type: "text",
|
|
11237
|
+
text: JSON.stringify(asEnvelope15(payload), null, 2)
|
|
11238
|
+
}
|
|
11239
|
+
],
|
|
10652
11240
|
isError: false
|
|
10653
11241
|
};
|
|
10654
11242
|
}
|
|
@@ -10697,14 +11285,22 @@ function registerPlanApprovalTools(server2) {
|
|
|
10697
11285
|
};
|
|
10698
11286
|
if ((response_format || "text") === "json") {
|
|
10699
11287
|
return {
|
|
10700
|
-
content: [
|
|
11288
|
+
content: [
|
|
11289
|
+
{
|
|
11290
|
+
type: "text",
|
|
11291
|
+
text: JSON.stringify(asEnvelope15(payload), null, 2)
|
|
11292
|
+
}
|
|
11293
|
+
],
|
|
10701
11294
|
isError: false
|
|
10702
11295
|
};
|
|
10703
11296
|
}
|
|
10704
11297
|
if (!data || data.length === 0) {
|
|
10705
11298
|
return {
|
|
10706
11299
|
content: [
|
|
10707
|
-
{
|
|
11300
|
+
{
|
|
11301
|
+
type: "text",
|
|
11302
|
+
text: `No approval items found for plan ${plan_id}.`
|
|
11303
|
+
}
|
|
10708
11304
|
],
|
|
10709
11305
|
isError: false
|
|
10710
11306
|
};
|
|
@@ -10717,7 +11313,10 @@ function registerPlanApprovalTools(server2) {
|
|
|
10717
11313
|
}
|
|
10718
11314
|
lines.push("");
|
|
10719
11315
|
lines.push(`Total: ${data.length}`);
|
|
10720
|
-
return {
|
|
11316
|
+
return {
|
|
11317
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
11318
|
+
isError: false
|
|
11319
|
+
};
|
|
10721
11320
|
}
|
|
10722
11321
|
);
|
|
10723
11322
|
server2.tool(
|
|
@@ -10736,7 +11335,10 @@ function registerPlanApprovalTools(server2) {
|
|
|
10736
11335
|
if (decision === "edited" && !edited_post) {
|
|
10737
11336
|
return {
|
|
10738
11337
|
content: [
|
|
10739
|
-
{
|
|
11338
|
+
{
|
|
11339
|
+
type: "text",
|
|
11340
|
+
text: 'edited_post is required when decision is "edited".'
|
|
11341
|
+
}
|
|
10740
11342
|
],
|
|
10741
11343
|
isError: true
|
|
10742
11344
|
};
|
|
@@ -10749,7 +11351,9 @@ function registerPlanApprovalTools(server2) {
|
|
|
10749
11351
|
if (decision === "edited") {
|
|
10750
11352
|
updates.edited_post = edited_post;
|
|
10751
11353
|
}
|
|
10752
|
-
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();
|
|
10753
11357
|
if (error) {
|
|
10754
11358
|
return {
|
|
10755
11359
|
content: [
|
|
@@ -10774,7 +11378,12 @@ function registerPlanApprovalTools(server2) {
|
|
|
10774
11378
|
}
|
|
10775
11379
|
if ((response_format || "text") === "json") {
|
|
10776
11380
|
return {
|
|
10777
|
-
content: [
|
|
11381
|
+
content: [
|
|
11382
|
+
{
|
|
11383
|
+
type: "text",
|
|
11384
|
+
text: JSON.stringify(asEnvelope15(data), null, 2)
|
|
11385
|
+
}
|
|
11386
|
+
],
|
|
10778
11387
|
isError: false
|
|
10779
11388
|
};
|
|
10780
11389
|
}
|
|
@@ -10914,16 +11523,40 @@ function registerAllTools(server2, options) {
|
|
|
10914
11523
|
init_posthog();
|
|
10915
11524
|
init_supabase();
|
|
10916
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
|
+
}
|
|
10917
11550
|
process.on("uncaughtException", (err) => {
|
|
10918
11551
|
process.stderr.write(`MCP server error: ${err.message}
|
|
10919
11552
|
`);
|
|
10920
|
-
|
|
11553
|
+
flushAndExit(1);
|
|
10921
11554
|
});
|
|
10922
11555
|
process.on("unhandledRejection", (reason) => {
|
|
10923
11556
|
const message = reason instanceof Error ? reason.message : String(reason);
|
|
10924
11557
|
process.stderr.write(`MCP server error: ${message}
|
|
10925
11558
|
`);
|
|
10926
|
-
|
|
11559
|
+
flushAndExit(1);
|
|
10927
11560
|
});
|
|
10928
11561
|
var command = process.argv[2];
|
|
10929
11562
|
if (command === "--version" || command === "-v") {
|
|
@@ -10932,7 +11565,11 @@ if (command === "--version" || command === "-v") {
|
|
|
10932
11565
|
const { fileURLToPath } = await import("node:url");
|
|
10933
11566
|
let version = MCP_VERSION;
|
|
10934
11567
|
try {
|
|
10935
|
-
const pkgPath = resolve3(
|
|
11568
|
+
const pkgPath = resolve3(
|
|
11569
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
11570
|
+
"..",
|
|
11571
|
+
"package.json"
|
|
11572
|
+
);
|
|
10936
11573
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
|
|
10937
11574
|
version = pkg.version;
|
|
10938
11575
|
} catch {
|
|
@@ -10964,7 +11601,11 @@ if (command === "--help" || command === "-h") {
|
|
|
10964
11601
|
ok: true,
|
|
10965
11602
|
command: "help",
|
|
10966
11603
|
commands: [
|
|
10967
|
-
{
|
|
11604
|
+
{
|
|
11605
|
+
name: "setup",
|
|
11606
|
+
aliases: ["login"],
|
|
11607
|
+
description: "Interactive OAuth setup"
|
|
11608
|
+
},
|
|
10968
11609
|
{ name: "logout", description: "Remove credentials" },
|
|
10969
11610
|
{ name: "whoami", description: "Show auth info" },
|
|
10970
11611
|
{ name: "health", description: "Check connectivity" },
|
|
@@ -11053,10 +11694,14 @@ if (command === "sn") {
|
|
|
11053
11694
|
await runSnCli(process.argv.slice(3));
|
|
11054
11695
|
process.exit(0);
|
|
11055
11696
|
}
|
|
11056
|
-
if (command && !["setup", "login", "logout", "whoami", "health", "sn", "repl"].includes(
|
|
11057
|
-
|
|
11697
|
+
if (command && !["setup", "login", "logout", "whoami", "health", "sn", "repl"].includes(
|
|
11698
|
+
command
|
|
11699
|
+
)) {
|
|
11700
|
+
process.stderr.write(
|
|
11701
|
+
`Unknown command: ${command}
|
|
11058
11702
|
Run socialneuron-mcp --help for usage.
|
|
11059
|
-
`
|
|
11703
|
+
`
|
|
11704
|
+
);
|
|
11060
11705
|
process.exit(1);
|
|
11061
11706
|
}
|
|
11062
11707
|
await initializeAuth();
|