@socialneuron/mcp-server 1.7.18 → 1.8.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 +32 -1
- package/README.md +8 -8
- package/dist/http.js +9204 -8671
- package/dist/index.js +9372 -8932
- package/dist/sn.js +635 -195
- package/package.json +6 -4
- package/tools.lock.json +36 -36
package/dist/sn.js
CHANGED
|
@@ -19,7 +19,7 @@ var MCP_VERSION;
|
|
|
19
19
|
var init_version = __esm({
|
|
20
20
|
"src/lib/version.ts"() {
|
|
21
21
|
"use strict";
|
|
22
|
-
MCP_VERSION = "1.
|
|
22
|
+
MCP_VERSION = "1.8.1";
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -34,6 +34,20 @@ function getRequestScopes() {
|
|
|
34
34
|
function getRequestToken() {
|
|
35
35
|
return requestContext.getStore()?.token ?? null;
|
|
36
36
|
}
|
|
37
|
+
function getRequestSurface() {
|
|
38
|
+
return requestContext.getStore()?.surface ?? null;
|
|
39
|
+
}
|
|
40
|
+
function getRequestProjectId() {
|
|
41
|
+
return requestContext.getStore()?.projectId ?? null;
|
|
42
|
+
}
|
|
43
|
+
function resolveSurface() {
|
|
44
|
+
const ctx = getRequestSurface();
|
|
45
|
+
if (ctx) return ctx;
|
|
46
|
+
const transport = process.env.MCP_TRANSPORT;
|
|
47
|
+
if (transport === "stdio") return "mcp-stdio";
|
|
48
|
+
if (transport === "http") return "mcp-http";
|
|
49
|
+
return "cli";
|
|
50
|
+
}
|
|
37
51
|
var requestContext;
|
|
38
52
|
var init_request_context = __esm({
|
|
39
53
|
"src/lib/request-context.ts"() {
|
|
@@ -348,7 +362,10 @@ async function captureToolEvent(args) {
|
|
|
348
362
|
properties: {
|
|
349
363
|
tool_name: args.toolName,
|
|
350
364
|
duration_ms: args.durationMs,
|
|
351
|
-
...args.details
|
|
365
|
+
...args.details,
|
|
366
|
+
// Which surface invoked the tool: mcp-stdio | mcp-http | rest | cli.
|
|
367
|
+
// Authoritative over any details.surface (there are no such callers today).
|
|
368
|
+
surface: resolveSurface()
|
|
352
369
|
}
|
|
353
370
|
});
|
|
354
371
|
}
|
|
@@ -363,6 +380,7 @@ var init_posthog = __esm({
|
|
|
363
380
|
"src/lib/posthog.ts"() {
|
|
364
381
|
"use strict";
|
|
365
382
|
init_supabase();
|
|
383
|
+
init_request_context();
|
|
366
384
|
POSTHOG_SALT = "socialneuron-mcp-ph-v1";
|
|
367
385
|
client = null;
|
|
368
386
|
}
|
|
@@ -377,6 +395,7 @@ __export(supabase_exports, {
|
|
|
377
395
|
getAuthenticatedApiKey: () => getAuthenticatedApiKey,
|
|
378
396
|
getAuthenticatedEmail: () => getAuthenticatedEmail,
|
|
379
397
|
getAuthenticatedExpiresAt: () => getAuthenticatedExpiresAt,
|
|
398
|
+
getAuthenticatedProjectId: () => getAuthenticatedProjectId,
|
|
380
399
|
getAuthenticatedScopes: () => getAuthenticatedScopes,
|
|
381
400
|
getDefaultProjectId: () => getDefaultProjectId,
|
|
382
401
|
getDefaultUserId: () => getDefaultUserId,
|
|
@@ -428,6 +447,9 @@ async function getDefaultUserId() {
|
|
|
428
447
|
throw new Error("No user ID available. Set SOCIALNEURON_USER_ID or authenticate via API key.");
|
|
429
448
|
}
|
|
430
449
|
async function getDefaultProjectId() {
|
|
450
|
+
const requestProjectId = getRequestProjectId();
|
|
451
|
+
if (requestProjectId) return requestProjectId;
|
|
452
|
+
if (authenticatedProjectId) return authenticatedProjectId;
|
|
431
453
|
const userId = await getDefaultUserId().catch(() => null);
|
|
432
454
|
if (userId) {
|
|
433
455
|
const cached = projectIdCache.get(userId);
|
|
@@ -469,6 +491,7 @@ async function initializeAuth() {
|
|
|
469
491
|
authenticatedScopes = result.scopes && result.scopes.length > 0 ? result.scopes : ["mcp:read"];
|
|
470
492
|
authenticatedEmail = result.email || null;
|
|
471
493
|
authenticatedExpiresAt = result.expiresAt || null;
|
|
494
|
+
authenticatedProjectId = result.projectId || null;
|
|
472
495
|
if (!_quietAuth) {
|
|
473
496
|
console.error(
|
|
474
497
|
"[MCP] Authenticated via API key (prefix: " + apiKey.substring(0, 6) + "..." + apiKey.slice(-4) + ")"
|
|
@@ -525,6 +548,9 @@ function getAuthMode() {
|
|
|
525
548
|
function getAuthenticatedApiKey() {
|
|
526
549
|
return authenticatedApiKey;
|
|
527
550
|
}
|
|
551
|
+
function getAuthenticatedProjectId() {
|
|
552
|
+
return authenticatedProjectId;
|
|
553
|
+
}
|
|
528
554
|
function isTelemetryDisabled() {
|
|
529
555
|
return process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true" || process.env.SOCIALNEURON_NO_TELEMETRY === "1";
|
|
530
556
|
}
|
|
@@ -554,7 +580,7 @@ async function logMcpToolInvocation(args) {
|
|
|
554
580
|
Promise.resolve().then(() => (init_posthog(), posthog_exports)).then(({ captureToolEvent: captureToolEvent2 }) => captureToolEvent2(args)).catch(() => {
|
|
555
581
|
});
|
|
556
582
|
}
|
|
557
|
-
var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY, projectIdCache;
|
|
583
|
+
var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, authenticatedProjectId, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY, projectIdCache;
|
|
558
584
|
var init_supabase = __esm({
|
|
559
585
|
"src/lib/supabase.ts"() {
|
|
560
586
|
"use strict";
|
|
@@ -568,6 +594,7 @@ var init_supabase = __esm({
|
|
|
568
594
|
authenticatedEmail = null;
|
|
569
595
|
authenticatedExpiresAt = null;
|
|
570
596
|
authenticatedApiKey = null;
|
|
597
|
+
authenticatedProjectId = null;
|
|
571
598
|
MCP_RUN_ID = randomUUID();
|
|
572
599
|
CLOUD_SUPABASE_URL = "https://rhukkjscgzauutioyeei.supabase.co";
|
|
573
600
|
CLOUD_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJodWtranNjZ3phdXV0aW95ZWVpIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ4NjM4ODYsImV4cCI6MjA4MDQzOTg4Nn0.JVtrviGvN0HaSh0JFS5KNl5FAB5ffG5Y1IMZsQFUrNQ";
|
|
@@ -2618,13 +2645,15 @@ var init_tool_catalog = __esm({
|
|
|
2618
2645
|
name: "get_loop_pulse",
|
|
2619
2646
|
description: "Read dynamic loop-health KPIs for the growth loop over the last 7 days (reflection/decision coverage, visual gate pass rate, learning-update application rate, per-platform uptake, autopilot lag) \u2014 each with an ok/warn/bad status. Use to decide whether the loop is closing or where it is stuck.",
|
|
2620
2647
|
module: "loop",
|
|
2621
|
-
scope: "mcp:read"
|
|
2648
|
+
scope: "mcp:read",
|
|
2649
|
+
internal: true
|
|
2622
2650
|
},
|
|
2623
2651
|
{
|
|
2624
2652
|
name: "get_bandit_state",
|
|
2625
2653
|
description: "Read the current content learning state for a project \u2014 top-K arms per (arm_type, platform) with expected performance and uncertainty. Use to reason about which hook family / format / timing slot currently performs best per platform.",
|
|
2626
2654
|
module: "loop",
|
|
2627
|
-
scope: "mcp:read"
|
|
2655
|
+
scope: "mcp:read",
|
|
2656
|
+
internal: true
|
|
2628
2657
|
}
|
|
2629
2658
|
];
|
|
2630
2659
|
}
|
|
@@ -2980,7 +3009,9 @@ var init_tool_annotations = __esm({
|
|
|
2980
3009
|
},
|
|
2981
3010
|
"mcp:write": {
|
|
2982
3011
|
readOnlyHint: false,
|
|
2983
|
-
|
|
3012
|
+
// Most write tools create additive drafts/assets. Per MCP semantics,
|
|
3013
|
+
// destructiveHint is reserved for overwrite/delete/irreversible behavior.
|
|
3014
|
+
destructiveHint: false,
|
|
2984
3015
|
idempotentHint: false,
|
|
2985
3016
|
openWorldHint: false
|
|
2986
3017
|
},
|
|
@@ -2998,21 +3029,30 @@ var init_tool_annotations = __esm({
|
|
|
2998
3029
|
},
|
|
2999
3030
|
"mcp:comments": {
|
|
3000
3031
|
readOnlyHint: false,
|
|
3001
|
-
destructiveHint:
|
|
3032
|
+
destructiveHint: false,
|
|
3002
3033
|
idempotentHint: false,
|
|
3003
3034
|
openWorldHint: true
|
|
3004
3035
|
},
|
|
3005
3036
|
"mcp:autopilot": {
|
|
3006
3037
|
readOnlyHint: false,
|
|
3007
|
-
destructiveHint:
|
|
3038
|
+
destructiveHint: false,
|
|
3008
3039
|
idempotentHint: false,
|
|
3009
3040
|
openWorldHint: false
|
|
3010
3041
|
}
|
|
3011
3042
|
};
|
|
3012
3043
|
OVERRIDES = {
|
|
3013
|
-
// Destructive tools
|
|
3044
|
+
// Destructive or overwrite tools
|
|
3014
3045
|
delete_comment: { destructiveHint: true },
|
|
3015
3046
|
moderate_comment: { destructiveHint: true },
|
|
3047
|
+
save_brand_profile: { destructiveHint: true, idempotentHint: true },
|
|
3048
|
+
update_platform_voice: { destructiveHint: true, idempotentHint: true },
|
|
3049
|
+
update_content_plan: { destructiveHint: true, idempotentHint: true },
|
|
3050
|
+
respond_plan_approval: { destructiveHint: true, idempotentHint: true },
|
|
3051
|
+
update_autopilot_config: { destructiveHint: true, idempotentHint: true },
|
|
3052
|
+
run_content_pipeline: { destructiveHint: true, openWorldHint: true },
|
|
3053
|
+
auto_approve_plan: { destructiveHint: true, idempotentHint: true },
|
|
3054
|
+
execute_recipe: { destructiveHint: true, openWorldHint: true },
|
|
3055
|
+
run_skill: { destructiveHint: true },
|
|
3016
3056
|
// Read-only tools in non-read scopes (must also clear destructiveHint from scope default)
|
|
3017
3057
|
list_comments: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
3018
3058
|
list_autopilot_configs: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
@@ -3023,14 +3063,12 @@ var init_tool_annotations = __esm({
|
|
|
3023
3063
|
// Analytics tool that triggers side effects (data refresh)
|
|
3024
3064
|
refresh_platform_analytics: { readOnlyHint: false, idempotentHint: true },
|
|
3025
3065
|
// Write tools that are idempotent
|
|
3026
|
-
save_brand_profile: { idempotentHint: true },
|
|
3027
|
-
update_platform_voice: { idempotentHint: true },
|
|
3028
|
-
update_autopilot_config: { idempotentHint: true },
|
|
3029
|
-
update_content_plan: { idempotentHint: true },
|
|
3030
|
-
respond_plan_approval: { idempotentHint: true },
|
|
3031
3066
|
// Distribution is open-world (publishes to external platforms)
|
|
3032
3067
|
schedule_post: { openWorldHint: true },
|
|
3033
3068
|
schedule_content_plan: { openWorldHint: true },
|
|
3069
|
+
// This only creates a short-lived OAuth deep link; it does not connect the
|
|
3070
|
+
// external account until the user completes the browser flow.
|
|
3071
|
+
start_platform_connection: { destructiveHint: false },
|
|
3034
3072
|
// Extraction reads external URLs
|
|
3035
3073
|
extract_url_content: { openWorldHint: true },
|
|
3036
3074
|
extract_brand: { openWorldHint: true },
|
|
@@ -3038,8 +3076,6 @@ var init_tool_annotations = __esm({
|
|
|
3038
3076
|
check_pipeline_readiness: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
3039
3077
|
get_pipeline_status: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
3040
3078
|
// Pipeline: orchestration tools (non-idempotent, may schedule externally)
|
|
3041
|
-
run_content_pipeline: { openWorldHint: true },
|
|
3042
|
-
auto_approve_plan: { idempotentHint: true },
|
|
3043
3079
|
// Suggest: read-only
|
|
3044
3080
|
suggest_next_content: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
3045
3081
|
// Digest/Anomalies: read-only analytics
|
|
@@ -3090,9 +3126,89 @@ function toolError(code, message, opts = {}) {
|
|
|
3090
3126
|
...opts.meta ? { _meta: opts.meta } : {}
|
|
3091
3127
|
};
|
|
3092
3128
|
}
|
|
3129
|
+
function classifyToolError(result) {
|
|
3130
|
+
const structured = result.structuredContent?.error?.error_type;
|
|
3131
|
+
if (structured && KNOWN_ERROR_TYPES.has(structured)) return structured;
|
|
3132
|
+
const text = result.content?.find((c) => c.type === "text")?.text ?? "";
|
|
3133
|
+
try {
|
|
3134
|
+
const parsed = JSON.parse(text);
|
|
3135
|
+
if (parsed.error_type && KNOWN_ERROR_TYPES.has(parsed.error_type)) {
|
|
3136
|
+
return parsed.error_type;
|
|
3137
|
+
}
|
|
3138
|
+
} catch {
|
|
3139
|
+
}
|
|
3140
|
+
if (/-32602|Input validation error|Invalid arguments/i.test(text)) {
|
|
3141
|
+
return "validation_error";
|
|
3142
|
+
}
|
|
3143
|
+
return "server_error";
|
|
3144
|
+
}
|
|
3145
|
+
var KNOWN_ERROR_TYPES;
|
|
3093
3146
|
var init_tool_error = __esm({
|
|
3094
3147
|
"src/lib/tool-error.ts"() {
|
|
3095
3148
|
"use strict";
|
|
3149
|
+
KNOWN_ERROR_TYPES = /* @__PURE__ */ new Set([
|
|
3150
|
+
"policy_block",
|
|
3151
|
+
"validation_error",
|
|
3152
|
+
"permission_denied",
|
|
3153
|
+
"billing_error",
|
|
3154
|
+
"rate_limited",
|
|
3155
|
+
"not_found",
|
|
3156
|
+
"upstream_error",
|
|
3157
|
+
"server_error"
|
|
3158
|
+
]);
|
|
3159
|
+
}
|
|
3160
|
+
});
|
|
3161
|
+
|
|
3162
|
+
// src/lib/tool-profile.ts
|
|
3163
|
+
function isToolAllowedByProfile(toolName, profile) {
|
|
3164
|
+
return profile === "full" || !ANTHROPIC_DIRECTORY_EXCLUDED_TOOLS.has(toolName);
|
|
3165
|
+
}
|
|
3166
|
+
function applyToolProfile(server, profile) {
|
|
3167
|
+
if (profile === "full") return;
|
|
3168
|
+
const currentTool = server.tool.bind(server);
|
|
3169
|
+
const currentRegisterTool = server.registerTool?.bind(server);
|
|
3170
|
+
server.tool = function profiledTool(...args) {
|
|
3171
|
+
const name = String(args[0] ?? "");
|
|
3172
|
+
if (!isToolAllowedByProfile(name, profile)) return void 0;
|
|
3173
|
+
return currentTool(...args);
|
|
3174
|
+
};
|
|
3175
|
+
if (currentRegisterTool) {
|
|
3176
|
+
server.registerTool = function profiledRegisterTool(...args) {
|
|
3177
|
+
const name = String(args[0] ?? "");
|
|
3178
|
+
if (!isToolAllowedByProfile(name, profile)) return void 0;
|
|
3179
|
+
return currentRegisterTool(...args);
|
|
3180
|
+
};
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
var ANTHROPIC_DIRECTORY_EXCLUDED_TOOLS;
|
|
3184
|
+
var init_tool_profile = __esm({
|
|
3185
|
+
"src/lib/tool-profile.ts"() {
|
|
3186
|
+
"use strict";
|
|
3187
|
+
init_tool_catalog();
|
|
3188
|
+
ANTHROPIC_DIRECTORY_EXCLUDED_TOOLS = /* @__PURE__ */ new Set([
|
|
3189
|
+
"generate_video",
|
|
3190
|
+
"generate_image",
|
|
3191
|
+
"check_status",
|
|
3192
|
+
"create_storyboard",
|
|
3193
|
+
"generate_voiceover",
|
|
3194
|
+
"generate_carousel",
|
|
3195
|
+
"create_carousel",
|
|
3196
|
+
"render_demo_video",
|
|
3197
|
+
"list_compositions",
|
|
3198
|
+
"render_template_video",
|
|
3199
|
+
"check_pipeline_readiness",
|
|
3200
|
+
"run_content_pipeline",
|
|
3201
|
+
"get_pipeline_status",
|
|
3202
|
+
"auto_approve_plan",
|
|
3203
|
+
"list_recipes",
|
|
3204
|
+
"get_recipe_details",
|
|
3205
|
+
"execute_recipe",
|
|
3206
|
+
"get_recipe_run_status",
|
|
3207
|
+
"list_hyperframes_blocks",
|
|
3208
|
+
"render_hyperframes",
|
|
3209
|
+
"list_skills",
|
|
3210
|
+
"run_skill"
|
|
3211
|
+
]);
|
|
3096
3212
|
}
|
|
3097
3213
|
});
|
|
3098
3214
|
|
|
@@ -3759,6 +3875,52 @@ var init_ideation = __esm({
|
|
|
3759
3875
|
}
|
|
3760
3876
|
});
|
|
3761
3877
|
|
|
3878
|
+
// src/lib/checkStatusShape.ts
|
|
3879
|
+
function buildCheckStatusPayload(job, liveStatus) {
|
|
3880
|
+
const status = liveStatus?.status ?? job.status;
|
|
3881
|
+
const progress = liveStatus?.progress ?? null;
|
|
3882
|
+
const resultUrl = liveStatus?.resultUrl ?? job.result_url ?? null;
|
|
3883
|
+
const r2Key = resultUrl && !resultUrl.startsWith("http") ? resultUrl : null;
|
|
3884
|
+
const errorMessage = liveStatus?.error ?? job.error_message ?? null;
|
|
3885
|
+
const allUrls = job.result_metadata?.all_urls ?? null;
|
|
3886
|
+
const modelRequested = job.result_metadata?.model_requested ?? null;
|
|
3887
|
+
const modelDelivered = job.result_metadata?.model_delivered ?? null;
|
|
3888
|
+
const fallbackReason = job.result_metadata?.fallback_reason ?? null;
|
|
3889
|
+
return {
|
|
3890
|
+
job_id: job.id,
|
|
3891
|
+
job_type: job.job_type,
|
|
3892
|
+
model: job.model,
|
|
3893
|
+
status,
|
|
3894
|
+
progress,
|
|
3895
|
+
result_url: resultUrl,
|
|
3896
|
+
r2_key: r2Key,
|
|
3897
|
+
all_urls: allUrls,
|
|
3898
|
+
error: errorMessage,
|
|
3899
|
+
credits_cost: job.credits_cost,
|
|
3900
|
+
created_at: job.created_at,
|
|
3901
|
+
completed_at: job.completed_at,
|
|
3902
|
+
model_requested: modelRequested,
|
|
3903
|
+
model_delivered: modelDelivered,
|
|
3904
|
+
fallback_reason: fallbackReason,
|
|
3905
|
+
id: job.id,
|
|
3906
|
+
jobId: job.id,
|
|
3907
|
+
jobType: job.job_type,
|
|
3908
|
+
resultUrl,
|
|
3909
|
+
credits: job.credits_cost,
|
|
3910
|
+
error_message: errorMessage,
|
|
3911
|
+
createdAt: job.created_at,
|
|
3912
|
+
completedAt: job.completed_at,
|
|
3913
|
+
modelRequested,
|
|
3914
|
+
modelDelivered,
|
|
3915
|
+
fallbackReason
|
|
3916
|
+
};
|
|
3917
|
+
}
|
|
3918
|
+
var init_checkStatusShape = __esm({
|
|
3919
|
+
"src/lib/checkStatusShape.ts"() {
|
|
3920
|
+
"use strict";
|
|
3921
|
+
}
|
|
3922
|
+
});
|
|
3923
|
+
|
|
3762
3924
|
// src/lib/budget.ts
|
|
3763
3925
|
function getCreditsUsed() {
|
|
3764
3926
|
const ctx = requestContext.getStore();
|
|
@@ -3848,34 +4010,32 @@ function asEnvelope2(data) {
|
|
|
3848
4010
|
function registerContentTools(server) {
|
|
3849
4011
|
server.tool(
|
|
3850
4012
|
"generate_video",
|
|
3851
|
-
"Start an async AI video generation job \u2014 returns a job_id immediately. Poll with check_status every 10-30s until complete.
|
|
4013
|
+
"Start an async AI video generation job \u2014 returns a job_id immediately. Poll with check_status every 10-30s until complete. Base credit costs (reference config; dynamic models scale with duration/audio/resolution): seedance-2-fast 264 (8s, best quality/credit) \xB7 kling-3 100 (5s no-audio) \xB7 grok-imagine 30 (6s, cheapest) \xB7 veo3-fast 65 (8s) \xB7 kling-3-pro 135 (5s no-audio) \xB7 seedance-2 328 (8s 720p) \xB7 veo3-quality 1000 (8s) \xB7 wan-2.6 105 (5s) \xB7 gemini-omni-video 126 (multi-reference composites) \xB7 hailuo-02-standard 180 / seedance-1.5-pro 150 / kling 170 (legacy/budget fallbacks). Costs are estimated pre-check and reconciled post-response from the actual charge. audio adds ~1.5x on kling-3/kling-3-pro and ~2.65x on kling (enable_audio defaults to FALSE). Check get_credit_balance first for expensive generations.",
|
|
3852
4014
|
{
|
|
3853
4015
|
prompt: z2.string().max(2500).describe(
|
|
3854
4016
|
'Video prompt \u2014 be specific about visual style, camera movement, lighting, and mood. Example: "Aerial drone shot of coastal cliffs at golden hour, slow dolly forward, cinematic 24fps, warm color grading." Vague prompts produce generic results.'
|
|
3855
4017
|
),
|
|
3856
|
-
model: z2.enum(
|
|
3857
|
-
"veo3-fast"
|
|
3858
|
-
"veo3-quality",
|
|
3859
|
-
"runway-aleph",
|
|
3860
|
-
"sora2",
|
|
3861
|
-
"sora2-pro",
|
|
3862
|
-
"kling",
|
|
3863
|
-
"kling-3",
|
|
3864
|
-
"kling-3-pro"
|
|
3865
|
-
]).describe(
|
|
3866
|
-
"Video model. veo3-fast: fastest (~15 credits/5s, ~60s render). veo3-quality: highest quality (~20 credits/5s, ~120s). sora2-pro: OpenAI premium (~60 credits/10s). kling-3: 4K with audio (~30 credits/5s). kling-3-pro: best Kling quality (~40 credits/5s)."
|
|
4018
|
+
model: z2.enum(VIDEO_MODEL_ENUM).describe(
|
|
4019
|
+
"Video model, quality ladder best->worst: seedance-2-fast (264cr/8s, S-tier, native audio, top quality/credit) > kling-3 (100cr/5s no-audio, compound prompts, up to 10s) > grok-imagine (30cr/6s, cheapest real model, great image-to-video) > veo3-fast (65cr/8s, Veo 3.1 Fast, native audio) > kling-3-pro (135cr/5s no-audio, up to 15s) > seedance-2 (328cr/8s 720p, premium cinematic, native audio) > veo3-quality (1000cr/8s, Veo 3.1 Quality, hero shots) > wan-2.6 (105cr/5s, fallback) > gemini-omni-video (126cr \u2014 pick this ONLY for multi-reference composites/edits, not raw quality). hailuo-02-standard (180cr), seedance-1.5-pro (150cr), kling (170cr/10s no-audio) are legacy/budget fallbacks."
|
|
3867
4020
|
),
|
|
3868
4021
|
duration: z2.number().min(3).max(30).optional().describe(
|
|
3869
|
-
"Video duration in seconds. kling: 5
|
|
4022
|
+
"Video duration in seconds. kling: 5 or 10s \xB7 kling-3: 5 or 10s \xB7 kling-3-pro: 5/10/15s \xB7 grok-imagine/hailuo: 6 or 10s \xB7 wan-2.6: 5/10/15s \xB7 seedance family: 4-15s \xB7 veo3-fast/veo3-quality: fixed 8s (duration ignored). Out-of-range values are clamped server-side, never rejected. Defaults to 5 seconds."
|
|
3870
4023
|
),
|
|
3871
4024
|
aspect_ratio: z2.enum(["16:9", "9:16", "1:1"]).optional().describe(
|
|
3872
4025
|
"Video aspect ratio. 16:9 for YouTube/landscape, 9:16 for TikTok/Reels/Shorts, 1:1 for Instagram feed/square. Defaults to 16:9."
|
|
3873
4026
|
),
|
|
3874
4027
|
enable_audio: z2.boolean().optional().describe(
|
|
3875
|
-
"Enable native audio generation.
|
|
4028
|
+
"Enable native audio generation. DEFAULTS TO FALSE (cost control). Cost multiplier when true: kling 2.6 ~2.65x (17->45 cr/sec), kling-3 1.5x (20->30 cr/sec), kling-3-pro ~1.5x (27->40 cr/sec). seedance-2/-fast generate audio natively regardless. 5+ languages."
|
|
4029
|
+
),
|
|
4030
|
+
image_url: z2.string().optional().describe(
|
|
4031
|
+
"Start frame image URL for image-to-video (Kling 3.0 frame control)."
|
|
4032
|
+
),
|
|
4033
|
+
end_frame_url: z2.string().optional().describe(
|
|
4034
|
+
"End frame image URL (Kling 3.0 only). Enables seamless loop transitions."
|
|
4035
|
+
),
|
|
4036
|
+
project_id: z2.string().optional().describe(
|
|
4037
|
+
"Project ID to associate the video with (brand context is auto-injected from the project brand profile). Omit to generate without a project association."
|
|
3876
4038
|
),
|
|
3877
|
-
image_url: z2.string().optional().describe("Start frame image URL for image-to-video (Kling 3.0 frame control)."),
|
|
3878
|
-
end_frame_url: z2.string().optional().describe("End frame image URL (Kling 3.0 only). Enables seamless loop transitions."),
|
|
3879
4039
|
response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
3880
4040
|
},
|
|
3881
4041
|
async ({
|
|
@@ -3886,6 +4046,7 @@ function registerContentTools(server) {
|
|
|
3886
4046
|
enable_audio,
|
|
3887
4047
|
image_url,
|
|
3888
4048
|
end_frame_url,
|
|
4049
|
+
project_id,
|
|
3889
4050
|
response_format
|
|
3890
4051
|
}) => {
|
|
3891
4052
|
const format = response_format ?? "text";
|
|
@@ -3924,9 +4085,14 @@ function registerContentTools(server) {
|
|
|
3924
4085
|
model,
|
|
3925
4086
|
duration: duration ?? 5,
|
|
3926
4087
|
aspectRatio: aspect_ratio ?? "16:9",
|
|
3927
|
-
|
|
4088
|
+
// Default FALSE (2026-07-13): the old `?? true` default silently multiplied
|
|
4089
|
+
// kling-family costs (2.65x on kling 2.6) for callers that never asked for audio.
|
|
4090
|
+
enableAudio: enable_audio ?? false,
|
|
3928
4091
|
...image_url && { imageUrl: image_url },
|
|
3929
|
-
...end_frame_url && { endFrameUrl: end_frame_url }
|
|
4092
|
+
...end_frame_url && { endFrameUrl: end_frame_url },
|
|
4093
|
+
// The server reads projectId and enforces
|
|
4094
|
+
// ownership via resolveProjectAndContent (index.ts:416-423).
|
|
4095
|
+
...project_id && { projectId: project_id }
|
|
3930
4096
|
},
|
|
3931
4097
|
{ timeoutMs: 3e4 }
|
|
3932
4098
|
);
|
|
@@ -3998,7 +4164,7 @@ function registerContentTools(server) {
|
|
|
3998
4164
|
);
|
|
3999
4165
|
server.tool(
|
|
4000
4166
|
"generate_image",
|
|
4001
|
-
"Start an async AI image generation job \u2014 returns a job_id immediately. Poll with check_status every 5-15s until complete. Costs
|
|
4167
|
+
"Start an async AI image generation job \u2014 returns a job_id immediately. Poll with check_status every 5-15s until complete. Costs 15-50 credits depending on model. Use for social media posts, carousel slides, or as input to generate_video (image-to-video). Pass project_id so the asset is stored with the correct brand/project.",
|
|
4002
4168
|
{
|
|
4003
4169
|
prompt: z2.string().max(2e3).describe(
|
|
4004
4170
|
"Text prompt describing the image to generate. Be specific about style, composition, colors, lighting, and subject matter."
|
|
@@ -4020,9 +4186,10 @@ function registerContentTools(server) {
|
|
|
4020
4186
|
image_url: z2.string().optional().describe(
|
|
4021
4187
|
"Reference image URL for image-to-image generation. Required for ideogram model. Optional for others."
|
|
4022
4188
|
),
|
|
4189
|
+
project_id: z2.string().optional().describe("Project ID to associate the generated image with."),
|
|
4023
4190
|
response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
4024
4191
|
},
|
|
4025
|
-
async ({ prompt: prompt2, model, aspect_ratio, image_url, response_format }) => {
|
|
4192
|
+
async ({ prompt: prompt2, model, aspect_ratio, image_url, project_id, response_format }) => {
|
|
4026
4193
|
const format = response_format ?? "text";
|
|
4027
4194
|
const userId = await getDefaultUserId();
|
|
4028
4195
|
const assetBudget = checkAssetBudget();
|
|
@@ -4058,7 +4225,8 @@ function registerContentTools(server) {
|
|
|
4058
4225
|
prompt: prompt2,
|
|
4059
4226
|
model,
|
|
4060
4227
|
aspectRatio: aspect_ratio ?? "1:1",
|
|
4061
|
-
imageUrl: image_url
|
|
4228
|
+
imageUrl: image_url,
|
|
4229
|
+
...project_id && { projectId: project_id }
|
|
4062
4230
|
},
|
|
4063
4231
|
{ timeoutMs: 3e4 }
|
|
4064
4232
|
);
|
|
@@ -4097,7 +4265,8 @@ function registerContentTools(server) {
|
|
|
4097
4265
|
jobId,
|
|
4098
4266
|
taskId: data.taskId,
|
|
4099
4267
|
asyncJobId: data.asyncJobId,
|
|
4100
|
-
model: data.model
|
|
4268
|
+
model: data.model,
|
|
4269
|
+
projectId: project_id ?? null
|
|
4101
4270
|
}),
|
|
4102
4271
|
null,
|
|
4103
4272
|
2
|
|
@@ -4124,7 +4293,7 @@ function registerContentTools(server) {
|
|
|
4124
4293
|
);
|
|
4125
4294
|
server.tool(
|
|
4126
4295
|
"check_status",
|
|
4127
|
-
'Poll an async job started by generate_video or generate_image. Returns status (queued/processing/completed/failed), progress %, and result URL on completion. Poll every 10-30s for video, 5-15s for images. On "failed" status, the error field explains why \u2014 check credits or try a different model.',
|
|
4296
|
+
'Poll an async job started by generate_video or generate_image. Returns status (queued/processing/completed/failed), progress %, and result URL on completion. Poll every 10-30s for video, 5-15s for images. On "failed" status, the error field explains why \u2014 check credits or try a different model. JSON shape (response_format=json) is stable across the whole poll lifecycle \u2014 canonical fields job_id, status, progress, result_url, r2_key, all_urls, error, credits_cost, created_at, completed_at are always present (never only camelCase or only snake_case); legacy aliases jobId/resultUrl/credits/createdAt/completedAt/error_message are also always populated for backward compatibility.',
|
|
4128
4297
|
{
|
|
4129
4298
|
job_id: z2.string().describe(
|
|
4130
4299
|
"The job ID returned by generate_video or generate_image. This is the asyncJobId or taskId value."
|
|
@@ -4170,10 +4339,13 @@ function registerContentTools(server) {
|
|
|
4170
4339
|
};
|
|
4171
4340
|
}
|
|
4172
4341
|
if (job.external_id && (job.status === "pending" || job.status === "processing")) {
|
|
4173
|
-
const { data: liveStatus } = await callEdgeFunction(
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4342
|
+
const { data: liveStatus } = await callEdgeFunction(
|
|
4343
|
+
"kie-task-status",
|
|
4344
|
+
{
|
|
4345
|
+
taskId: job.external_id,
|
|
4346
|
+
model: job.model
|
|
4347
|
+
}
|
|
4348
|
+
);
|
|
4177
4349
|
if (liveStatus) {
|
|
4178
4350
|
const lines2 = [
|
|
4179
4351
|
`Job: ${job.id}`,
|
|
@@ -4196,13 +4368,17 @@ function registerContentTools(server) {
|
|
|
4196
4368
|
{
|
|
4197
4369
|
type: "text",
|
|
4198
4370
|
text: JSON.stringify(
|
|
4371
|
+
// v1.8.0: spread legacy live-branch fields first for
|
|
4372
|
+
// backward compatibility, then overlay
|
|
4373
|
+
// buildCheckStatusPayload's canonical snake_case fields +
|
|
4374
|
+
// both-alias set so a consumer never sees a different
|
|
4375
|
+
// field name depending on which branch served the poll.
|
|
4376
|
+
// lib/checkStatusShape.ts is the single stable shape —
|
|
4377
|
+
// it derives jobId/jobType/model/credits/createdAt from
|
|
4378
|
+
// `job` + `liveStatus`; do not duplicate them here.
|
|
4199
4379
|
asEnvelope2({
|
|
4200
|
-
jobId: job.id,
|
|
4201
|
-
jobType: job.job_type,
|
|
4202
|
-
model: job.model,
|
|
4203
4380
|
...liveStatus,
|
|
4204
|
-
|
|
4205
|
-
createdAt: job.created_at
|
|
4381
|
+
...buildCheckStatusPayload(job, liveStatus)
|
|
4206
4382
|
}),
|
|
4207
4383
|
null,
|
|
4208
4384
|
2
|
|
@@ -4229,7 +4405,7 @@ function registerContentTools(server) {
|
|
|
4229
4405
|
const filename = segments[segments.length - 1] || "media";
|
|
4230
4406
|
lines.push(`Media ready: ${filename}`);
|
|
4231
4407
|
lines.push(
|
|
4232
|
-
"(Pass job_id directly to schedule_post, or
|
|
4408
|
+
"(Pass job_id directly to schedule_post, or pass response_format=json's r2_key to get_media_url for a download link)"
|
|
4233
4409
|
);
|
|
4234
4410
|
} else {
|
|
4235
4411
|
lines.push(`Result URL: ${job.result_url}`);
|
|
@@ -4253,11 +4429,15 @@ function registerContentTools(server) {
|
|
|
4253
4429
|
if (format === "json") {
|
|
4254
4430
|
const enriched = {
|
|
4255
4431
|
...job,
|
|
4256
|
-
|
|
4257
|
-
all_urls: allUrls ?? null
|
|
4432
|
+
...buildCheckStatusPayload(job)
|
|
4258
4433
|
};
|
|
4259
4434
|
return {
|
|
4260
|
-
content: [
|
|
4435
|
+
content: [
|
|
4436
|
+
{
|
|
4437
|
+
type: "text",
|
|
4438
|
+
text: JSON.stringify(asEnvelope2(enriched), null, 2)
|
|
4439
|
+
}
|
|
4440
|
+
]
|
|
4261
4441
|
};
|
|
4262
4442
|
}
|
|
4263
4443
|
return {
|
|
@@ -4267,7 +4447,7 @@ function registerContentTools(server) {
|
|
|
4267
4447
|
);
|
|
4268
4448
|
server.tool(
|
|
4269
4449
|
"create_storyboard",
|
|
4270
|
-
"Plan a multi-scene video storyboard with AI-generated prompts, durations, captions, and voiceover text per frame. Use before generate_video or generate_image to create cohesive multi-shot content. Include brand_context from get_brand_profile for consistent
|
|
4450
|
+
"Plan a multi-scene video storyboard with AI-generated prompts, durations, captions, and voiceover text per frame. Use before generate_video or generate_image to create cohesive multi-shot content. Include brand_context from get_brand_profile and project_id for consistent, project-scoped production. Costs 10 credits.",
|
|
4271
4451
|
{
|
|
4272
4452
|
concept: z2.string().max(2e3).describe(
|
|
4273
4453
|
'The video concept/idea. Include: hook, key messages, target audience, and desired outcome (e.g., "TikTok ad for VPN app targeting privacy-conscious millennials, hook with shocking stat about data leaks").'
|
|
@@ -4275,7 +4455,15 @@ function registerContentTools(server) {
|
|
|
4275
4455
|
brand_context: z2.string().max(3e3).optional().describe(
|
|
4276
4456
|
"Brand context JSON from extract_brand. Include colors, voice tone, visual style keywords for consistent branding across frames."
|
|
4277
4457
|
),
|
|
4278
|
-
platform: z2.enum([
|
|
4458
|
+
platform: z2.enum([
|
|
4459
|
+
"tiktok",
|
|
4460
|
+
"instagram-reels",
|
|
4461
|
+
"youtube-shorts",
|
|
4462
|
+
"youtube",
|
|
4463
|
+
"general"
|
|
4464
|
+
]).describe(
|
|
4465
|
+
"Target platform. Determines aspect ratio, duration, and pacing."
|
|
4466
|
+
),
|
|
4279
4467
|
target_duration: z2.number().min(5).max(120).optional().describe(
|
|
4280
4468
|
"Target total duration in seconds. Defaults to 30s for short-form, 60s for YouTube."
|
|
4281
4469
|
),
|
|
@@ -4283,7 +4471,10 @@ function registerContentTools(server) {
|
|
|
4283
4471
|
style: z2.string().optional().describe(
|
|
4284
4472
|
'Visual style direction (e.g., "cinematic", "anime", "documentary", "motion graphics").'
|
|
4285
4473
|
),
|
|
4286
|
-
|
|
4474
|
+
project_id: z2.string().optional().describe("Project ID for brand-scoped generation and attribution."),
|
|
4475
|
+
response_format: z2.enum(["text", "json"]).optional().describe(
|
|
4476
|
+
"Response format. Defaults to json for structured storyboard data."
|
|
4477
|
+
)
|
|
4287
4478
|
},
|
|
4288
4479
|
async ({
|
|
4289
4480
|
concept,
|
|
@@ -4292,10 +4483,15 @@ function registerContentTools(server) {
|
|
|
4292
4483
|
target_duration,
|
|
4293
4484
|
num_scenes,
|
|
4294
4485
|
style,
|
|
4486
|
+
project_id,
|
|
4295
4487
|
response_format
|
|
4296
4488
|
}) => {
|
|
4297
4489
|
const format = response_format ?? "json";
|
|
4298
|
-
const isShortForm = [
|
|
4490
|
+
const isShortForm = [
|
|
4491
|
+
"tiktok",
|
|
4492
|
+
"instagram-reels",
|
|
4493
|
+
"youtube-shorts"
|
|
4494
|
+
].includes(platform3);
|
|
4299
4495
|
const duration = target_duration ?? (isShortForm ? 30 : 60);
|
|
4300
4496
|
const scenes = num_scenes ?? (isShortForm ? 7 : 10);
|
|
4301
4497
|
const aspectRatio = isShortForm ? "9:16" : "16:9";
|
|
@@ -4370,23 +4566,45 @@ Return ONLY valid JSON in this exact format:
|
|
|
4370
4566
|
prompt: storyboardPrompt,
|
|
4371
4567
|
type: "storyboard",
|
|
4372
4568
|
model: "gemini-2.5-flash",
|
|
4373
|
-
responseFormat: "json"
|
|
4569
|
+
responseFormat: "json",
|
|
4570
|
+
...project_id && { projectId: project_id }
|
|
4374
4571
|
},
|
|
4375
4572
|
{ timeoutMs: 6e4 }
|
|
4376
4573
|
);
|
|
4377
4574
|
if (error) {
|
|
4378
4575
|
return {
|
|
4379
|
-
content: [
|
|
4576
|
+
content: [
|
|
4577
|
+
{
|
|
4578
|
+
type: "text",
|
|
4579
|
+
text: `Storyboard generation failed: ${error}`
|
|
4580
|
+
}
|
|
4581
|
+
],
|
|
4582
|
+
isError: true
|
|
4583
|
+
};
|
|
4584
|
+
}
|
|
4585
|
+
const rawContent = data?.content?.trim() || data?.text?.trim() || "";
|
|
4586
|
+
if (!rawContent) {
|
|
4587
|
+
return {
|
|
4588
|
+
content: [
|
|
4589
|
+
{
|
|
4590
|
+
type: "text",
|
|
4591
|
+
text: "Storyboard generation failed: the AI service returned an empty response."
|
|
4592
|
+
}
|
|
4593
|
+
],
|
|
4380
4594
|
isError: true
|
|
4381
4595
|
};
|
|
4382
4596
|
}
|
|
4383
|
-
const rawContent = data?.content ?? "";
|
|
4384
4597
|
addCreditsUsed(estimatedCost);
|
|
4385
4598
|
if (format === "json") {
|
|
4386
4599
|
try {
|
|
4387
4600
|
const parsed = JSON.parse(rawContent);
|
|
4388
4601
|
return {
|
|
4389
|
-
content: [
|
|
4602
|
+
content: [
|
|
4603
|
+
{
|
|
4604
|
+
type: "text",
|
|
4605
|
+
text: JSON.stringify(asEnvelope2(parsed), null, 2)
|
|
4606
|
+
}
|
|
4607
|
+
]
|
|
4390
4608
|
};
|
|
4391
4609
|
} catch {
|
|
4392
4610
|
return {
|
|
@@ -4411,16 +4629,17 @@ Return ONLY valid JSON in this exact format:
|
|
|
4411
4629
|
);
|
|
4412
4630
|
server.tool(
|
|
4413
4631
|
"generate_voiceover",
|
|
4414
|
-
"Generate a voiceover audio file for video narration. Returns an R2-hosted audio URL. Use after create_storyboard to add narration to each scene, or standalone for podcast intros and ad reads. Costs
|
|
4632
|
+
"Generate a voiceover audio file for video narration. Returns an R2-hosted audio URL. Use after create_storyboard to add narration to each scene, or standalone for podcast intros and ad reads. Pass project_id to keep the asset with the correct brand/project. Costs 15 credits per generation.",
|
|
4415
4633
|
{
|
|
4416
4634
|
text: z2.string().max(5e3).describe("The script/text to convert to speech."),
|
|
4417
4635
|
voice: z2.enum(["rachel", "domi"]).optional().describe(
|
|
4418
4636
|
"Voice selection. rachel=warm female, domi=confident female. Defaults to rachel."
|
|
4419
4637
|
),
|
|
4420
4638
|
speed: z2.number().min(0.5).max(2).optional().describe("Speech speed multiplier. 1.0 is normal. Defaults to 1.0."),
|
|
4639
|
+
project_id: z2.string().optional().describe("Project ID to associate the generated voiceover with."),
|
|
4421
4640
|
response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
4422
4641
|
},
|
|
4423
|
-
async ({ text, voice, speed, response_format }) => {
|
|
4642
|
+
async ({ text, voice, speed, project_id, response_format }) => {
|
|
4424
4643
|
const format = response_format ?? "text";
|
|
4425
4644
|
const userId = await getDefaultUserId();
|
|
4426
4645
|
const estimatedCost = 15;
|
|
@@ -4431,7 +4650,10 @@ Return ONLY valid JSON in this exact format:
|
|
|
4431
4650
|
isError: true
|
|
4432
4651
|
};
|
|
4433
4652
|
}
|
|
4434
|
-
const rateLimit = checkRateLimit(
|
|
4653
|
+
const rateLimit = checkRateLimit(
|
|
4654
|
+
"posting",
|
|
4655
|
+
`generate_voiceover:${userId}`
|
|
4656
|
+
);
|
|
4435
4657
|
if (!rateLimit.allowed) {
|
|
4436
4658
|
return {
|
|
4437
4659
|
content: [
|
|
@@ -4452,20 +4674,29 @@ Return ONLY valid JSON in this exact format:
|
|
|
4452
4674
|
{
|
|
4453
4675
|
text,
|
|
4454
4676
|
voiceId: ELEVENLABS_VOICE_IDS[voice ?? "rachel"],
|
|
4455
|
-
speed: speed ?? 1
|
|
4677
|
+
speed: speed ?? 1,
|
|
4678
|
+
...project_id && { projectId: project_id }
|
|
4456
4679
|
},
|
|
4457
4680
|
{ timeoutMs: 6e4 }
|
|
4458
4681
|
);
|
|
4459
4682
|
if (error) {
|
|
4460
4683
|
return {
|
|
4461
|
-
content: [
|
|
4684
|
+
content: [
|
|
4685
|
+
{
|
|
4686
|
+
type: "text",
|
|
4687
|
+
text: `Voiceover generation failed: ${error}`
|
|
4688
|
+
}
|
|
4689
|
+
],
|
|
4462
4690
|
isError: true
|
|
4463
4691
|
};
|
|
4464
4692
|
}
|
|
4465
4693
|
if (!data?.audioUrl) {
|
|
4466
4694
|
return {
|
|
4467
4695
|
content: [
|
|
4468
|
-
{
|
|
4696
|
+
{
|
|
4697
|
+
type: "text",
|
|
4698
|
+
text: "Voiceover generation failed: no audio URL returned."
|
|
4699
|
+
}
|
|
4469
4700
|
],
|
|
4470
4701
|
isError: true
|
|
4471
4702
|
};
|
|
@@ -4480,7 +4711,8 @@ Return ONLY valid JSON in this exact format:
|
|
|
4480
4711
|
asEnvelope2({
|
|
4481
4712
|
audioUrl: data.audioUrl,
|
|
4482
4713
|
durationSeconds: data.durationSeconds,
|
|
4483
|
-
voice: voice ?? "rachel"
|
|
4714
|
+
voice: voice ?? "rachel",
|
|
4715
|
+
projectId: project_id ?? null
|
|
4484
4716
|
}),
|
|
4485
4717
|
null,
|
|
4486
4718
|
2
|
|
@@ -4527,20 +4759,30 @@ Return ONLY valid JSON in this exact format:
|
|
|
4527
4759
|
"Carousel template. hormozi-authority: bold typography, one idea per slide, dark backgrounds. educational-series: numbered tips. Default: hormozi-authority."
|
|
4528
4760
|
),
|
|
4529
4761
|
slide_count: z2.number().min(3).max(10).optional().describe("Number of slides (3-10). Default: 7."),
|
|
4530
|
-
aspect_ratio: z2.enum(["1:1", "4:5", "9:16"]).optional().describe(
|
|
4762
|
+
aspect_ratio: z2.enum(["1:1", "4:5", "9:16"]).optional().describe(
|
|
4763
|
+
"Aspect ratio. 1:1 square (default), 4:5 portrait, 9:16 story."
|
|
4764
|
+
),
|
|
4531
4765
|
style: z2.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
|
|
4532
4766
|
"Visual style. hormozi: black bg, bold white text, gold accents. Default: hormozi (when using hormozi-authority template)."
|
|
4533
4767
|
),
|
|
4534
4768
|
hook: z2.string().max(300).optional().describe(
|
|
4535
4769
|
"Explicit hook/opener for slide 1. Overrides any hook derived from topic. Keep under 15 words."
|
|
4536
4770
|
),
|
|
4537
|
-
hook_family: z2.enum([
|
|
4538
|
-
"
|
|
4771
|
+
hook_family: z2.enum([
|
|
4772
|
+
"curiosity",
|
|
4773
|
+
"authority",
|
|
4774
|
+
"pain_point",
|
|
4775
|
+
"contrarian",
|
|
4776
|
+
"data_driven"
|
|
4777
|
+
]).optional().describe(
|
|
4778
|
+
"Hook family tag. Persisted with the carousel so downstream analytics can attribute engagement to hook pattern."
|
|
4539
4779
|
),
|
|
4540
4780
|
cta_text: z2.string().max(200).optional().describe("Explicit CTA copy for the final slide."),
|
|
4541
4781
|
cta_url: z2.string().url().optional().describe("URL promoted on the CTA slide."),
|
|
4542
4782
|
tone: z2.string().max(200).optional().describe("Voice/tone override. Composes with brand profile voice."),
|
|
4543
|
-
constraints: z2.string().max(500).optional().describe(
|
|
4783
|
+
constraints: z2.string().max(500).optional().describe(
|
|
4784
|
+
'Content constraints. Example: "No fabricated statistics. Sentence case only."'
|
|
4785
|
+
),
|
|
4544
4786
|
platform: z2.enum(["linkedin", "instagram", "tiktok", "x"]).optional().describe("Target platform. Affects tone and format guardrails."),
|
|
4545
4787
|
project_id: z2.string().optional().describe("Project ID to associate the carousel with."),
|
|
4546
4788
|
response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to json.")
|
|
@@ -4575,7 +4817,10 @@ Return ONLY valid JSON in this exact format:
|
|
|
4575
4817
|
};
|
|
4576
4818
|
}
|
|
4577
4819
|
const userId = await getDefaultUserId();
|
|
4578
|
-
const rateLimit = checkRateLimit(
|
|
4820
|
+
const rateLimit = checkRateLimit(
|
|
4821
|
+
"posting",
|
|
4822
|
+
`generate_carousel:${userId}`
|
|
4823
|
+
);
|
|
4579
4824
|
if (!rateLimit.allowed) {
|
|
4580
4825
|
return {
|
|
4581
4826
|
content: [
|
|
@@ -4608,13 +4853,23 @@ Return ONLY valid JSON in this exact format:
|
|
|
4608
4853
|
);
|
|
4609
4854
|
if (error) {
|
|
4610
4855
|
return {
|
|
4611
|
-
content: [
|
|
4856
|
+
content: [
|
|
4857
|
+
{
|
|
4858
|
+
type: "text",
|
|
4859
|
+
text: `Carousel generation failed: ${error}`
|
|
4860
|
+
}
|
|
4861
|
+
],
|
|
4612
4862
|
isError: true
|
|
4613
4863
|
};
|
|
4614
4864
|
}
|
|
4615
4865
|
if (!data?.carousel) {
|
|
4616
4866
|
return {
|
|
4617
|
-
content: [
|
|
4867
|
+
content: [
|
|
4868
|
+
{
|
|
4869
|
+
type: "text",
|
|
4870
|
+
text: "Carousel generation returned no data."
|
|
4871
|
+
}
|
|
4872
|
+
],
|
|
4618
4873
|
isError: true
|
|
4619
4874
|
};
|
|
4620
4875
|
}
|
|
@@ -4662,7 +4917,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
4662
4917
|
}
|
|
4663
4918
|
);
|
|
4664
4919
|
}
|
|
4665
|
-
var VIDEO_CREDIT_ESTIMATES, IMAGE_CREDIT_ESTIMATES;
|
|
4920
|
+
var VIDEO_CREDIT_ESTIMATES, VIDEO_MODEL_ENUM, IMAGE_CREDIT_ESTIMATES;
|
|
4666
4921
|
var init_content2 = __esm({
|
|
4667
4922
|
"src/tools/content.ts"() {
|
|
4668
4923
|
"use strict";
|
|
@@ -4670,17 +4925,36 @@ var init_content2 = __esm({
|
|
|
4670
4925
|
init_rate_limit();
|
|
4671
4926
|
init_supabase();
|
|
4672
4927
|
init_version();
|
|
4928
|
+
init_checkStatusShape();
|
|
4673
4929
|
init_budget();
|
|
4674
4930
|
VIDEO_CREDIT_ESTIMATES = {
|
|
4675
|
-
"
|
|
4676
|
-
"veo3-quality": 1e3,
|
|
4677
|
-
"runway-aleph": 340,
|
|
4678
|
-
sora2: 500,
|
|
4679
|
-
"sora2-pro": 1500,
|
|
4680
|
-
kling: 170,
|
|
4931
|
+
"seedance-2-fast": 264,
|
|
4681
4932
|
"kling-3": 100,
|
|
4682
|
-
"
|
|
4933
|
+
"grok-imagine": 30,
|
|
4934
|
+
"veo3-fast": 65,
|
|
4935
|
+
"kling-3-pro": 135,
|
|
4936
|
+
"seedance-2": 328,
|
|
4937
|
+
"veo3-quality": 1e3,
|
|
4938
|
+
"wan-2.6": 105,
|
|
4939
|
+
"gemini-omni-video": 126,
|
|
4940
|
+
"hailuo-02-standard": 180,
|
|
4941
|
+
"seedance-1.5-pro": 150,
|
|
4942
|
+
kling: 170
|
|
4683
4943
|
};
|
|
4944
|
+
VIDEO_MODEL_ENUM = [
|
|
4945
|
+
"seedance-2-fast",
|
|
4946
|
+
"kling-3",
|
|
4947
|
+
"grok-imagine",
|
|
4948
|
+
"veo3-fast",
|
|
4949
|
+
"kling-3-pro",
|
|
4950
|
+
"seedance-2",
|
|
4951
|
+
"veo3-quality",
|
|
4952
|
+
"wan-2.6",
|
|
4953
|
+
"gemini-omni-video",
|
|
4954
|
+
"hailuo-02-standard",
|
|
4955
|
+
"seedance-1.5-pro",
|
|
4956
|
+
"kling"
|
|
4957
|
+
];
|
|
4684
4958
|
IMAGE_CREDIT_ESTIMATES = {
|
|
4685
4959
|
midjourney: 20,
|
|
4686
4960
|
"nano-banana": 15,
|
|
@@ -4750,6 +5024,7 @@ var init_sanitize_error = __esm({
|
|
|
4750
5024
|
});
|
|
4751
5025
|
|
|
4752
5026
|
// src/lib/ssrf.ts
|
|
5027
|
+
import { promises as dnsPromises } from "node:dns";
|
|
4753
5028
|
function isBlockedIP(ip) {
|
|
4754
5029
|
const normalized = ip.replace(/^\[/, "").replace(/\]$/, "");
|
|
4755
5030
|
if (normalized.includes(":")) {
|
|
@@ -4803,8 +5078,7 @@ async function validateUrlForSSRF(urlString) {
|
|
|
4803
5078
|
let resolvedIP;
|
|
4804
5079
|
if (!isIPAddress(hostname)) {
|
|
4805
5080
|
try {
|
|
4806
|
-
const
|
|
4807
|
-
const resolver = new dns.promises.Resolver();
|
|
5081
|
+
const resolver = new dnsPromises.Resolver();
|
|
4808
5082
|
const resolvedIPs = [];
|
|
4809
5083
|
try {
|
|
4810
5084
|
const aRecords = await resolver.resolve4(hostname);
|
|
@@ -4930,6 +5204,24 @@ function asEnvelope3(data) {
|
|
|
4930
5204
|
data
|
|
4931
5205
|
};
|
|
4932
5206
|
}
|
|
5207
|
+
function accountEffectiveStatus(account) {
|
|
5208
|
+
return account.effective_status || account.status;
|
|
5209
|
+
}
|
|
5210
|
+
function isUsableAccount(account) {
|
|
5211
|
+
const status = accountEffectiveStatus(account);
|
|
5212
|
+
return status === "active" || status === "expires_soon";
|
|
5213
|
+
}
|
|
5214
|
+
function formatAccountChoice(account) {
|
|
5215
|
+
const name = account.username ? `@${account.username}` : "unnamed";
|
|
5216
|
+
const project = account.project_id ? `project_id=${account.project_id}` : "project_id=unassigned";
|
|
5217
|
+
const refresh = account.has_refresh_token ? "OAuth 2.0 refresh" : "no refresh token";
|
|
5218
|
+
return `${account.id} (${name}, ${project}, ${accountEffectiveStatus(account)}, ${refresh})`;
|
|
5219
|
+
}
|
|
5220
|
+
function requestedAccountIdForPlatform(accountId, accountIds, platform3) {
|
|
5221
|
+
if (accountId) return accountId;
|
|
5222
|
+
if (!accountIds) return void 0;
|
|
5223
|
+
return accountIds[platform3] || accountIds[platform3.toLowerCase()];
|
|
5224
|
+
}
|
|
4933
5225
|
function isAlreadyR2Signed(url) {
|
|
4934
5226
|
try {
|
|
4935
5227
|
const u = new URL(url);
|
|
@@ -5073,16 +5365,18 @@ function registerDistributionTools(server) {
|
|
|
5073
5365
|
schedule_at: z3.string().optional().describe(
|
|
5074
5366
|
'ISO 8601 UTC datetime for scheduled posting (e.g. "2026-03-20T14:00:00Z"). Omit to post immediately. Must be in the future.'
|
|
5075
5367
|
),
|
|
5076
|
-
project_id: z3.string().optional().describe(
|
|
5368
|
+
project_id: z3.string().optional().describe(
|
|
5369
|
+
"Social Neuron brand/project ID to associate this post with. Provide this when the account has multiple brands so brand voice and connected account routing stay scoped to the right brand."
|
|
5370
|
+
),
|
|
5077
5371
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
|
|
5078
5372
|
attribution: z3.boolean().optional().describe(
|
|
5079
5373
|
'If true, appends "Created with Social Neuron" to the caption. Default: false.'
|
|
5080
5374
|
),
|
|
5081
5375
|
account_id: z3.string().optional().describe(
|
|
5082
|
-
"Connected account ID to post from. Use list_connected_accounts to find the right ID. Required when multiple accounts exist for the same platform."
|
|
5376
|
+
"Connected account ID to post from. Use list_connected_accounts to find the right ID. Required when multiple accounts exist for the same platform. If project_id is provided, the account must belong to that project or be unassigned."
|
|
5083
5377
|
),
|
|
5084
5378
|
account_ids: z3.record(z3.string(), z3.string()).optional().describe(
|
|
5085
|
-
'Per-platform account IDs when posting to multiple platforms. Example: {"twitter": "abc123", "instagram": "def456"}. Use list_connected_accounts to find IDs.'
|
|
5379
|
+
'Per-platform account IDs when posting to multiple platforms. Example: {"twitter": "abc123", "instagram": "def456"}. Use list_connected_accounts with the same project_id to find IDs.'
|
|
5086
5380
|
),
|
|
5087
5381
|
auto_rehost: z3.boolean().optional().describe(
|
|
5088
5382
|
"Whether to persist non-R2 media_url/media_urls into R2 before posting. Default: true. Set to false only if you know the source URL will outlive the scheduling window and every target platform supports URL ingest."
|
|
@@ -5292,22 +5586,53 @@ function registerDistributionTools(server) {
|
|
|
5292
5586
|
isError: true
|
|
5293
5587
|
};
|
|
5294
5588
|
}
|
|
5295
|
-
const { data: accountsData } = await callEdgeFunction(
|
|
5589
|
+
const { data: accountsData } = await callEdgeFunction(
|
|
5590
|
+
"mcp-data",
|
|
5591
|
+
{
|
|
5592
|
+
action: "connected-accounts",
|
|
5593
|
+
...project_id ? { projectId: project_id, project_id } : {}
|
|
5594
|
+
},
|
|
5595
|
+
{ timeoutMs: 1e4 }
|
|
5596
|
+
);
|
|
5296
5597
|
if (accountsData?.accounts) {
|
|
5297
5598
|
const accounts = accountsData.accounts;
|
|
5298
5599
|
const issues = [];
|
|
5299
5600
|
for (const platform3 of normalizedPlatforms) {
|
|
5300
5601
|
const platformAccounts = accounts.filter(
|
|
5301
|
-
(a) => a.platform.toLowerCase() === platform3.toLowerCase() && a
|
|
5602
|
+
(a) => a.platform.toLowerCase() === platform3.toLowerCase() && isUsableAccount(a)
|
|
5302
5603
|
);
|
|
5604
|
+
const requestedAccountId = requestedAccountIdForPlatform(
|
|
5605
|
+
account_id,
|
|
5606
|
+
account_ids,
|
|
5607
|
+
platform3
|
|
5608
|
+
);
|
|
5609
|
+
if (requestedAccountId) {
|
|
5610
|
+
const selected = accounts.find((a) => a.id === requestedAccountId);
|
|
5611
|
+
if (!selected) {
|
|
5612
|
+
issues.push(
|
|
5613
|
+
`${platform3}: Account "${requestedAccountId}" is not available${project_id ? ` for project_id ${project_id}` : ""}. Call \`list_connected_accounts\`${project_id ? " with the same project_id" : ""} and choose one of the returned IDs.`
|
|
5614
|
+
);
|
|
5615
|
+
} else if (selected.platform.toLowerCase() !== platform3.toLowerCase()) {
|
|
5616
|
+
issues.push(
|
|
5617
|
+
`${platform3}: Account "${requestedAccountId}" belongs to ${selected.platform}, not ${platform3}.`
|
|
5618
|
+
);
|
|
5619
|
+
} else if (!isUsableAccount(selected)) {
|
|
5620
|
+
issues.push(
|
|
5621
|
+
`${platform3}: Account "${selected.username || selected.id}" is ${accountEffectiveStatus(selected)}. Reconnect at socialneuron.com/settings/connections.`
|
|
5622
|
+
);
|
|
5623
|
+
} else if (project_id && selected.project_id && selected.project_id !== project_id) {
|
|
5624
|
+
issues.push(
|
|
5625
|
+
`${platform3}: Account "${selected.username || selected.id}" is bound to project_id ${selected.project_id}, not ${project_id}. Use the selected brand's account or reconnect/assign it in Settings > Integrations.`
|
|
5626
|
+
);
|
|
5627
|
+
}
|
|
5628
|
+
continue;
|
|
5629
|
+
}
|
|
5303
5630
|
if (platformAccounts.length === 0) {
|
|
5304
5631
|
issues.push(
|
|
5305
|
-
`${platform3}: not connected yet. This is a one-time browser setup on socialneuron.com \u2014 NOT another OAuth in Claude. Call \`start_platform_connection\` with platform="${platform3.toLowerCase()}" to get a deep link, ask the user to open it in their browser and approve on the platform, then call \`wait_for_connection\` before retrying schedule_post.`
|
|
5632
|
+
`${platform3}: not connected yet. This is a one-time browser setup on socialneuron.com \u2014 NOT another OAuth in Claude. Call \`start_platform_connection\` with platform="${platform3.toLowerCase()}"${project_id ? ` and project_id="${project_id}"` : ""} to get a deep link, ask the user to open it in their browser and approve on the platform, then call \`wait_for_connection\` before retrying schedule_post.`
|
|
5306
5633
|
);
|
|
5307
|
-
} else if (platformAccounts.length > 1
|
|
5308
|
-
const accountList = platformAccounts.map(
|
|
5309
|
-
(a) => ` - ${a.id} (${a.username || "unnamed"}${a.has_refresh_token ? ", OAuth 2.0" : ", no refresh token"})`
|
|
5310
|
-
).join("\n");
|
|
5634
|
+
} else if (platformAccounts.length > 1) {
|
|
5635
|
+
const accountList = platformAccounts.map((a) => ` - ${formatAccountChoice(a)}`).join("\n");
|
|
5311
5636
|
issues.push(
|
|
5312
5637
|
`${platform3}: Multiple accounts found. Specify account_id or account_ids to choose:
|
|
5313
5638
|
${accountList}`
|
|
@@ -5462,13 +5787,21 @@ Created with Social Neuron`;
|
|
|
5462
5787
|
);
|
|
5463
5788
|
server.tool(
|
|
5464
5789
|
"list_connected_accounts",
|
|
5465
|
-
"Check which social platforms have active OAuth connections for posting. Call this before schedule_post to verify credentials. If a platform is missing or expired, the user needs to reconnect at socialneuron.com/settings/connections.",
|
|
5790
|
+
"Check which social platforms have active OAuth connections for posting. Call this before schedule_post to verify credentials. Pass project_id to list the accounts for a specific brand/project, then pass the returned account id as account_id/account_ids when posting. If a platform is missing or expired, the user needs to reconnect at socialneuron.com/settings/connections.",
|
|
5466
5791
|
{
|
|
5792
|
+
project_id: z3.string().optional().describe(
|
|
5793
|
+
"Brand/project ID to scope connected accounts. Use the same project_id when calling schedule_post."
|
|
5794
|
+
),
|
|
5795
|
+
include_all: z3.boolean().optional().describe("If true, include expired or inactive accounts as well as usable accounts."),
|
|
5467
5796
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
5468
5797
|
},
|
|
5469
|
-
async ({ response_format }) => {
|
|
5798
|
+
async ({ project_id, include_all, response_format }) => {
|
|
5470
5799
|
const format = response_format ?? "text";
|
|
5471
|
-
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
5800
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
5801
|
+
action: "connected-accounts",
|
|
5802
|
+
...project_id ? { projectId: project_id, project_id } : {},
|
|
5803
|
+
...include_all ? { includeAll: true } : {}
|
|
5804
|
+
});
|
|
5472
5805
|
if (efError || !result?.success) {
|
|
5473
5806
|
return {
|
|
5474
5807
|
content: [
|
|
@@ -5501,12 +5834,17 @@ Created with Social Neuron`;
|
|
|
5501
5834
|
]
|
|
5502
5835
|
};
|
|
5503
5836
|
}
|
|
5504
|
-
const lines = [
|
|
5837
|
+
const lines = [
|
|
5838
|
+
`${accounts.length} connected account(s)${project_id ? ` for project ${project_id}` : ""}:`,
|
|
5839
|
+
""
|
|
5840
|
+
];
|
|
5505
5841
|
for (const account of accounts) {
|
|
5506
5842
|
const name = account.username || "(unnamed)";
|
|
5507
5843
|
const platformLower = account.platform.toLowerCase();
|
|
5844
|
+
const project = account.project_id ? `project_id=${account.project_id}` : "project_id=unassigned";
|
|
5845
|
+
const status = accountEffectiveStatus(account);
|
|
5508
5846
|
lines.push(
|
|
5509
|
-
` ${platformLower}: ${name} (connected ${account.created_at.split("T")[0]})`
|
|
5847
|
+
` ${platformLower}: ${name} | id=${account.id} | ${project} | status=${status} (connected ${account.created_at.split("T")[0]})`
|
|
5510
5848
|
);
|
|
5511
5849
|
}
|
|
5512
5850
|
if (format === "json") {
|
|
@@ -6714,7 +7052,8 @@ function registerMediaTools(server) {
|
|
|
6714
7052
|
},
|
|
6715
7053
|
async ({ r2_key, response_format }) => {
|
|
6716
7054
|
const format = response_format ?? "text";
|
|
6717
|
-
const
|
|
7055
|
+
const normalizedR2Key = r2_key.startsWith("r2://") ? r2_key.slice("r2://".length) : r2_key;
|
|
7056
|
+
const { data, error } = await callEdgeFunction("get-signed-url", { r2Key: normalizedR2Key, operation: "get" }, { timeoutMs: 1e4 });
|
|
6718
7057
|
const signedDownloadUrl = data?.url ?? data?.signedUrl;
|
|
6719
7058
|
if (error) {
|
|
6720
7059
|
return {
|
|
@@ -6799,7 +7138,7 @@ function asEnvelope4(data) {
|
|
|
6799
7138
|
function registerAnalyticsTools(server) {
|
|
6800
7139
|
server.tool(
|
|
6801
7140
|
"fetch_analytics",
|
|
6802
|
-
"Get post performance metrics \u2014 views, likes, comments, shares, and engagement rate. Filter by platform, time range (default 30 days), or specific content_id. Call refresh_platform_analytics first if data seems stale. Results sorted by most recent capture.",
|
|
7141
|
+
"Get project-scoped post performance metrics \u2014 views, likes, comments, shares, and engagement rate. Filter by platform, time range (default 30 days), or specific content_id. Call refresh_platform_analytics first if data seems stale. Results sorted by most recent capture.",
|
|
6803
7142
|
{
|
|
6804
7143
|
platform: z5.enum([
|
|
6805
7144
|
"youtube",
|
|
@@ -6817,19 +7156,22 @@ function registerAnalyticsTools(server) {
|
|
|
6817
7156
|
content_id: z5.string().uuid().optional().describe(
|
|
6818
7157
|
"Filter to a specific content_history ID to see performance of one piece of content."
|
|
6819
7158
|
),
|
|
7159
|
+
project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
6820
7160
|
limit: z5.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
|
|
6821
7161
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6822
7162
|
},
|
|
6823
|
-
async ({ platform: platform3, days, content_id, limit, response_format }) => {
|
|
7163
|
+
async ({ platform: platform3, days, content_id, project_id, limit, response_format }) => {
|
|
6824
7164
|
const format = response_format ?? "text";
|
|
6825
7165
|
const lookbackDays = days ?? 30;
|
|
6826
7166
|
const maxPosts = limit ?? 20;
|
|
7167
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
6827
7168
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
6828
7169
|
action: "analytics",
|
|
6829
7170
|
platform: platform3,
|
|
6830
7171
|
days: lookbackDays,
|
|
6831
7172
|
limit: maxPosts,
|
|
6832
|
-
contentId: content_id
|
|
7173
|
+
contentId: content_id,
|
|
7174
|
+
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
6833
7175
|
});
|
|
6834
7176
|
if (efError) {
|
|
6835
7177
|
return {
|
|
@@ -6899,14 +7241,19 @@ function registerAnalyticsTools(server) {
|
|
|
6899
7241
|
);
|
|
6900
7242
|
server.tool(
|
|
6901
7243
|
"refresh_platform_analytics",
|
|
6902
|
-
"Queue analytics refresh jobs for
|
|
7244
|
+
"Queue analytics refresh jobs for posts from the last 7 days in one project across its connected platforms. Call this before fetch_analytics if you need fresh data. Returns immediately \u2014 data updates asynchronously over the next 1-5 minutes.",
|
|
6903
7245
|
{
|
|
7246
|
+
project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
6904
7247
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6905
7248
|
},
|
|
6906
|
-
async ({ response_format }) => {
|
|
7249
|
+
async ({ project_id, response_format }) => {
|
|
6907
7250
|
const format = response_format ?? "text";
|
|
6908
7251
|
const userId = await getDefaultUserId();
|
|
6909
|
-
const
|
|
7252
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
7253
|
+
const rateLimit = checkRateLimit(
|
|
7254
|
+
"posting",
|
|
7255
|
+
`refresh_platform_analytics:${userId}:${resolvedProjectId ?? "default"}`
|
|
7256
|
+
);
|
|
6910
7257
|
if (!rateLimit.allowed) {
|
|
6911
7258
|
return {
|
|
6912
7259
|
content: [
|
|
@@ -6918,7 +7265,10 @@ function registerAnalyticsTools(server) {
|
|
|
6918
7265
|
isError: true
|
|
6919
7266
|
};
|
|
6920
7267
|
}
|
|
6921
|
-
const { data, error } = await callEdgeFunction("fetch-analytics", {
|
|
7268
|
+
const { data, error } = await callEdgeFunction("fetch-analytics", {
|
|
7269
|
+
userId,
|
|
7270
|
+
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
7271
|
+
});
|
|
6922
7272
|
if (error) {
|
|
6923
7273
|
return {
|
|
6924
7274
|
content: [{ type: "text", text: `Error refreshing analytics: ${error}` }],
|
|
@@ -6947,7 +7297,8 @@ function registerAnalyticsTools(server) {
|
|
|
6947
7297
|
success: true,
|
|
6948
7298
|
postsProcessed: result.postsProcessed,
|
|
6949
7299
|
queued,
|
|
6950
|
-
errored
|
|
7300
|
+
errored,
|
|
7301
|
+
projectId: resolvedProjectId ?? null
|
|
6951
7302
|
});
|
|
6952
7303
|
return {
|
|
6953
7304
|
structuredContent,
|
|
@@ -8050,22 +8401,25 @@ function asEnvelope6(data) {
|
|
|
8050
8401
|
function registerInsightsTools(server) {
|
|
8051
8402
|
server.tool(
|
|
8052
8403
|
"get_performance_insights",
|
|
8053
|
-
"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.",
|
|
8404
|
+
"Query project-scoped 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.",
|
|
8054
8405
|
{
|
|
8055
8406
|
insight_type: z9.enum(["top_hooks", "optimal_timing", "best_models", "competitor_patterns"]).optional().describe("Filter to a specific insight type."),
|
|
8056
8407
|
days: z9.number().min(1).max(90).optional().describe("Number of days to look back. Defaults to 30. Max 90."),
|
|
8057
8408
|
limit: z9.number().min(1).max(50).optional().describe("Maximum number of insights to return. Defaults to 10."),
|
|
8409
|
+
project_id: z9.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
8058
8410
|
response_format: z9.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8059
8411
|
},
|
|
8060
|
-
async ({ insight_type, days, limit, response_format }) => {
|
|
8412
|
+
async ({ insight_type, days, limit, project_id, response_format }) => {
|
|
8061
8413
|
const format = response_format ?? "text";
|
|
8062
8414
|
const lookbackDays = days ?? 30;
|
|
8063
8415
|
const maxRows = limit ?? 10;
|
|
8064
8416
|
const effectiveDays = Math.min(lookbackDays, MAX_INSIGHT_AGE_DAYS);
|
|
8417
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
8065
8418
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
8066
8419
|
action: "performance-insights",
|
|
8067
8420
|
days: effectiveDays,
|
|
8068
|
-
limit: maxRows
|
|
8421
|
+
limit: maxRows,
|
|
8422
|
+
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
8069
8423
|
});
|
|
8070
8424
|
if (efError || !result?.success) {
|
|
8071
8425
|
return {
|
|
@@ -8152,19 +8506,22 @@ function registerInsightsTools(server) {
|
|
|
8152
8506
|
);
|
|
8153
8507
|
server.tool(
|
|
8154
8508
|
"get_best_posting_times",
|
|
8155
|
-
"Analyze post analytics data to find the best times to post for maximum engagement. Returns the top 5 time slots (day of week + hour) ranked by average engagement.",
|
|
8509
|
+
"Analyze project-scoped post analytics data to find the best times to post for maximum engagement. Returns the top 5 time slots (day of week + hour) ranked by average engagement.",
|
|
8156
8510
|
{
|
|
8157
8511
|
platform: z9.enum(PLATFORM_ENUM).optional().describe("Filter to a specific platform."),
|
|
8158
8512
|
days: z9.number().min(1).max(90).optional().describe("Number of days to analyze. Defaults to 30. Max 90."),
|
|
8513
|
+
project_id: z9.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
8159
8514
|
response_format: z9.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8160
8515
|
},
|
|
8161
|
-
async ({ platform: platform3, days, response_format }) => {
|
|
8516
|
+
async ({ platform: platform3, days, project_id, response_format }) => {
|
|
8162
8517
|
const format = response_format ?? "text";
|
|
8163
8518
|
const lookbackDays = days ?? 30;
|
|
8519
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
8164
8520
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
8165
8521
|
action: "best-posting-times",
|
|
8166
8522
|
days: lookbackDays,
|
|
8167
|
-
platform: platform3 ?? void 0
|
|
8523
|
+
platform: platform3 ?? void 0,
|
|
8524
|
+
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
8168
8525
|
});
|
|
8169
8526
|
if (efError || !result?.success) {
|
|
8170
8527
|
return {
|
|
@@ -8278,6 +8635,7 @@ var init_insights = __esm({
|
|
|
8278
8635
|
"src/tools/insights.ts"() {
|
|
8279
8636
|
"use strict";
|
|
8280
8637
|
init_edge_function();
|
|
8638
|
+
init_supabase();
|
|
8281
8639
|
init_version();
|
|
8282
8640
|
MAX_INSIGHT_AGE_DAYS = 30;
|
|
8283
8641
|
PLATFORM_ENUM = [
|
|
@@ -9321,7 +9679,7 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
|
|
|
9321
9679
|
}
|
|
9322
9680
|
const statusData = {
|
|
9323
9681
|
activeConfigs: result?.activeConfigs ?? 0,
|
|
9324
|
-
recentRuns: [],
|
|
9682
|
+
recentRuns: result?.recentRuns ?? result?.recent_runs ?? [],
|
|
9325
9683
|
pendingApprovals: result?.pendingApprovals ?? 0
|
|
9326
9684
|
};
|
|
9327
9685
|
if (format === "json") {
|
|
@@ -9343,8 +9701,20 @@ ${"=".repeat(40)}
|
|
|
9343
9701
|
text += `Pending Approvals: ${statusData.pendingApprovals}
|
|
9344
9702
|
|
|
9345
9703
|
`;
|
|
9346
|
-
|
|
9704
|
+
if (statusData.recentRuns.length === 0) {
|
|
9705
|
+
text += `No recent runs.
|
|
9706
|
+
`;
|
|
9707
|
+
} else {
|
|
9708
|
+
text += `Recent Runs (${statusData.recentRuns.length}):
|
|
9709
|
+
`;
|
|
9710
|
+
for (const run of statusData.recentRuns.slice(0, 10)) {
|
|
9711
|
+
const id = String(run.id ?? run.run_id ?? "unknown");
|
|
9712
|
+
const status = String(run.status ?? "unknown");
|
|
9713
|
+
const credits = run.credits_used ?? run.creditsUsed;
|
|
9714
|
+
text += ` ${id}: ${status}${credits == null ? "" : ` (${String(credits)} credits)`}
|
|
9347
9715
|
`;
|
|
9716
|
+
}
|
|
9717
|
+
}
|
|
9348
9718
|
return {
|
|
9349
9719
|
content: [{ type: "text", text }]
|
|
9350
9720
|
};
|
|
@@ -14122,7 +14492,7 @@ function registerCarouselTools(server) {
|
|
|
14122
14492
|
"Explicit hook/opener for slide 1. Overrides any hook derived from topic. Keep under 15 words for strongest scroll-stop."
|
|
14123
14493
|
),
|
|
14124
14494
|
hook_family: z28.enum(["curiosity", "authority", "pain_point", "contrarian", "data_driven"]).optional().describe(
|
|
14125
|
-
"Hook family tag. Recorded on the carousel so downstream analytics
|
|
14495
|
+
"Hook family tag. Recorded on the carousel so downstream analytics can attribute engagement to hook pattern."
|
|
14126
14496
|
),
|
|
14127
14497
|
cta_text: z28.string().max(200).optional().describe("Explicit CTA copy for the final slide. If omitted, derived from topic."),
|
|
14128
14498
|
cta_url: z28.string().url().optional().describe("URL promoted on the CTA slide. Defaults to the project landing page."),
|
|
@@ -14514,7 +14884,7 @@ function registerHyperframesTools(server) {
|
|
|
14514
14884
|
);
|
|
14515
14885
|
server.tool(
|
|
14516
14886
|
"render_hyperframes",
|
|
14517
|
-
"Render an HTML video composition (Hyperframes) to MP4. Author the composition as HTML with data-* timing attributes and GSAP timelines \u2014 frame-accurate, no React build step. Use list_hyperframes_blocks to see the pre-built block catalog.
|
|
14887
|
+
"Render an HTML video composition (Hyperframes) to MP4. Author the composition as HTML with data-* timing attributes and GSAP timelines \u2014 frame-accurate, no React build step. Use list_hyperframes_blocks to see the pre-built block catalog. Pass project_id to keep the render with the correct brand/project. Returns a job ID \u2014 poll with check_status.",
|
|
14518
14888
|
{
|
|
14519
14889
|
composition_html: z30.string().max(5e5).optional().describe(
|
|
14520
14890
|
"Inline HTML composition (full <html>...</html>). Max 500KB. Use composition_url for larger."
|
|
@@ -14526,7 +14896,8 @@ function registerHyperframesTools(server) {
|
|
|
14526
14896
|
aspect_ratio: z30.enum(["9:16", "16:9", "1:1"]).optional().describe('Output aspect ratio. Default "9:16".'),
|
|
14527
14897
|
duration_sec: z30.number().min(0.1).max(600).describe("Output duration in seconds. Required. Max 600 (10 min)."),
|
|
14528
14898
|
fps: z30.union([z30.literal(24), z30.literal(30), z30.literal(60)]).optional().describe("Frames per second. Default 30."),
|
|
14529
|
-
quality: z30.enum(["draft", "standard", "high"]).optional().describe("draft: fast iteration; standard: production; high: final master.")
|
|
14899
|
+
quality: z30.enum(["draft", "standard", "high"]).optional().describe("draft: fast iteration; standard: production; high: final master."),
|
|
14900
|
+
project_id: z30.string().optional().describe("Project ID to associate the Hyperframes render with.")
|
|
14530
14901
|
},
|
|
14531
14902
|
async ({
|
|
14532
14903
|
composition_html,
|
|
@@ -14535,7 +14906,8 @@ function registerHyperframesTools(server) {
|
|
|
14535
14906
|
aspect_ratio,
|
|
14536
14907
|
duration_sec,
|
|
14537
14908
|
fps,
|
|
14538
|
-
quality
|
|
14909
|
+
quality,
|
|
14910
|
+
project_id
|
|
14539
14911
|
}) => {
|
|
14540
14912
|
const userId = await getDefaultUserId();
|
|
14541
14913
|
const rateLimit = checkRateLimit("generation", `render_hyperframes:${userId}`);
|
|
@@ -14580,7 +14952,8 @@ function registerHyperframesTools(server) {
|
|
|
14580
14952
|
aspectRatio: aspect_ratio || "9:16",
|
|
14581
14953
|
durationSec: duration_sec,
|
|
14582
14954
|
fps: fps || 30,
|
|
14583
|
-
quality: quality || "standard"
|
|
14955
|
+
quality: quality || "standard",
|
|
14956
|
+
...project_id && { projectId: project_id }
|
|
14584
14957
|
});
|
|
14585
14958
|
if (error || !data?.jobId) {
|
|
14586
14959
|
throw new Error(error || data?.error || "Failed to create Hyperframes render job");
|
|
@@ -14596,7 +14969,7 @@ function registerHyperframesTools(server) {
|
|
|
14596
14969
|
` Duration: ${duration_sec}s @ ${fps || 30}fps (${aspect_ratio || "9:16"})`,
|
|
14597
14970
|
` Quality: ${quality || "standard"}`,
|
|
14598
14971
|
``,
|
|
14599
|
-
`Poll with check_status
|
|
14972
|
+
`Poll with check_status.`
|
|
14600
14973
|
].join("\n")
|
|
14601
14974
|
}
|
|
14602
14975
|
]
|
|
@@ -14869,7 +15242,10 @@ var init_content_calendar = __esm({
|
|
|
14869
15242
|
import { z as z32 } from "zod";
|
|
14870
15243
|
function findActiveAccount(accounts, platform3) {
|
|
14871
15244
|
const target = platform3.toLowerCase();
|
|
14872
|
-
return accounts.find((a) =>
|
|
15245
|
+
return accounts.find((a) => {
|
|
15246
|
+
const effectiveStatus = a.effective_status || a.status;
|
|
15247
|
+
return a.platform.toLowerCase() === target && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
|
|
15248
|
+
}) ?? null;
|
|
14873
15249
|
}
|
|
14874
15250
|
function registerConnectionTools(server) {
|
|
14875
15251
|
server.tool(
|
|
@@ -14877,11 +15253,15 @@ function registerConnectionTools(server) {
|
|
|
14877
15253
|
"Begin connecting a social platform (Instagram, TikTok, YouTube, etc.). Returns a single-use deep link the user opens in a browser to complete the one-time OAuth handshake on socialneuron.com. This is NOT another OAuth in Claude \u2014 platform connections require a browser session because the social platforms (Meta, Google, TikTok) only accept callbacks on socialneuron.com. After the user clicks the link and approves on the platform, call `wait_for_connection` to detect completion. Link expires in 2 minutes; mint a new one if needed. Use `list_connected_accounts` first to check whether the platform is already connected before calling this.",
|
|
14878
15254
|
{
|
|
14879
15255
|
platform: z32.enum(PLATFORM_ENUM4).describe("Platform to connect. Lower-case: instagram, tiktok, youtube, etc."),
|
|
15256
|
+
project_id: z32.string().optional().describe(
|
|
15257
|
+
"Brand/project ID to bind the new social account to. Use this when connecting a brand-specific account."
|
|
15258
|
+
),
|
|
14880
15259
|
response_format: z32.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
14881
15260
|
},
|
|
14882
|
-
async ({ platform: platform3, response_format }) => {
|
|
15261
|
+
async ({ platform: platform3, project_id, response_format }) => {
|
|
14883
15262
|
const format = response_format ?? "text";
|
|
14884
|
-
const
|
|
15263
|
+
const userId = await getDefaultUserId();
|
|
15264
|
+
const rl = checkRateLimit("read", `start_platform_connection:${userId}`);
|
|
14885
15265
|
if (!rl.allowed) {
|
|
14886
15266
|
return {
|
|
14887
15267
|
content: [
|
|
@@ -14893,7 +15273,15 @@ function registerConnectionTools(server) {
|
|
|
14893
15273
|
isError: true
|
|
14894
15274
|
};
|
|
14895
15275
|
}
|
|
14896
|
-
const { data, error } = await callEdgeFunction(
|
|
15276
|
+
const { data, error } = await callEdgeFunction(
|
|
15277
|
+
"mcp-data",
|
|
15278
|
+
{
|
|
15279
|
+
action: "mint-connection-nonce",
|
|
15280
|
+
platform: platform3,
|
|
15281
|
+
...project_id ? { projectId: project_id, project_id } : {}
|
|
15282
|
+
},
|
|
15283
|
+
{ timeoutMs: 1e4 }
|
|
15284
|
+
);
|
|
14897
15285
|
if (error || !data?.success || !data.deep_link) {
|
|
14898
15286
|
const errMsg = error ?? data?.error ?? "Unknown error";
|
|
14899
15287
|
return {
|
|
@@ -14914,6 +15302,7 @@ function registerConnectionTools(server) {
|
|
|
14914
15302
|
text: JSON.stringify(
|
|
14915
15303
|
{
|
|
14916
15304
|
platform: data.platform,
|
|
15305
|
+
project_id: project_id ?? null,
|
|
14917
15306
|
deep_link: data.deep_link,
|
|
14918
15307
|
expires_at: data.expires_at,
|
|
14919
15308
|
next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection."
|
|
@@ -14932,6 +15321,7 @@ function registerConnectionTools(server) {
|
|
|
14932
15321
|
type: "text",
|
|
14933
15322
|
text: [
|
|
14934
15323
|
`${data.platform} connection ready.`,
|
|
15324
|
+
...project_id ? [`Brand/project: ${project_id}`] : [],
|
|
14935
15325
|
"",
|
|
14936
15326
|
'Ask the user to open this link in a browser and click "Connect" on the platform:',
|
|
14937
15327
|
` ${data.deep_link}`,
|
|
@@ -14952,17 +15342,21 @@ function registerConnectionTools(server) {
|
|
|
14952
15342
|
"Poll until a platform connection becomes active. Use after `start_platform_connection` while the user completes the browser OAuth flow. Returns when the account row appears with status=active, or when the timeout elapses. Default timeout 120s, max 600s.",
|
|
14953
15343
|
{
|
|
14954
15344
|
platform: z32.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
|
|
15345
|
+
project_id: z32.string().optional().describe(
|
|
15346
|
+
"Brand/project ID to scope the connection poll. Use the same project_id passed to start_platform_connection."
|
|
15347
|
+
),
|
|
14955
15348
|
timeout_s: z32.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
|
|
14956
15349
|
poll_interval_s: z32.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
|
|
14957
15350
|
response_format: z32.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
14958
15351
|
},
|
|
14959
|
-
async ({ platform: platform3, timeout_s, poll_interval_s, response_format }) => {
|
|
15352
|
+
async ({ platform: platform3, project_id, timeout_s, poll_interval_s, response_format }) => {
|
|
14960
15353
|
const format = response_format ?? "text";
|
|
14961
15354
|
const startedAt = Date.now();
|
|
14962
15355
|
const timeoutMs = (timeout_s ?? 120) * 1e3;
|
|
14963
15356
|
const intervalMs = (poll_interval_s ?? 5) * 1e3;
|
|
14964
15357
|
const deadline = startedAt + timeoutMs;
|
|
14965
|
-
const
|
|
15358
|
+
const userId = await getDefaultUserId();
|
|
15359
|
+
const rl = checkRateLimit("read", `wait_for_connection:${userId}`);
|
|
14966
15360
|
if (!rl.allowed) {
|
|
14967
15361
|
return {
|
|
14968
15362
|
content: [
|
|
@@ -14974,90 +15368,123 @@ function registerConnectionTools(server) {
|
|
|
14974
15368
|
isError: true
|
|
14975
15369
|
};
|
|
14976
15370
|
}
|
|
14977
|
-
|
|
14978
|
-
|
|
14979
|
-
|
|
14980
|
-
|
|
14981
|
-
|
|
14982
|
-
|
|
14983
|
-
|
|
14984
|
-
|
|
15371
|
+
const activeWaits = inFlightWaitsByUser.get(userId) ?? 0;
|
|
15372
|
+
if (activeWaits >= MAX_CONCURRENT_WAITS_PER_USER) {
|
|
15373
|
+
return {
|
|
15374
|
+
content: [
|
|
15375
|
+
{
|
|
15376
|
+
type: "text",
|
|
15377
|
+
text: `Too many concurrent \`wait_for_connection\` calls (max ${MAX_CONCURRENT_WAITS_PER_USER}). Let an existing wait finish, or poll \`list_connected_accounts\` instead.`
|
|
15378
|
+
}
|
|
15379
|
+
],
|
|
15380
|
+
isError: true
|
|
15381
|
+
};
|
|
15382
|
+
}
|
|
15383
|
+
inFlightWaitsByUser.set(userId, activeWaits + 1);
|
|
15384
|
+
try {
|
|
15385
|
+
let attempts = 0;
|
|
15386
|
+
while (Date.now() < deadline) {
|
|
15387
|
+
attempts++;
|
|
15388
|
+
const { data, error } = await callEdgeFunction(
|
|
15389
|
+
"mcp-data",
|
|
15390
|
+
{
|
|
15391
|
+
action: "connected-accounts",
|
|
15392
|
+
...project_id ? { projectId: project_id, project_id } : {}
|
|
15393
|
+
},
|
|
15394
|
+
{ timeoutMs: 1e4 }
|
|
15395
|
+
);
|
|
15396
|
+
if (!error && data?.success) {
|
|
15397
|
+
const found = findActiveAccount(data.accounts ?? [], platform3);
|
|
15398
|
+
if (found) {
|
|
15399
|
+
if (format === "json") {
|
|
15400
|
+
return {
|
|
15401
|
+
content: [
|
|
15402
|
+
{
|
|
15403
|
+
type: "text",
|
|
15404
|
+
text: JSON.stringify(
|
|
15405
|
+
{
|
|
15406
|
+
connected: true,
|
|
15407
|
+
platform: found.platform,
|
|
15408
|
+
project_id: found.project_id ?? project_id ?? null,
|
|
15409
|
+
account_id: found.id,
|
|
15410
|
+
username: found.username,
|
|
15411
|
+
connected_at: found.created_at,
|
|
15412
|
+
attempts
|
|
15413
|
+
},
|
|
15414
|
+
null,
|
|
15415
|
+
2
|
|
15416
|
+
)
|
|
15417
|
+
}
|
|
15418
|
+
],
|
|
15419
|
+
isError: false
|
|
15420
|
+
};
|
|
15421
|
+
}
|
|
14985
15422
|
return {
|
|
14986
15423
|
content: [
|
|
14987
15424
|
{
|
|
14988
15425
|
type: "text",
|
|
14989
|
-
text:
|
|
14990
|
-
{
|
|
14991
|
-
|
|
14992
|
-
|
|
14993
|
-
|
|
14994
|
-
|
|
14995
|
-
|
|
14996
|
-
attempts
|
|
14997
|
-
},
|
|
14998
|
-
null,
|
|
14999
|
-
2
|
|
15000
|
-
)
|
|
15426
|
+
text: [
|
|
15427
|
+
`${found.platform} is connected.`,
|
|
15428
|
+
...found.project_id || project_id ? [`Brand/project: ${found.project_id ?? project_id}`] : [],
|
|
15429
|
+
`Account: ${found.username || "(unnamed)"} (id=${found.id})`,
|
|
15430
|
+
`Detected after ${attempts} poll(s) in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s.`,
|
|
15431
|
+
"Ready to call `schedule_post`."
|
|
15432
|
+
].join("\n")
|
|
15001
15433
|
}
|
|
15002
15434
|
],
|
|
15003
15435
|
isError: false
|
|
15004
15436
|
};
|
|
15005
15437
|
}
|
|
15006
|
-
return {
|
|
15007
|
-
content: [
|
|
15008
|
-
{
|
|
15009
|
-
type: "text",
|
|
15010
|
-
text: [
|
|
15011
|
-
`${found.platform} is connected.`,
|
|
15012
|
-
`Account: ${found.username || "(unnamed)"} (id=${found.id})`,
|
|
15013
|
-
`Detected after ${attempts} poll(s) in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s.`,
|
|
15014
|
-
"Ready to call `schedule_post`."
|
|
15015
|
-
].join("\n")
|
|
15016
|
-
}
|
|
15017
|
-
],
|
|
15018
|
-
isError: false
|
|
15019
|
-
};
|
|
15020
15438
|
}
|
|
15439
|
+
const remaining = deadline - Date.now();
|
|
15440
|
+
if (remaining <= 0) break;
|
|
15441
|
+
await new Promise((resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining)));
|
|
15442
|
+
}
|
|
15443
|
+
const message = `${platform3} did not connect within ${timeout_s ?? 120}s (${attempts} polls). The user may not have completed the browser OAuth yet, or the link expired. Mint a new link with \`start_platform_connection\` and try again, or have the user go directly to socialneuron.com/settings/connections.`;
|
|
15444
|
+
if (format === "json") {
|
|
15445
|
+
return {
|
|
15446
|
+
content: [
|
|
15447
|
+
{
|
|
15448
|
+
type: "text",
|
|
15449
|
+
text: JSON.stringify(
|
|
15450
|
+
{
|
|
15451
|
+
connected: false,
|
|
15452
|
+
platform: platform3,
|
|
15453
|
+
project_id: project_id ?? null,
|
|
15454
|
+
attempts,
|
|
15455
|
+
timed_out: true,
|
|
15456
|
+
message
|
|
15457
|
+
},
|
|
15458
|
+
null,
|
|
15459
|
+
2
|
|
15460
|
+
)
|
|
15461
|
+
}
|
|
15462
|
+
],
|
|
15463
|
+
isError: true
|
|
15464
|
+
};
|
|
15021
15465
|
}
|
|
15022
|
-
const remaining = deadline - Date.now();
|
|
15023
|
-
if (remaining <= 0) break;
|
|
15024
|
-
await new Promise((resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining)));
|
|
15025
|
-
}
|
|
15026
|
-
const message = `${platform3} did not connect within ${timeout_s ?? 120}s (${attempts} polls). The user may not have completed the browser OAuth yet, or the link expired. Mint a new link with \`start_platform_connection\` and try again, or have the user go directly to socialneuron.com/settings/connections.`;
|
|
15027
|
-
if (format === "json") {
|
|
15028
15466
|
return {
|
|
15029
|
-
content: [
|
|
15030
|
-
{
|
|
15031
|
-
type: "text",
|
|
15032
|
-
text: JSON.stringify(
|
|
15033
|
-
{
|
|
15034
|
-
connected: false,
|
|
15035
|
-
platform: platform3,
|
|
15036
|
-
attempts,
|
|
15037
|
-
timed_out: true,
|
|
15038
|
-
message
|
|
15039
|
-
},
|
|
15040
|
-
null,
|
|
15041
|
-
2
|
|
15042
|
-
)
|
|
15043
|
-
}
|
|
15044
|
-
],
|
|
15467
|
+
content: [{ type: "text", text: message }],
|
|
15045
15468
|
isError: true
|
|
15046
15469
|
};
|
|
15470
|
+
} finally {
|
|
15471
|
+
const remainingWaits = (inFlightWaitsByUser.get(userId) ?? 1) - 1;
|
|
15472
|
+
if (remainingWaits <= 0) {
|
|
15473
|
+
inFlightWaitsByUser.delete(userId);
|
|
15474
|
+
} else {
|
|
15475
|
+
inFlightWaitsByUser.set(userId, remainingWaits);
|
|
15476
|
+
}
|
|
15047
15477
|
}
|
|
15048
|
-
return {
|
|
15049
|
-
content: [{ type: "text", text: message }],
|
|
15050
|
-
isError: true
|
|
15051
|
-
};
|
|
15052
15478
|
}
|
|
15053
15479
|
);
|
|
15054
15480
|
}
|
|
15055
|
-
var PLATFORM_ENUM4;
|
|
15481
|
+
var PLATFORM_ENUM4, MAX_CONCURRENT_WAITS_PER_USER, inFlightWaitsByUser;
|
|
15056
15482
|
var init_connections = __esm({
|
|
15057
15483
|
"src/tools/connections.ts"() {
|
|
15058
15484
|
"use strict";
|
|
15059
15485
|
init_edge_function();
|
|
15060
15486
|
init_rate_limit();
|
|
15487
|
+
init_supabase();
|
|
15061
15488
|
PLATFORM_ENUM4 = [
|
|
15062
15489
|
"youtube",
|
|
15063
15490
|
"tiktok",
|
|
@@ -15070,6 +15497,8 @@ var init_connections = __esm({
|
|
|
15070
15497
|
"shopify",
|
|
15071
15498
|
"etsy"
|
|
15072
15499
|
];
|
|
15500
|
+
MAX_CONCURRENT_WAITS_PER_USER = 3;
|
|
15501
|
+
inFlightWaitsByUser = /* @__PURE__ */ new Map();
|
|
15073
15502
|
}
|
|
15074
15503
|
});
|
|
15075
15504
|
|
|
@@ -15934,7 +16363,12 @@ function applyScopeEnforcement(server, scopeResolver) {
|
|
|
15934
16363
|
toolName: name,
|
|
15935
16364
|
status,
|
|
15936
16365
|
durationMs: Date.now() - startedAt,
|
|
15937
|
-
details: {
|
|
16366
|
+
details: {
|
|
16367
|
+
source: "wrapper",
|
|
16368
|
+
// Classify failures so the tool-error rate is diagnosable
|
|
16369
|
+
// (validation vs permission vs billing vs upstream vs server).
|
|
16370
|
+
...status === "error" ? { error_type: classifyToolError(result) } : {}
|
|
16371
|
+
}
|
|
15938
16372
|
});
|
|
15939
16373
|
return truncateResponse(result);
|
|
15940
16374
|
} catch (err) {
|
|
@@ -15944,6 +16378,8 @@ function applyScopeEnforcement(server, scopeResolver) {
|
|
|
15944
16378
|
durationMs: Date.now() - startedAt,
|
|
15945
16379
|
details: {
|
|
15946
16380
|
source: "wrapper",
|
|
16381
|
+
// A thrown exception escaped the handler — an unclassified fault.
|
|
16382
|
+
error_type: "server_error",
|
|
15947
16383
|
exception: err instanceof Error ? err.message.slice(0, 200) : "unknown"
|
|
15948
16384
|
}
|
|
15949
16385
|
});
|
|
@@ -16037,6 +16473,7 @@ function truncateResponse(result) {
|
|
|
16037
16473
|
return { ...result, content: truncated };
|
|
16038
16474
|
}
|
|
16039
16475
|
function registerAllTools(server, options) {
|
|
16476
|
+
applyToolProfile(server, options?.toolProfile ?? "full");
|
|
16040
16477
|
registerIdeationTools(server);
|
|
16041
16478
|
registerContentTools(server);
|
|
16042
16479
|
registerDistributionTools(server);
|
|
@@ -16089,6 +16526,7 @@ var init_register_tools = __esm({
|
|
|
16089
16526
|
init_supabase();
|
|
16090
16527
|
init_www_authenticate();
|
|
16091
16528
|
init_tool_error();
|
|
16529
|
+
init_tool_profile();
|
|
16092
16530
|
init_scanner();
|
|
16093
16531
|
init_ideation();
|
|
16094
16532
|
init_content2();
|
|
@@ -16173,13 +16611,13 @@ async function invokeToolRest(name, args) {
|
|
|
16173
16611
|
}
|
|
16174
16612
|
function extractRestError(result) {
|
|
16175
16613
|
const sc = result.structuredContent?.error;
|
|
16176
|
-
if (sc?.error_type &&
|
|
16614
|
+
if (sc?.error_type && KNOWN_ERROR_TYPES2.has(sc.error_type)) {
|
|
16177
16615
|
return { error_type: sc.error_type, message: sc.message ?? "Tool error." };
|
|
16178
16616
|
}
|
|
16179
16617
|
const text = result.content?.find((c) => c.type === "text")?.text ?? "";
|
|
16180
16618
|
try {
|
|
16181
16619
|
const parsed = JSON.parse(text);
|
|
16182
|
-
if (parsed.error_type &&
|
|
16620
|
+
if (parsed.error_type && KNOWN_ERROR_TYPES2.has(parsed.error_type)) {
|
|
16183
16621
|
return { error_type: parsed.error_type, message: parsed.message ?? text };
|
|
16184
16622
|
}
|
|
16185
16623
|
} catch {
|
|
@@ -16189,7 +16627,7 @@ function extractRestError(result) {
|
|
|
16189
16627
|
}
|
|
16190
16628
|
return { error_type: "server_error", message: text || "Tool error." };
|
|
16191
16629
|
}
|
|
16192
|
-
var cachedCallHandler,
|
|
16630
|
+
var cachedCallHandler, KNOWN_ERROR_TYPES2;
|
|
16193
16631
|
var init_rest_invoke = __esm({
|
|
16194
16632
|
"src/lib/rest-invoke.ts"() {
|
|
16195
16633
|
"use strict";
|
|
@@ -16197,7 +16635,7 @@ var init_rest_invoke = __esm({
|
|
|
16197
16635
|
init_request_context();
|
|
16198
16636
|
init_version();
|
|
16199
16637
|
init_tool_catalog();
|
|
16200
|
-
|
|
16638
|
+
KNOWN_ERROR_TYPES2 = /* @__PURE__ */ new Set([
|
|
16201
16639
|
"policy_block",
|
|
16202
16640
|
"validation_error",
|
|
16203
16641
|
"permission_denied",
|
|
@@ -16274,7 +16712,9 @@ async function handleCall(args, asJson) {
|
|
|
16274
16712
|
scopes: auth.scopes ?? [],
|
|
16275
16713
|
token: apiKey,
|
|
16276
16714
|
creditsUsed: 0,
|
|
16277
|
-
assetsGenerated: 0
|
|
16715
|
+
assetsGenerated: 0,
|
|
16716
|
+
surface: "cli",
|
|
16717
|
+
projectId: auth.projectId ?? null
|
|
16278
16718
|
},
|
|
16279
16719
|
() => invokeToolRest(toolName, toolArgs)
|
|
16280
16720
|
);
|