@socialneuron/mcp-server 1.8.2 → 1.9.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 +37 -0
- package/dist/http.js +1908 -649
- package/dist/index.js +1864 -680
- package/dist/sn.js +1859 -675
- package/package.json +6 -3
- package/tools.lock.json +15 -15
package/dist/http.js
CHANGED
|
@@ -46,6 +46,209 @@ var init_request_context = __esm({
|
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
// src/lib/edge-function.ts
|
|
50
|
+
var edge_function_exports = {};
|
|
51
|
+
__export(edge_function_exports, {
|
|
52
|
+
callEdgeFunction: () => callEdgeFunction
|
|
53
|
+
});
|
|
54
|
+
function safeGatewayError(responseText, status) {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(responseText);
|
|
57
|
+
const nested = parsed.error && typeof parsed.error === "object" ? parsed.error : null;
|
|
58
|
+
const candidates = [
|
|
59
|
+
nested?.error_type,
|
|
60
|
+
nested?.code,
|
|
61
|
+
parsed.error_type,
|
|
62
|
+
parsed.error_code,
|
|
63
|
+
parsed.code,
|
|
64
|
+
typeof parsed.error === "string" ? parsed.error : null
|
|
65
|
+
];
|
|
66
|
+
for (const value of candidates) {
|
|
67
|
+
if (typeof value !== "string") continue;
|
|
68
|
+
const normalized = value.trim().toLowerCase();
|
|
69
|
+
if (SAFE_GATEWAY_ERROR_CODES.has(normalized)) return normalized;
|
|
70
|
+
const embedded = [...SAFE_GATEWAY_ERROR_CODES].find((code) => normalized.includes(code));
|
|
71
|
+
if (embedded) return embedded;
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
return `Backend request failed (HTTP ${status}).`;
|
|
76
|
+
}
|
|
77
|
+
function safeFailureData(responseText) {
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(responseText);
|
|
80
|
+
const metric = (name) => {
|
|
81
|
+
const value = parsed[name];
|
|
82
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
83
|
+
};
|
|
84
|
+
const billingStatus = typeof parsed.billing_status === "string" && SAFE_BILLING_STATUSES.has(parsed.billing_status) ? parsed.billing_status : void 0;
|
|
85
|
+
const failureReason = parsed.failure_reason === "generation_failed" || parsed.failure_reason === "authentication_failed" || parsed.failure_reason === "cancelled_by_user" ? parsed.failure_reason : void 0;
|
|
86
|
+
const jobStatus = typeof parsed.status === "string" && SAFE_JOB_STATUSES.has(parsed.status) ? parsed.status : void 0;
|
|
87
|
+
const safe = {
|
|
88
|
+
...jobStatus ? { status: jobStatus } : {},
|
|
89
|
+
...metric("credits_reserved") !== void 0 ? { credits_reserved: metric("credits_reserved") } : {},
|
|
90
|
+
...metric("credits_charged") !== void 0 ? { credits_charged: metric("credits_charged") } : {},
|
|
91
|
+
...metric("credits_refunded") !== void 0 ? { credits_refunded: metric("credits_refunded") } : {},
|
|
92
|
+
...billingStatus ? { billing_status: billingStatus } : {},
|
|
93
|
+
...failureReason ? { failure_reason: failureReason } : {}
|
|
94
|
+
};
|
|
95
|
+
return Object.keys(safe).length > 0 ? safe : null;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function getApiKeyOrNull() {
|
|
101
|
+
const envKey = process.env.SOCIALNEURON_API_KEY;
|
|
102
|
+
if (envKey && envKey.trim().length) return envKey.trim();
|
|
103
|
+
const requestToken = getRequestToken();
|
|
104
|
+
if (requestToken) return requestToken;
|
|
105
|
+
return getAuthenticatedApiKey();
|
|
106
|
+
}
|
|
107
|
+
async function callEdgeFunction(functionName, body, options) {
|
|
108
|
+
const supabaseUrl = getSupabaseUrl();
|
|
109
|
+
const apiKey = getApiKeyOrNull();
|
|
110
|
+
const controller = new AbortController();
|
|
111
|
+
const timeoutMs = options?.timeoutMs ?? 6e4;
|
|
112
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
113
|
+
const enrichedBody = { ...body };
|
|
114
|
+
if (!enrichedBody.userId && !enrichedBody.user_id) {
|
|
115
|
+
try {
|
|
116
|
+
const defaultId = await getDefaultUserId();
|
|
117
|
+
enrichedBody.userId = defaultId;
|
|
118
|
+
enrichedBody.user_id = defaultId;
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
if (enrichedBody.userId && !enrichedBody.user_id) enrichedBody.user_id = enrichedBody.userId;
|
|
123
|
+
if (enrichedBody.user_id && !enrichedBody.userId) enrichedBody.userId = enrichedBody.user_id;
|
|
124
|
+
}
|
|
125
|
+
if (!enrichedBody.projectId && !enrichedBody.project_id) {
|
|
126
|
+
try {
|
|
127
|
+
const { getDefaultProjectId: getDefaultProjectId2 } = await Promise.resolve().then(() => (init_supabase(), supabase_exports));
|
|
128
|
+
const defaultProjectId = await getDefaultProjectId2();
|
|
129
|
+
if (defaultProjectId) {
|
|
130
|
+
enrichedBody.projectId = defaultProjectId;
|
|
131
|
+
enrichedBody.project_id = defaultProjectId;
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
let url;
|
|
137
|
+
let method = options?.method ?? "POST";
|
|
138
|
+
let headers;
|
|
139
|
+
let requestBody;
|
|
140
|
+
if (!apiKey) {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
return {
|
|
143
|
+
data: null,
|
|
144
|
+
error: "Not authenticated. Run: npx @socialneuron/mcp-server login \u2014 Requires a paid plan (Starter+). See https://socialneuron.com/pricing"
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
url = new URL(`${supabaseUrl}/functions/v1/mcp-gateway`);
|
|
148
|
+
headers = {
|
|
149
|
+
Authorization: `Bearer ${apiKey}`,
|
|
150
|
+
"Content-Type": "application/json"
|
|
151
|
+
};
|
|
152
|
+
requestBody = {
|
|
153
|
+
functionName,
|
|
154
|
+
body: enrichedBody,
|
|
155
|
+
query: options?.query,
|
|
156
|
+
method: method.toUpperCase(),
|
|
157
|
+
timeoutMs
|
|
158
|
+
};
|
|
159
|
+
method = "POST";
|
|
160
|
+
try {
|
|
161
|
+
const response = await fetch(url.toString(), {
|
|
162
|
+
method,
|
|
163
|
+
headers,
|
|
164
|
+
body: method === "GET" ? void 0 : JSON.stringify(requestBody),
|
|
165
|
+
signal: controller.signal
|
|
166
|
+
});
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
const responseText = await response.text();
|
|
169
|
+
if (!response.ok) {
|
|
170
|
+
const errorCode = safeGatewayError(responseText, response.status);
|
|
171
|
+
const failureData = safeFailureData(responseText);
|
|
172
|
+
if (response.status === 401) {
|
|
173
|
+
return {
|
|
174
|
+
data: failureData,
|
|
175
|
+
error: `Authentication failed (HTTP 401). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (response.status === 403) {
|
|
179
|
+
return {
|
|
180
|
+
data: failureData,
|
|
181
|
+
error: `Forbidden (HTTP 403): ${errorCode}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (response.status === 429) {
|
|
185
|
+
const retryAfter = response.headers.get("retry-after") || "60";
|
|
186
|
+
return {
|
|
187
|
+
data: failureData,
|
|
188
|
+
error: `Rate limit exceeded (HTTP 429). Wait ${retryAfter}s before retrying. Reduce request frequency or upgrade your plan.`
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return { data: failureData, error: errorCode };
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const data = JSON.parse(responseText);
|
|
195
|
+
return { data, error: null };
|
|
196
|
+
} catch {
|
|
197
|
+
return { data: { text: responseText }, error: null };
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
clearTimeout(timer);
|
|
201
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
202
|
+
return {
|
|
203
|
+
data: null,
|
|
204
|
+
error: `Edge Function '${functionName}' timed out after ${timeoutMs}ms`
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return { data: null, error: "Network request failed. Please retry." };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
var SAFE_GATEWAY_ERROR_CODES, SAFE_BILLING_STATUSES, SAFE_JOB_STATUSES;
|
|
211
|
+
var init_edge_function = __esm({
|
|
212
|
+
"src/lib/edge-function.ts"() {
|
|
213
|
+
"use strict";
|
|
214
|
+
init_supabase();
|
|
215
|
+
init_request_context();
|
|
216
|
+
SAFE_GATEWAY_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
217
|
+
"daily_limit_reached",
|
|
218
|
+
"insufficient_credits",
|
|
219
|
+
"project_scope_mismatch",
|
|
220
|
+
"schedule_conflict",
|
|
221
|
+
"post_not_found",
|
|
222
|
+
"post_not_reschedulable",
|
|
223
|
+
"post_in_progress",
|
|
224
|
+
"not_cancellable",
|
|
225
|
+
"publishing_in_progress",
|
|
226
|
+
"plan_upgrade_required",
|
|
227
|
+
"rate_limited",
|
|
228
|
+
"validation_error",
|
|
229
|
+
"permission_denied",
|
|
230
|
+
"not_found"
|
|
231
|
+
]);
|
|
232
|
+
SAFE_BILLING_STATUSES = /* @__PURE__ */ new Set([
|
|
233
|
+
"reserved",
|
|
234
|
+
"charged",
|
|
235
|
+
"refunded",
|
|
236
|
+
"failed_no_charge",
|
|
237
|
+
"refund_pending",
|
|
238
|
+
"not_charged"
|
|
239
|
+
]);
|
|
240
|
+
SAFE_JOB_STATUSES = /* @__PURE__ */ new Set([
|
|
241
|
+
"queued",
|
|
242
|
+
"pending",
|
|
243
|
+
"processing",
|
|
244
|
+
"completed",
|
|
245
|
+
"failed",
|
|
246
|
+
"cancelled",
|
|
247
|
+
"canceled"
|
|
248
|
+
]);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
49
252
|
// src/cli/credentials.ts
|
|
50
253
|
var credentials_exports = {};
|
|
51
254
|
__export(credentials_exports, {
|
|
@@ -72,6 +275,10 @@ import {
|
|
|
72
275
|
} from "node:fs";
|
|
73
276
|
import { homedir, platform } from "node:os";
|
|
74
277
|
import { join } from "node:path";
|
|
278
|
+
function loadNativeKeyring() {
|
|
279
|
+
nativeKeyringModule ??= import("@napi-rs/keyring").catch(() => null);
|
|
280
|
+
return nativeKeyringModule;
|
|
281
|
+
}
|
|
75
282
|
function assertSafeCredentialPaths() {
|
|
76
283
|
if (platform() === "win32") return;
|
|
77
284
|
const uid = process.getuid?.();
|
|
@@ -191,49 +398,90 @@ function writeCredentialsFile(data) {
|
|
|
191
398
|
}
|
|
192
399
|
hardenCredentialPermissions();
|
|
193
400
|
}
|
|
194
|
-
function
|
|
401
|
+
function isMacKeychainItemMissing(error) {
|
|
402
|
+
const commandError = error;
|
|
403
|
+
if (commandError?.status === 44) return true;
|
|
404
|
+
const detail = `${commandError?.stderr ?? ""}
|
|
405
|
+
${commandError?.message ?? ""}`;
|
|
406
|
+
return /\berrsecitemnotfound\b/i.test(detail) || /(?:^|:\s*)the specified item could not be found in the keychain\.\s*$/i.test(detail.trim());
|
|
407
|
+
}
|
|
408
|
+
async function inspectMacKeychain(service) {
|
|
409
|
+
const native = await loadNativeKeyring();
|
|
410
|
+
if (native) {
|
|
411
|
+
try {
|
|
412
|
+
const value = new native.Entry(service, KEYCHAIN_ACCOUNT).getPassword();
|
|
413
|
+
if (value) return { status: "found", value };
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
416
|
+
}
|
|
195
417
|
try {
|
|
196
418
|
const result = execFileSync(
|
|
197
419
|
"security",
|
|
198
420
|
["find-generic-password", "-a", KEYCHAIN_ACCOUNT, "-s", service, "-w"],
|
|
199
421
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
200
422
|
);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
423
|
+
const value = result.trim();
|
|
424
|
+
return value ? { status: "found", value } : { status: "missing" };
|
|
425
|
+
} catch (error) {
|
|
426
|
+
return isMacKeychainItemMissing(error) ? { status: "missing" } : { status: "unavailable" };
|
|
204
427
|
}
|
|
205
428
|
}
|
|
206
|
-
function
|
|
429
|
+
async function macKeychainRead(service) {
|
|
430
|
+
const result = await inspectMacKeychain(service);
|
|
431
|
+
return result.status === "found" ? result.value : null;
|
|
432
|
+
}
|
|
433
|
+
async function macKeychainWrite(service, value) {
|
|
434
|
+
const native = await loadNativeKeyring();
|
|
435
|
+
if (!native) return false;
|
|
207
436
|
try {
|
|
208
|
-
|
|
209
|
-
"security",
|
|
210
|
-
[
|
|
211
|
-
"add-generic-password",
|
|
212
|
-
"-a",
|
|
213
|
-
KEYCHAIN_ACCOUNT,
|
|
214
|
-
"-s",
|
|
215
|
-
service,
|
|
216
|
-
"-w",
|
|
217
|
-
value,
|
|
218
|
-
"-U"
|
|
219
|
-
// update if exists
|
|
220
|
-
],
|
|
221
|
-
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
222
|
-
);
|
|
437
|
+
new native.Entry(service, KEYCHAIN_ACCOUNT).setPassword(value);
|
|
223
438
|
return true;
|
|
224
439
|
} catch {
|
|
225
440
|
return false;
|
|
226
441
|
}
|
|
227
442
|
}
|
|
228
|
-
function macKeychainDelete(service) {
|
|
443
|
+
async function macKeychainDelete(service) {
|
|
444
|
+
const native = await loadNativeKeyring();
|
|
445
|
+
let nativeDeleted = false;
|
|
446
|
+
if (native) {
|
|
447
|
+
try {
|
|
448
|
+
nativeDeleted = new native.Entry(service, KEYCHAIN_ACCOUNT).deletePassword();
|
|
449
|
+
} catch {
|
|
450
|
+
}
|
|
451
|
+
}
|
|
229
452
|
try {
|
|
230
453
|
execFileSync("security", ["delete-generic-password", "-a", KEYCHAIN_ACCOUNT, "-s", service], {
|
|
231
454
|
stdio: ["pipe", "pipe", "pipe"]
|
|
232
455
|
});
|
|
233
|
-
return
|
|
234
|
-
} catch {
|
|
235
|
-
return
|
|
456
|
+
return "deleted";
|
|
457
|
+
} catch (error) {
|
|
458
|
+
if (isMacKeychainItemMissing(error)) return nativeDeleted ? "deleted" : "missing";
|
|
459
|
+
return "unavailable";
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async function clearMacKeychain(service) {
|
|
463
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
464
|
+
const deleted = await macKeychainDelete(service);
|
|
465
|
+
if (deleted === "unavailable") break;
|
|
466
|
+
const remaining = await inspectMacKeychain(service);
|
|
467
|
+
if (remaining.status === "missing") return;
|
|
468
|
+
if (remaining.status === "unavailable") break;
|
|
236
469
|
}
|
|
470
|
+
throw new Error(
|
|
471
|
+
"Unable to verify removal of the existing Social Neuron Keychain credential. Unlock Keychain Access, remove the item, and retry."
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
async function verifyMacFileFallback(service) {
|
|
475
|
+
const existing = await inspectMacKeychain(service);
|
|
476
|
+
if (existing.status === "missing") return;
|
|
477
|
+
if (existing.status === "found") {
|
|
478
|
+
throw new Error(
|
|
479
|
+
"An existing Social Neuron Keychain credential could not be replaced. The existing value was retained; unlock or remove it in Keychain Access and retry."
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
throw new Error(
|
|
483
|
+
"Unable to verify absence of an existing Social Neuron Keychain credential. Unlock Keychain Access and retry."
|
|
484
|
+
);
|
|
237
485
|
}
|
|
238
486
|
function linuxSecretRead(key) {
|
|
239
487
|
try {
|
|
@@ -274,7 +522,7 @@ async function loadApiKey() {
|
|
|
274
522
|
if (envKey) return envKey;
|
|
275
523
|
const os = platform();
|
|
276
524
|
if (os === "darwin") {
|
|
277
|
-
const key = macKeychainRead(KEYCHAIN_SERVICE_API);
|
|
525
|
+
const key = await macKeychainRead(KEYCHAIN_SERVICE_API);
|
|
278
526
|
if (key) return key;
|
|
279
527
|
} else if (os === "linux") {
|
|
280
528
|
const key = linuxSecretRead("api-key");
|
|
@@ -291,13 +539,18 @@ async function loadApiKey() {
|
|
|
291
539
|
async function saveApiKey(key) {
|
|
292
540
|
const os = platform();
|
|
293
541
|
let saved = false;
|
|
542
|
+
let fallbackCredentials;
|
|
294
543
|
if (os === "darwin") {
|
|
295
|
-
saved = macKeychainWrite(KEYCHAIN_SERVICE_API, key);
|
|
544
|
+
saved = await macKeychainWrite(KEYCHAIN_SERVICE_API, key);
|
|
545
|
+
if (!saved) {
|
|
546
|
+
fallbackCredentials = readCredentialsFile();
|
|
547
|
+
await verifyMacFileFallback(KEYCHAIN_SERVICE_API);
|
|
548
|
+
}
|
|
296
549
|
} else if (os === "linux") {
|
|
297
550
|
saved = linuxSecretWrite("api-key", key);
|
|
298
551
|
}
|
|
299
552
|
if (!saved) {
|
|
300
|
-
const creds = readCredentialsFile();
|
|
553
|
+
const creds = fallbackCredentials ?? readCredentialsFile();
|
|
301
554
|
creds.apiKey = key;
|
|
302
555
|
writeCredentialsFile(creds);
|
|
303
556
|
if (os === "win32") {
|
|
@@ -320,8 +573,13 @@ async function saveApiKey(key) {
|
|
|
320
573
|
}
|
|
321
574
|
async function deleteApiKey() {
|
|
322
575
|
const os = platform();
|
|
576
|
+
let keychainError;
|
|
323
577
|
if (os === "darwin") {
|
|
324
|
-
|
|
578
|
+
try {
|
|
579
|
+
await clearMacKeychain(KEYCHAIN_SERVICE_API);
|
|
580
|
+
} catch (error) {
|
|
581
|
+
keychainError = error;
|
|
582
|
+
}
|
|
325
583
|
} else if (os === "linux") {
|
|
326
584
|
linuxSecretDelete("api-key");
|
|
327
585
|
}
|
|
@@ -337,13 +595,14 @@ async function deleteApiKey() {
|
|
|
337
595
|
writeCredentialsFile(creds);
|
|
338
596
|
}
|
|
339
597
|
}
|
|
598
|
+
if (keychainError) throw keychainError;
|
|
340
599
|
}
|
|
341
600
|
async function loadSupabaseUrl() {
|
|
342
601
|
const envUrl = process.env.SOCIALNEURON_SUPABASE_URL || process.env.SUPABASE_URL;
|
|
343
602
|
if (envUrl) return envUrl;
|
|
344
603
|
const os = platform();
|
|
345
604
|
if (os === "darwin") {
|
|
346
|
-
const url = macKeychainRead(KEYCHAIN_SERVICE_URL);
|
|
605
|
+
const url = await macKeychainRead(KEYCHAIN_SERVICE_URL);
|
|
347
606
|
if (url) return url;
|
|
348
607
|
} else if (os === "linux") {
|
|
349
608
|
const url = linuxSecretRead("supabase-url");
|
|
@@ -355,18 +614,23 @@ async function loadSupabaseUrl() {
|
|
|
355
614
|
async function saveSupabaseUrl(url) {
|
|
356
615
|
const os = platform();
|
|
357
616
|
let saved = false;
|
|
617
|
+
let fallbackCredentials;
|
|
358
618
|
if (os === "darwin") {
|
|
359
|
-
saved = macKeychainWrite(KEYCHAIN_SERVICE_URL, url);
|
|
619
|
+
saved = await macKeychainWrite(KEYCHAIN_SERVICE_URL, url);
|
|
620
|
+
if (!saved) {
|
|
621
|
+
fallbackCredentials = readCredentialsFile();
|
|
622
|
+
await verifyMacFileFallback(KEYCHAIN_SERVICE_URL);
|
|
623
|
+
}
|
|
360
624
|
} else if (os === "linux") {
|
|
361
625
|
saved = linuxSecretWrite("supabase-url", url);
|
|
362
626
|
}
|
|
363
627
|
if (!saved) {
|
|
364
|
-
const creds = readCredentialsFile();
|
|
628
|
+
const creds = fallbackCredentials ?? readCredentialsFile();
|
|
365
629
|
creds.supabaseUrl = url;
|
|
366
630
|
writeCredentialsFile(creds);
|
|
367
631
|
}
|
|
368
632
|
}
|
|
369
|
-
var KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE_API, KEYCHAIN_SERVICE_URL, CONFIG_DIR, CREDENTIALS_FILE;
|
|
633
|
+
var KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE_API, KEYCHAIN_SERVICE_URL, CONFIG_DIR, CREDENTIALS_FILE, nativeKeyringModule;
|
|
370
634
|
var init_credentials = __esm({
|
|
371
635
|
"src/cli/credentials.ts"() {
|
|
372
636
|
"use strict";
|
|
@@ -519,7 +783,10 @@ __export(supabase_exports, {
|
|
|
519
783
|
getSupabaseUrl: () => getSupabaseUrl,
|
|
520
784
|
initializeAuth: () => initializeAuth,
|
|
521
785
|
isTelemetryDisabled: () => isTelemetryDisabled,
|
|
522
|
-
|
|
786
|
+
listAccessibleProjectsWithAccountStatus: () => listAccessibleProjectsWithAccountStatus,
|
|
787
|
+
logMcpToolInvocation: () => logMcpToolInvocation,
|
|
788
|
+
resolveProjectForConnectedAccountTool: () => resolveProjectForConnectedAccountTool,
|
|
789
|
+
resolveProjectStrict: () => resolveProjectStrict
|
|
523
790
|
});
|
|
524
791
|
import { createClient } from "@supabase/supabase-js";
|
|
525
792
|
import { randomUUID } from "node:crypto";
|
|
@@ -558,20 +825,17 @@ async function getDefaultUserId() {
|
|
|
558
825
|
if (authenticatedUserId) return authenticatedUserId;
|
|
559
826
|
const envUserId = process.env.SOCIALNEURON_USER_ID;
|
|
560
827
|
if (envUserId) return envUserId;
|
|
561
|
-
throw new Error(
|
|
828
|
+
throw new Error(
|
|
829
|
+
"No user ID available. Set SOCIALNEURON_USER_ID or authenticate via API key."
|
|
830
|
+
);
|
|
562
831
|
}
|
|
563
832
|
async function getDefaultProjectId() {
|
|
564
833
|
const requestProjectId = getRequestProjectId();
|
|
565
834
|
if (requestProjectId) return requestProjectId;
|
|
566
835
|
if (authenticatedProjectId) return authenticatedProjectId;
|
|
567
836
|
const userId = await getDefaultUserId().catch(() => null);
|
|
568
|
-
if (userId) {
|
|
569
|
-
const cached3 = projectIdCache.get(userId);
|
|
570
|
-
if (cached3) return cached3;
|
|
571
|
-
}
|
|
572
837
|
const envProjectId = process.env.SOCIALNEURON_PROJECT_ID;
|
|
573
838
|
if (envProjectId) {
|
|
574
|
-
if (userId) projectIdCache.set(userId, envProjectId);
|
|
575
839
|
return envProjectId;
|
|
576
840
|
}
|
|
577
841
|
if (!userId) return null;
|
|
@@ -580,15 +844,126 @@ async function getDefaultProjectId() {
|
|
|
580
844
|
const { data: memberships } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId);
|
|
581
845
|
const orgIds = (memberships ?? []).map((m) => m.organization_id);
|
|
582
846
|
if (orgIds.length === 0) return null;
|
|
583
|
-
const { data } = await supabase.from("projects").select("id").in("organization_id", orgIds).order("created_at", { ascending: false }).limit(
|
|
584
|
-
if (data?.
|
|
585
|
-
projectIdCache.set(userId, data.id);
|
|
586
|
-
return data.id;
|
|
587
|
-
}
|
|
847
|
+
const { data } = await supabase.from("projects").select("id").in("organization_id", orgIds).order("created_at", { ascending: false }).limit(2);
|
|
848
|
+
if (data?.length === 1) return data[0].id;
|
|
588
849
|
} catch {
|
|
589
850
|
}
|
|
590
851
|
return null;
|
|
591
852
|
}
|
|
853
|
+
function normalizePlatformFilter(platform2) {
|
|
854
|
+
if (!platform2) return null;
|
|
855
|
+
const values = Array.isArray(platform2) ? platform2 : [platform2];
|
|
856
|
+
const set = new Set(values.filter(Boolean).map((p) => p.toLowerCase()));
|
|
857
|
+
return set.size > 0 ? set : null;
|
|
858
|
+
}
|
|
859
|
+
async function listAccessibleProjectsWithAccountStatusDirect(userId, platform2) {
|
|
860
|
+
try {
|
|
861
|
+
const supabase = getSupabaseClient();
|
|
862
|
+
const { data: memberships } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId);
|
|
863
|
+
const orgIds = (memberships ?? []).map((m) => m.organization_id);
|
|
864
|
+
if (orgIds.length === 0) return [];
|
|
865
|
+
const { data: projects } = await supabase.from("projects").select("id, name").in("organization_id", orgIds).order("created_at", { ascending: false });
|
|
866
|
+
const projectRows = projects ?? [];
|
|
867
|
+
if (projectRows.length === 0) return [];
|
|
868
|
+
const projectIds = projectRows.map((p) => p.id);
|
|
869
|
+
const { data: accounts } = await supabase.from("connected_accounts").select("project_id, status, platform").eq("user_id", userId).in("project_id", projectIds);
|
|
870
|
+
const anyAccountByProject = /* @__PURE__ */ new Set();
|
|
871
|
+
const platformsByProject = /* @__PURE__ */ new Map();
|
|
872
|
+
for (const row of accounts ?? []) {
|
|
873
|
+
if (row.status !== "active" && row.status !== "expires_soon") continue;
|
|
874
|
+
if (!row.project_id) continue;
|
|
875
|
+
anyAccountByProject.add(row.project_id);
|
|
876
|
+
if (row.platform) {
|
|
877
|
+
const set = platformsByProject.get(row.project_id) ?? /* @__PURE__ */ new Set();
|
|
878
|
+
set.add(row.platform.toLowerCase());
|
|
879
|
+
platformsByProject.set(row.project_id, set);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
const requestedPlatforms = normalizePlatformFilter(platform2);
|
|
883
|
+
return projectRows.map((p) => {
|
|
884
|
+
const platforms = Array.from(platformsByProject.get(p.id) ?? []);
|
|
885
|
+
const hasConnectedAccounts = requestedPlatforms ? platforms.some((pl) => requestedPlatforms.has(pl)) : anyAccountByProject.has(p.id);
|
|
886
|
+
return { id: p.id, name: p.name, hasConnectedAccounts, platforms };
|
|
887
|
+
});
|
|
888
|
+
} catch {
|
|
889
|
+
return [];
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
async function listAccessibleProjectsWithAccountStatusViaEdgeFunction(userId, platform2) {
|
|
893
|
+
try {
|
|
894
|
+
const { callEdgeFunction: callEdgeFunction2 } = await Promise.resolve().then(() => (init_edge_function(), edge_function_exports));
|
|
895
|
+
const { data, error } = await callEdgeFunction2(
|
|
896
|
+
"mcp-data",
|
|
897
|
+
{ action: "projects", userId, user_id: userId },
|
|
898
|
+
{ timeoutMs: 1e4 }
|
|
899
|
+
);
|
|
900
|
+
if (error || !data?.success || !Array.isArray(data.projects)) return [];
|
|
901
|
+
const requestedPlatforms = normalizePlatformFilter(platform2);
|
|
902
|
+
return data.projects.map((p) => {
|
|
903
|
+
const platforms = (p.platforms ?? []).map((pl) => pl.toLowerCase());
|
|
904
|
+
const hasConnectedAccounts = requestedPlatforms ? platforms.some((pl) => requestedPlatforms.has(pl)) : Boolean(p.hasConnectedAccounts);
|
|
905
|
+
return { id: p.id, name: p.name, hasConnectedAccounts, platforms };
|
|
906
|
+
});
|
|
907
|
+
} catch {
|
|
908
|
+
return [];
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
async function listAccessibleProjectsWithAccountStatus(userId, platform2) {
|
|
912
|
+
if (getServiceKeyOrNull()) {
|
|
913
|
+
return listAccessibleProjectsWithAccountStatusDirect(userId, platform2);
|
|
914
|
+
}
|
|
915
|
+
return listAccessibleProjectsWithAccountStatusViaEdgeFunction(
|
|
916
|
+
userId,
|
|
917
|
+
platform2
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
async function resolveProjectForConnectedAccountTool(explicitProjectId, platform2) {
|
|
921
|
+
if (explicitProjectId) return { projectId: explicitProjectId };
|
|
922
|
+
const defaultProjectId = await getDefaultProjectId();
|
|
923
|
+
if (defaultProjectId) return { projectId: defaultProjectId };
|
|
924
|
+
const genericError = "project_id is required. Configure an explicit project or use an API key scoped to exactly one project.";
|
|
925
|
+
const userId = await getDefaultUserId().catch(() => null);
|
|
926
|
+
if (!userId) return { error: genericError };
|
|
927
|
+
const projects = await listAccessibleProjectsWithAccountStatus(
|
|
928
|
+
userId,
|
|
929
|
+
platform2
|
|
930
|
+
);
|
|
931
|
+
if (projects.length === 0) return { error: genericError };
|
|
932
|
+
const withAccounts = projects.filter((p) => p.hasConnectedAccounts);
|
|
933
|
+
if (withAccounts.length === 1) {
|
|
934
|
+
const chosen = withAccounts[0];
|
|
935
|
+
const platformNote = platform2 ? ` for ${Array.isArray(platform2) ? platform2.join("/") : platform2}` : "";
|
|
936
|
+
return {
|
|
937
|
+
projectId: chosen.id,
|
|
938
|
+
autoResolvedNote: `project_id was not provided; auto-resolved to "${chosen.name}" (${chosen.id}) \u2014 the only one of your ${projects.length} project(s) with an active connected account${platformNote}.`,
|
|
939
|
+
projects
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
const projectList = projects.map(
|
|
943
|
+
(p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
|
|
944
|
+
).join("; ");
|
|
945
|
+
return {
|
|
946
|
+
error: `project_id is required \u2014 your account has ${projects.length} projects and the target could not be auto-resolved. Pass the exact project_id from this list: ${projectList}.`,
|
|
947
|
+
projects
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
async function resolveProjectStrict(explicitProjectId) {
|
|
951
|
+
if (explicitProjectId) return { projectId: explicitProjectId };
|
|
952
|
+
const defaultProjectId = await getDefaultProjectId();
|
|
953
|
+
if (defaultProjectId) return { projectId: defaultProjectId };
|
|
954
|
+
const genericError = "project_id is required. Configure an explicit project or use an API key scoped to exactly one project.";
|
|
955
|
+
const userId = await getDefaultUserId().catch(() => null);
|
|
956
|
+
if (!userId) return { error: genericError };
|
|
957
|
+
const projects = await listAccessibleProjectsWithAccountStatus(userId);
|
|
958
|
+
if (projects.length === 0) return { error: genericError };
|
|
959
|
+
const projectList = projects.map(
|
|
960
|
+
(p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
|
|
961
|
+
).join("; ");
|
|
962
|
+
return {
|
|
963
|
+
error: `project_id is required \u2014 your account has ${projects.length} projects. Pass the exact project_id from this list: ${projectList}.`,
|
|
964
|
+
projects
|
|
965
|
+
};
|
|
966
|
+
}
|
|
592
967
|
async function initializeAuth() {
|
|
593
968
|
const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
594
969
|
const apiKey = await loadApiKey2();
|
|
@@ -614,8 +989,11 @@ async function initializeAuth() {
|
|
|
614
989
|
}
|
|
615
990
|
if (authenticatedExpiresAt) {
|
|
616
991
|
const expiresMs = new Date(authenticatedExpiresAt).getTime();
|
|
617
|
-
const daysLeft = Math.ceil(
|
|
618
|
-
|
|
992
|
+
const daysLeft = Math.ceil(
|
|
993
|
+
(expiresMs - Date.now()) / (1e3 * 60 * 60 * 24)
|
|
994
|
+
);
|
|
995
|
+
if (!_quietAuth)
|
|
996
|
+
console.error("[MCP] Key expires: " + authenticatedExpiresAt);
|
|
619
997
|
if (daysLeft <= 7) {
|
|
620
998
|
console.error(
|
|
621
999
|
`[MCP] Warning: API key expires in ${daysLeft} day(s). Run: npx @socialneuron/mcp-server login`
|
|
@@ -694,7 +1072,7 @@ async function logMcpToolInvocation(args) {
|
|
|
694
1072
|
Promise.resolve().then(() => (init_posthog(), posthog_exports)).then(({ captureToolEvent: captureToolEvent2 }) => captureToolEvent2(args)).catch(() => {
|
|
695
1073
|
});
|
|
696
1074
|
}
|
|
697
|
-
var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, authenticatedProjectId, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY
|
|
1075
|
+
var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, authenticatedProjectId, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY;
|
|
698
1076
|
var init_supabase = __esm({
|
|
699
1077
|
"src/lib/supabase.ts"() {
|
|
700
1078
|
"use strict";
|
|
@@ -712,7 +1090,6 @@ var init_supabase = __esm({
|
|
|
712
1090
|
MCP_RUN_ID = randomUUID();
|
|
713
1091
|
CLOUD_SUPABASE_URL = "https://rhukkjscgzauutioyeei.supabase.co";
|
|
714
1092
|
CLOUD_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJodWtranNjZ3phdXV0aW95ZWVpIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ4NjM4ODYsImV4cCI6MjA4MDQzOTg4Nn0.JVtrviGvN0HaSh0JFS5KNl5FAB5ffG5Y1IMZsQFUrNQ";
|
|
715
|
-
projectIdCache = /* @__PURE__ */ new Map();
|
|
716
1093
|
}
|
|
717
1094
|
});
|
|
718
1095
|
|
|
@@ -2013,201 +2390,9 @@ function scan(text, options) {
|
|
|
2013
2390
|
}
|
|
2014
2391
|
|
|
2015
2392
|
// src/tools/ideation.ts
|
|
2393
|
+
init_edge_function();
|
|
2016
2394
|
import { z } from "zod";
|
|
2017
2395
|
|
|
2018
|
-
// src/lib/edge-function.ts
|
|
2019
|
-
init_supabase();
|
|
2020
|
-
init_request_context();
|
|
2021
|
-
var SAFE_GATEWAY_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2022
|
-
"daily_limit_reached",
|
|
2023
|
-
"insufficient_credits",
|
|
2024
|
-
"project_scope_mismatch",
|
|
2025
|
-
"schedule_conflict",
|
|
2026
|
-
"post_not_found",
|
|
2027
|
-
"post_not_reschedulable",
|
|
2028
|
-
"post_in_progress",
|
|
2029
|
-
"not_cancellable",
|
|
2030
|
-
"publishing_in_progress",
|
|
2031
|
-
"plan_upgrade_required",
|
|
2032
|
-
"rate_limited",
|
|
2033
|
-
"validation_error",
|
|
2034
|
-
"permission_denied",
|
|
2035
|
-
"not_found"
|
|
2036
|
-
]);
|
|
2037
|
-
function safeGatewayError(responseText, status) {
|
|
2038
|
-
try {
|
|
2039
|
-
const parsed = JSON.parse(responseText);
|
|
2040
|
-
const nested = parsed.error && typeof parsed.error === "object" ? parsed.error : null;
|
|
2041
|
-
const candidates = [
|
|
2042
|
-
nested?.error_type,
|
|
2043
|
-
nested?.code,
|
|
2044
|
-
parsed.error_type,
|
|
2045
|
-
parsed.error_code,
|
|
2046
|
-
parsed.code,
|
|
2047
|
-
typeof parsed.error === "string" ? parsed.error : null
|
|
2048
|
-
];
|
|
2049
|
-
for (const value of candidates) {
|
|
2050
|
-
if (typeof value !== "string") continue;
|
|
2051
|
-
const normalized = value.trim().toLowerCase();
|
|
2052
|
-
if (SAFE_GATEWAY_ERROR_CODES.has(normalized)) return normalized;
|
|
2053
|
-
const embedded = [...SAFE_GATEWAY_ERROR_CODES].find((code) => normalized.includes(code));
|
|
2054
|
-
if (embedded) return embedded;
|
|
2055
|
-
}
|
|
2056
|
-
} catch {
|
|
2057
|
-
}
|
|
2058
|
-
return `Backend request failed (HTTP ${status}).`;
|
|
2059
|
-
}
|
|
2060
|
-
var SAFE_BILLING_STATUSES = /* @__PURE__ */ new Set([
|
|
2061
|
-
"reserved",
|
|
2062
|
-
"charged",
|
|
2063
|
-
"refunded",
|
|
2064
|
-
"failed_no_charge",
|
|
2065
|
-
"refund_pending",
|
|
2066
|
-
"not_charged"
|
|
2067
|
-
]);
|
|
2068
|
-
var SAFE_JOB_STATUSES = /* @__PURE__ */ new Set([
|
|
2069
|
-
"queued",
|
|
2070
|
-
"pending",
|
|
2071
|
-
"processing",
|
|
2072
|
-
"completed",
|
|
2073
|
-
"failed",
|
|
2074
|
-
"cancelled",
|
|
2075
|
-
"canceled"
|
|
2076
|
-
]);
|
|
2077
|
-
function safeFailureData(responseText) {
|
|
2078
|
-
try {
|
|
2079
|
-
const parsed = JSON.parse(responseText);
|
|
2080
|
-
const metric = (name) => {
|
|
2081
|
-
const value = parsed[name];
|
|
2082
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
2083
|
-
};
|
|
2084
|
-
const billingStatus = typeof parsed.billing_status === "string" && SAFE_BILLING_STATUSES.has(parsed.billing_status) ? parsed.billing_status : void 0;
|
|
2085
|
-
const failureReason = parsed.failure_reason === "generation_failed" || parsed.failure_reason === "authentication_failed" || parsed.failure_reason === "cancelled_by_user" ? parsed.failure_reason : void 0;
|
|
2086
|
-
const jobStatus = typeof parsed.status === "string" && SAFE_JOB_STATUSES.has(parsed.status) ? parsed.status : void 0;
|
|
2087
|
-
const safe = {
|
|
2088
|
-
...jobStatus ? { status: jobStatus } : {},
|
|
2089
|
-
...metric("credits_reserved") !== void 0 ? { credits_reserved: metric("credits_reserved") } : {},
|
|
2090
|
-
...metric("credits_charged") !== void 0 ? { credits_charged: metric("credits_charged") } : {},
|
|
2091
|
-
...metric("credits_refunded") !== void 0 ? { credits_refunded: metric("credits_refunded") } : {},
|
|
2092
|
-
...billingStatus ? { billing_status: billingStatus } : {},
|
|
2093
|
-
...failureReason ? { failure_reason: failureReason } : {}
|
|
2094
|
-
};
|
|
2095
|
-
return Object.keys(safe).length > 0 ? safe : null;
|
|
2096
|
-
} catch {
|
|
2097
|
-
return null;
|
|
2098
|
-
}
|
|
2099
|
-
}
|
|
2100
|
-
function getApiKeyOrNull() {
|
|
2101
|
-
const envKey = process.env.SOCIALNEURON_API_KEY;
|
|
2102
|
-
if (envKey && envKey.trim().length) return envKey.trim();
|
|
2103
|
-
const requestToken = getRequestToken();
|
|
2104
|
-
if (requestToken) return requestToken;
|
|
2105
|
-
return getAuthenticatedApiKey();
|
|
2106
|
-
}
|
|
2107
|
-
async function callEdgeFunction(functionName, body, options) {
|
|
2108
|
-
const supabaseUrl = getSupabaseUrl();
|
|
2109
|
-
const apiKey = getApiKeyOrNull();
|
|
2110
|
-
const controller = new AbortController();
|
|
2111
|
-
const timeoutMs = options?.timeoutMs ?? 6e4;
|
|
2112
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2113
|
-
const enrichedBody = { ...body };
|
|
2114
|
-
if (!enrichedBody.userId && !enrichedBody.user_id) {
|
|
2115
|
-
try {
|
|
2116
|
-
const defaultId = await getDefaultUserId();
|
|
2117
|
-
enrichedBody.userId = defaultId;
|
|
2118
|
-
enrichedBody.user_id = defaultId;
|
|
2119
|
-
} catch {
|
|
2120
|
-
}
|
|
2121
|
-
} else {
|
|
2122
|
-
if (enrichedBody.userId && !enrichedBody.user_id) enrichedBody.user_id = enrichedBody.userId;
|
|
2123
|
-
if (enrichedBody.user_id && !enrichedBody.userId) enrichedBody.userId = enrichedBody.user_id;
|
|
2124
|
-
}
|
|
2125
|
-
if (!enrichedBody.projectId && !enrichedBody.project_id) {
|
|
2126
|
-
try {
|
|
2127
|
-
const { getDefaultProjectId: getDefaultProjectId2 } = await Promise.resolve().then(() => (init_supabase(), supabase_exports));
|
|
2128
|
-
const defaultProjectId = await getDefaultProjectId2();
|
|
2129
|
-
if (defaultProjectId) {
|
|
2130
|
-
enrichedBody.projectId = defaultProjectId;
|
|
2131
|
-
enrichedBody.project_id = defaultProjectId;
|
|
2132
|
-
}
|
|
2133
|
-
} catch {
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
let url;
|
|
2137
|
-
let method = options?.method ?? "POST";
|
|
2138
|
-
let headers;
|
|
2139
|
-
let requestBody;
|
|
2140
|
-
if (!apiKey) {
|
|
2141
|
-
clearTimeout(timer);
|
|
2142
|
-
return {
|
|
2143
|
-
data: null,
|
|
2144
|
-
error: "Not authenticated. Run: npx @socialneuron/mcp-server login \u2014 Requires a paid plan (Starter+). See https://socialneuron.com/pricing"
|
|
2145
|
-
};
|
|
2146
|
-
}
|
|
2147
|
-
url = new URL(`${supabaseUrl}/functions/v1/mcp-gateway`);
|
|
2148
|
-
headers = {
|
|
2149
|
-
Authorization: `Bearer ${apiKey}`,
|
|
2150
|
-
"Content-Type": "application/json"
|
|
2151
|
-
};
|
|
2152
|
-
requestBody = {
|
|
2153
|
-
functionName,
|
|
2154
|
-
body: enrichedBody,
|
|
2155
|
-
query: options?.query,
|
|
2156
|
-
method: method.toUpperCase(),
|
|
2157
|
-
timeoutMs
|
|
2158
|
-
};
|
|
2159
|
-
method = "POST";
|
|
2160
|
-
try {
|
|
2161
|
-
const response = await fetch(url.toString(), {
|
|
2162
|
-
method,
|
|
2163
|
-
headers,
|
|
2164
|
-
body: method === "GET" ? void 0 : JSON.stringify(requestBody),
|
|
2165
|
-
signal: controller.signal
|
|
2166
|
-
});
|
|
2167
|
-
clearTimeout(timer);
|
|
2168
|
-
const responseText = await response.text();
|
|
2169
|
-
if (!response.ok) {
|
|
2170
|
-
const errorCode = safeGatewayError(responseText, response.status);
|
|
2171
|
-
const failureData = safeFailureData(responseText);
|
|
2172
|
-
if (response.status === 401) {
|
|
2173
|
-
return {
|
|
2174
|
-
data: failureData,
|
|
2175
|
-
error: `Authentication failed (HTTP 401). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
|
|
2176
|
-
};
|
|
2177
|
-
}
|
|
2178
|
-
if (response.status === 403) {
|
|
2179
|
-
return {
|
|
2180
|
-
data: failureData,
|
|
2181
|
-
error: `Forbidden (HTTP 403): ${errorCode}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
|
|
2182
|
-
};
|
|
2183
|
-
}
|
|
2184
|
-
if (response.status === 429) {
|
|
2185
|
-
const retryAfter = response.headers.get("retry-after") || "60";
|
|
2186
|
-
return {
|
|
2187
|
-
data: failureData,
|
|
2188
|
-
error: `Rate limit exceeded (HTTP 429). Wait ${retryAfter}s before retrying. Reduce request frequency or upgrade your plan.`
|
|
2189
|
-
};
|
|
2190
|
-
}
|
|
2191
|
-
return { data: failureData, error: errorCode };
|
|
2192
|
-
}
|
|
2193
|
-
try {
|
|
2194
|
-
const data = JSON.parse(responseText);
|
|
2195
|
-
return { data, error: null };
|
|
2196
|
-
} catch {
|
|
2197
|
-
return { data: { text: responseText }, error: null };
|
|
2198
|
-
}
|
|
2199
|
-
} catch (err) {
|
|
2200
|
-
clearTimeout(timer);
|
|
2201
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
2202
|
-
return {
|
|
2203
|
-
data: null,
|
|
2204
|
-
error: `Edge Function '${functionName}' timed out after ${timeoutMs}ms`
|
|
2205
|
-
};
|
|
2206
|
-
}
|
|
2207
|
-
return { data: null, error: "Network request failed. Please retry." };
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
2396
|
// src/lib/rate-limit.ts
|
|
2212
2397
|
var CATEGORY_CONFIGS = {
|
|
2213
2398
|
posting: { maxTokens: 30, refillRate: 30 / 60 },
|
|
@@ -2324,7 +2509,7 @@ function checkRateLimit(category, key) {
|
|
|
2324
2509
|
init_supabase();
|
|
2325
2510
|
|
|
2326
2511
|
// src/lib/version.ts
|
|
2327
|
-
var MCP_VERSION = "1.
|
|
2512
|
+
var MCP_VERSION = "1.9.1";
|
|
2328
2513
|
|
|
2329
2514
|
// src/tools/ideation.ts
|
|
2330
2515
|
function asEnvelope(data) {
|
|
@@ -2725,9 +2910,90 @@ ${content}`,
|
|
|
2725
2910
|
}
|
|
2726
2911
|
|
|
2727
2912
|
// src/tools/content.ts
|
|
2913
|
+
init_edge_function();
|
|
2728
2914
|
import { z as z2 } from "zod";
|
|
2729
2915
|
init_supabase();
|
|
2730
2916
|
|
|
2917
|
+
// src/lib/sanitize-error.ts
|
|
2918
|
+
var ERROR_PATTERNS = [
|
|
2919
|
+
// Postgres / PostgREST
|
|
2920
|
+
[
|
|
2921
|
+
/PGRST301|permission denied/i,
|
|
2922
|
+
"Access denied. Check your account permissions."
|
|
2923
|
+
],
|
|
2924
|
+
[
|
|
2925
|
+
/42P01|does not exist/i,
|
|
2926
|
+
"Service temporarily unavailable. Please try again."
|
|
2927
|
+
],
|
|
2928
|
+
[
|
|
2929
|
+
/23505|unique.*constraint|duplicate key/i,
|
|
2930
|
+
"A duplicate record already exists."
|
|
2931
|
+
],
|
|
2932
|
+
[/23503|foreign key/i, "Referenced record not found."],
|
|
2933
|
+
// Gemini / Google AI
|
|
2934
|
+
[
|
|
2935
|
+
/google.*api.*key|googleapis\.com.*40[13]/i,
|
|
2936
|
+
"Content generation failed. Please try again."
|
|
2937
|
+
],
|
|
2938
|
+
[
|
|
2939
|
+
/RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
|
|
2940
|
+
"AI service rate limit reached. Please wait and retry."
|
|
2941
|
+
],
|
|
2942
|
+
[
|
|
2943
|
+
/SAFETY|prompt.*blocked|content.*filter/i,
|
|
2944
|
+
"Content was blocked by the AI safety filter. Try rephrasing."
|
|
2945
|
+
],
|
|
2946
|
+
[
|
|
2947
|
+
/gemini.*error|generativelanguage/i,
|
|
2948
|
+
"Content generation failed. Please try again."
|
|
2949
|
+
],
|
|
2950
|
+
// Kie.ai
|
|
2951
|
+
[/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
|
|
2952
|
+
// Stripe
|
|
2953
|
+
[
|
|
2954
|
+
/stripe.*api|sk_live_|sk_test_/i,
|
|
2955
|
+
"Payment processing error. Please try again."
|
|
2956
|
+
],
|
|
2957
|
+
// Network / fetch
|
|
2958
|
+
[
|
|
2959
|
+
/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
|
|
2960
|
+
"External service unavailable. Please try again."
|
|
2961
|
+
],
|
|
2962
|
+
[
|
|
2963
|
+
/fetch failed|network error|abort.*timeout/i,
|
|
2964
|
+
"Network request failed. Please try again."
|
|
2965
|
+
],
|
|
2966
|
+
[/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
|
|
2967
|
+
// Supabase Edge Function internals
|
|
2968
|
+
[
|
|
2969
|
+
/FunctionsHttpError|non-2xx status/i,
|
|
2970
|
+
"Backend service error. Please try again."
|
|
2971
|
+
],
|
|
2972
|
+
[
|
|
2973
|
+
/JWT|token.*expired|token.*invalid/i,
|
|
2974
|
+
"Authentication expired. Please re-authenticate."
|
|
2975
|
+
],
|
|
2976
|
+
// Generic sensitive patterns (API keys, URLs with secrets)
|
|
2977
|
+
[
|
|
2978
|
+
/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i,
|
|
2979
|
+
"An internal error occurred. Please try again."
|
|
2980
|
+
]
|
|
2981
|
+
];
|
|
2982
|
+
var UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
|
|
2983
|
+
var EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
2984
|
+
function redactSensitiveIdentifiers(message) {
|
|
2985
|
+
return message.replace(EMAIL_PATTERN, "[redacted-email]").replace(UUID_PATTERN, "[redacted-id]");
|
|
2986
|
+
}
|
|
2987
|
+
function sanitizeError(error) {
|
|
2988
|
+
const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
2989
|
+
for (const [pattern, userMessage] of ERROR_PATTERNS) {
|
|
2990
|
+
if (pattern.test(msg)) {
|
|
2991
|
+
return userMessage;
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
return "An unexpected error occurred. Please try again.";
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2731
2997
|
// src/lib/checkStatusShape.ts
|
|
2732
2998
|
function buildCheckStatusPayload(job, liveStatus) {
|
|
2733
2999
|
const status = liveStatus?.status ?? job.status;
|
|
@@ -2777,8 +3043,14 @@ function buildCheckStatusPayload(job, liveStatus) {
|
|
|
2777
3043
|
|
|
2778
3044
|
// src/lib/budget.ts
|
|
2779
3045
|
init_request_context();
|
|
2780
|
-
var MAX_CREDITS_PER_RUN = Math.max(
|
|
2781
|
-
|
|
3046
|
+
var MAX_CREDITS_PER_RUN = Math.max(
|
|
3047
|
+
0,
|
|
3048
|
+
Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0)
|
|
3049
|
+
);
|
|
3050
|
+
var MAX_ASSETS_PER_RUN = Math.max(
|
|
3051
|
+
0,
|
|
3052
|
+
Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0)
|
|
3053
|
+
);
|
|
2782
3054
|
var _globalCreditsUsed = 0;
|
|
2783
3055
|
var _globalAssetsGenerated = 0;
|
|
2784
3056
|
function getCreditsUsed() {
|
|
@@ -2823,9 +3095,22 @@ function checkCreditBudget(estimatedCost) {
|
|
|
2823
3095
|
}
|
|
2824
3096
|
const used = getCreditsUsed();
|
|
2825
3097
|
if (used + estimatedCost > MAX_CREDITS_PER_RUN) {
|
|
3098
|
+
const message = `Per-run credit cap reached \u2014 this is a local spend guard, not a server rate limit, so retrying will not help. The SOCIALNEURON_MAX_CREDITS_PER_RUN environment variable is set to ${MAX_CREDITS_PER_RUN} credits. This run has already spent ${used} credits and this call is estimated at ~${estimatedCost} more (${used} + ${estimatedCost} = ${used + estimatedCost} > ${MAX_CREDITS_PER_RUN}). To proceed, raise or unset SOCIALNEURON_MAX_CREDITS_PER_RUN.`;
|
|
2826
3099
|
return {
|
|
2827
3100
|
ok: false,
|
|
2828
|
-
message
|
|
3101
|
+
message,
|
|
3102
|
+
error: toolError("billing_error", message, {
|
|
3103
|
+
recover_with: [
|
|
3104
|
+
"Raise SOCIALNEURON_MAX_CREDITS_PER_RUN to a higher credit ceiling.",
|
|
3105
|
+
"Unset SOCIALNEURON_MAX_CREDITS_PER_RUN to remove the per-run cap."
|
|
3106
|
+
],
|
|
3107
|
+
details: {
|
|
3108
|
+
env_var: "SOCIALNEURON_MAX_CREDITS_PER_RUN",
|
|
3109
|
+
credits_used_this_run: used,
|
|
3110
|
+
estimated_call_cost: estimatedCost,
|
|
3111
|
+
max_credits_per_run: MAX_CREDITS_PER_RUN
|
|
3112
|
+
}
|
|
3113
|
+
})
|
|
2829
3114
|
};
|
|
2830
3115
|
}
|
|
2831
3116
|
return { ok: true };
|
|
@@ -2875,6 +3160,10 @@ var VIDEO_CREDIT_ESTIMATES = {
|
|
|
2875
3160
|
"seedance-1.5-pro": 150,
|
|
2876
3161
|
kling: 170
|
|
2877
3162
|
};
|
|
3163
|
+
var AUDIO_NATIVE_DEFAULT_MODELS = /* @__PURE__ */ new Set([
|
|
3164
|
+
"seedance-2-fast",
|
|
3165
|
+
"seedance-2"
|
|
3166
|
+
]);
|
|
2878
3167
|
var VIDEO_MODEL_ENUM = [
|
|
2879
3168
|
"seedance-2-fast",
|
|
2880
3169
|
"kling-3",
|
|
@@ -2903,7 +3192,7 @@ var IMAGE_CREDIT_ESTIMATES = {
|
|
|
2903
3192
|
function registerContentTools(server) {
|
|
2904
3193
|
server.tool(
|
|
2905
3194
|
"generate_video",
|
|
2906
|
-
"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
|
|
3195
|
+
"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 EXCEPT the seedance-2 family (seedance-2-fast, seedance-2) where audio is ON by default and its cost is already included. Check get_credit_balance first for expensive generations.",
|
|
2907
3196
|
{
|
|
2908
3197
|
prompt: z2.string().max(2500).describe(
|
|
2909
3198
|
'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.'
|
|
@@ -2918,7 +3207,7 @@ function registerContentTools(server) {
|
|
|
2918
3207
|
"Video aspect ratio. 16:9 for YouTube/landscape, 9:16 for TikTok/Reels/Shorts, 1:1 for Instagram feed/square. Defaults to 16:9."
|
|
2919
3208
|
),
|
|
2920
3209
|
enable_audio: z2.boolean().optional().describe(
|
|
2921
|
-
"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).
|
|
3210
|
+
"Enable native audio generation. For most models this DEFAULTS TO FALSE (cost control). EXCEPTION: the seedance-2 family (seedance-2-fast, seedance-2) DEFAULTS TO TRUE \u2014 these generate audio natively and their credit cost already includes it, so audio is on unless you explicitly pass false. Cost multiplier when true on other models: 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). 5+ languages."
|
|
2922
3211
|
),
|
|
2923
3212
|
image_url: z2.string().optional().describe(
|
|
2924
3213
|
"Start frame image URL for image-to-video (Kling 3.0 frame control)."
|
|
@@ -2954,12 +3243,12 @@ function registerContentTools(server) {
|
|
|
2954
3243
|
const estimatedCost = VIDEO_CREDIT_ESTIMATES[model] ?? 120;
|
|
2955
3244
|
const budgetCheck = checkCreditBudget(estimatedCost);
|
|
2956
3245
|
if (!budgetCheck.ok) {
|
|
2957
|
-
return
|
|
2958
|
-
content: [{ type: "text", text: budgetCheck.message }],
|
|
2959
|
-
isError: true
|
|
2960
|
-
};
|
|
3246
|
+
return budgetCheck.error;
|
|
2961
3247
|
}
|
|
2962
|
-
const rateLimit = checkRateLimit(
|
|
3248
|
+
const rateLimit = checkRateLimit(
|
|
3249
|
+
"generation",
|
|
3250
|
+
`generate_video:${userId}`
|
|
3251
|
+
);
|
|
2963
3252
|
if (!rateLimit.allowed) {
|
|
2964
3253
|
return {
|
|
2965
3254
|
content: [
|
|
@@ -2978,9 +3267,12 @@ function registerContentTools(server) {
|
|
|
2978
3267
|
model,
|
|
2979
3268
|
duration: duration ?? 5,
|
|
2980
3269
|
aspectRatio: aspect_ratio ?? "16:9",
|
|
2981
|
-
// Default FALSE (2026-07-13): the old `?? true` default silently
|
|
2982
|
-
// kling-family costs (2.65x on kling 2.6) for callers that never asked
|
|
2983
|
-
|
|
3270
|
+
// Default FALSE for most models (2026-07-13): the old `?? true` default silently
|
|
3271
|
+
// multiplied kling-family costs (2.65x on kling 2.6) for callers that never asked
|
|
3272
|
+
// for audio. Exception (2026-07-17): the seedance-2 family bills audio into its base
|
|
3273
|
+
// cost and generates it natively, so a false default there shipped silent no-audio
|
|
3274
|
+
// mp4s — those models default TRUE when the caller omits enable_audio.
|
|
3275
|
+
enableAudio: enable_audio ?? AUDIO_NATIVE_DEFAULT_MODELS.has(model),
|
|
2984
3276
|
...image_url && { imageUrl: image_url },
|
|
2985
3277
|
...end_frame_url && { endFrameUrl: end_frame_url },
|
|
2986
3278
|
// The server reads projectId and enforces
|
|
@@ -3082,7 +3374,14 @@ function registerContentTools(server) {
|
|
|
3082
3374
|
project_id: z2.string().optional().describe("Project ID to associate the generated image with."),
|
|
3083
3375
|
response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
3084
3376
|
},
|
|
3085
|
-
async ({
|
|
3377
|
+
async ({
|
|
3378
|
+
prompt,
|
|
3379
|
+
model,
|
|
3380
|
+
aspect_ratio,
|
|
3381
|
+
image_url,
|
|
3382
|
+
project_id,
|
|
3383
|
+
response_format
|
|
3384
|
+
}) => {
|
|
3086
3385
|
const format = response_format ?? "text";
|
|
3087
3386
|
const userId = await getDefaultUserId();
|
|
3088
3387
|
const assetBudget = checkAssetBudget();
|
|
@@ -3095,12 +3394,12 @@ function registerContentTools(server) {
|
|
|
3095
3394
|
const estimatedCost = IMAGE_CREDIT_ESTIMATES[model] ?? 30;
|
|
3096
3395
|
const budgetCheck = checkCreditBudget(estimatedCost);
|
|
3097
3396
|
if (!budgetCheck.ok) {
|
|
3098
|
-
return
|
|
3099
|
-
content: [{ type: "text", text: budgetCheck.message }],
|
|
3100
|
-
isError: true
|
|
3101
|
-
};
|
|
3397
|
+
return budgetCheck.error;
|
|
3102
3398
|
}
|
|
3103
|
-
const rateLimit = checkRateLimit(
|
|
3399
|
+
const rateLimit = checkRateLimit(
|
|
3400
|
+
"generation",
|
|
3401
|
+
`generate_image:${userId}`
|
|
3402
|
+
);
|
|
3104
3403
|
if (!rateLimit.allowed) {
|
|
3105
3404
|
return {
|
|
3106
3405
|
content: [
|
|
@@ -3231,6 +3530,15 @@ function registerContentTools(server) {
|
|
|
3231
3530
|
isError: true
|
|
3232
3531
|
};
|
|
3233
3532
|
}
|
|
3533
|
+
let projectsDisclosure;
|
|
3534
|
+
const ownScopedProjectId = await getDefaultProjectId();
|
|
3535
|
+
if (!ownScopedProjectId) {
|
|
3536
|
+
const ownUserId = await getDefaultUserId().catch(() => null);
|
|
3537
|
+
if (ownUserId) {
|
|
3538
|
+
const list = await listAccessibleProjectsWithAccountStatus(ownUserId);
|
|
3539
|
+
if (list.length > 0) projectsDisclosure = list;
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3234
3542
|
if (job.external_id && (job.status === "pending" || job.status === "processing")) {
|
|
3235
3543
|
const { data: liveStatus } = await callEdgeFunction(
|
|
3236
3544
|
"kie-task-status",
|
|
@@ -3254,7 +3562,9 @@ function registerContentTools(server) {
|
|
|
3254
3562
|
if (livePayload.error) {
|
|
3255
3563
|
lines2.push(`Error: ${livePayload.error}`);
|
|
3256
3564
|
}
|
|
3257
|
-
const fallbackDisclosure2 = buildFallbackDisclosureLine(
|
|
3565
|
+
const fallbackDisclosure2 = buildFallbackDisclosureLine(
|
|
3566
|
+
job.result_metadata
|
|
3567
|
+
);
|
|
3258
3568
|
if (fallbackDisclosure2) lines2.push(fallbackDisclosure2);
|
|
3259
3569
|
lines2.push(`Credits: ${job.credits_cost}`);
|
|
3260
3570
|
if (job.billing_status) {
|
|
@@ -3263,6 +3573,14 @@ function registerContentTools(server) {
|
|
|
3263
3573
|
);
|
|
3264
3574
|
}
|
|
3265
3575
|
lines2.push(`Created: ${job.created_at}`);
|
|
3576
|
+
if (projectsDisclosure) {
|
|
3577
|
+
lines2.push(
|
|
3578
|
+
"",
|
|
3579
|
+
`Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
|
|
3580
|
+
(p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
|
|
3581
|
+
).join("; ")}`
|
|
3582
|
+
);
|
|
3583
|
+
}
|
|
3266
3584
|
if (format === "json") {
|
|
3267
3585
|
return {
|
|
3268
3586
|
content: [
|
|
@@ -3279,7 +3597,8 @@ function registerContentTools(server) {
|
|
|
3279
3597
|
// `job` + `liveStatus`; do not duplicate them here.
|
|
3280
3598
|
asEnvelope2({
|
|
3281
3599
|
...liveStatus,
|
|
3282
|
-
...livePayload
|
|
3600
|
+
...livePayload,
|
|
3601
|
+
...projectsDisclosure ? { projects: projectsDisclosure } : {}
|
|
3283
3602
|
}),
|
|
3284
3603
|
null,
|
|
3285
3604
|
2
|
|
@@ -3320,9 +3639,11 @@ function registerContentTools(server) {
|
|
|
3320
3639
|
);
|
|
3321
3640
|
}
|
|
3322
3641
|
if (job.error_message) {
|
|
3323
|
-
lines.push(`Error: ${job.error_message}`);
|
|
3642
|
+
lines.push(`Error: ${redactSensitiveIdentifiers(job.error_message)}`);
|
|
3324
3643
|
}
|
|
3325
|
-
const fallbackDisclosure = buildFallbackDisclosureLine(
|
|
3644
|
+
const fallbackDisclosure = buildFallbackDisclosureLine(
|
|
3645
|
+
job.result_metadata
|
|
3646
|
+
);
|
|
3326
3647
|
if (fallbackDisclosure) lines.push(fallbackDisclosure);
|
|
3327
3648
|
lines.push(`Credits: ${job.credits_cost}`);
|
|
3328
3649
|
if (job.billing_status) {
|
|
@@ -3334,10 +3655,19 @@ function registerContentTools(server) {
|
|
|
3334
3655
|
if (job.completed_at) {
|
|
3335
3656
|
lines.push(`Completed: ${job.completed_at}`);
|
|
3336
3657
|
}
|
|
3658
|
+
if (projectsDisclosure) {
|
|
3659
|
+
lines.push(
|
|
3660
|
+
"",
|
|
3661
|
+
`Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
|
|
3662
|
+
(p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
|
|
3663
|
+
).join("; ")}`
|
|
3664
|
+
);
|
|
3665
|
+
}
|
|
3337
3666
|
if (format === "json") {
|
|
3338
3667
|
const enriched = {
|
|
3339
3668
|
...job,
|
|
3340
|
-
...buildCheckStatusPayload(job)
|
|
3669
|
+
...buildCheckStatusPayload(job),
|
|
3670
|
+
...projectsDisclosure ? { projects: projectsDisclosure } : {}
|
|
3341
3671
|
};
|
|
3342
3672
|
return {
|
|
3343
3673
|
content: [
|
|
@@ -3463,10 +3793,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
3463
3793
|
const estimatedCost = 10;
|
|
3464
3794
|
const budgetCheck = checkCreditBudget(estimatedCost);
|
|
3465
3795
|
if (!budgetCheck.ok) {
|
|
3466
|
-
return
|
|
3467
|
-
content: [{ type: "text", text: budgetCheck.message }],
|
|
3468
|
-
isError: true
|
|
3469
|
-
};
|
|
3796
|
+
return budgetCheck.error;
|
|
3470
3797
|
}
|
|
3471
3798
|
const { data, error } = await callEdgeFunction(
|
|
3472
3799
|
"social-neuron-ai",
|
|
@@ -3553,10 +3880,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
3553
3880
|
const estimatedCost = 15;
|
|
3554
3881
|
const budgetCheck = checkCreditBudget(estimatedCost);
|
|
3555
3882
|
if (!budgetCheck.ok) {
|
|
3556
|
-
return
|
|
3557
|
-
content: [{ type: "text", text: budgetCheck.message }],
|
|
3558
|
-
isError: true
|
|
3559
|
-
};
|
|
3883
|
+
return budgetCheck.error;
|
|
3560
3884
|
}
|
|
3561
3885
|
const rateLimit = checkRateLimit(
|
|
3562
3886
|
"generation",
|
|
@@ -3719,10 +4043,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
3719
4043
|
const estimatedCost = 10 + slideCount * 2;
|
|
3720
4044
|
const budgetCheck = checkCreditBudget(estimatedCost);
|
|
3721
4045
|
if (!budgetCheck.ok) {
|
|
3722
|
-
return
|
|
3723
|
-
content: [{ type: "text", text: budgetCheck.message }],
|
|
3724
|
-
isError: true
|
|
3725
|
-
};
|
|
4046
|
+
return budgetCheck.error;
|
|
3726
4047
|
}
|
|
3727
4048
|
const userId = await getDefaultUserId();
|
|
3728
4049
|
const rateLimit = checkRateLimit(
|
|
@@ -3798,83 +4119,39 @@ Return ONLY valid JSON in this exact format:
|
|
|
3798
4119
|
credits: data.carousel.credits
|
|
3799
4120
|
}),
|
|
3800
4121
|
null,
|
|
3801
|
-
2
|
|
3802
|
-
)
|
|
3803
|
-
}
|
|
3804
|
-
]
|
|
3805
|
-
};
|
|
3806
|
-
}
|
|
3807
|
-
const lines = [
|
|
3808
|
-
`Carousel generated successfully.`,
|
|
3809
|
-
` ID: ${data.carousel.id}`,
|
|
3810
|
-
` Template: ${templateId}`,
|
|
3811
|
-
` Style: ${resolvedStyle}`,
|
|
3812
|
-
` Slides: ${data.carousel.slides.length}`,
|
|
3813
|
-
` Credits: ${creditsUsed}`,
|
|
3814
|
-
"",
|
|
3815
|
-
"Slides:",
|
|
3816
|
-
...data.carousel.slides.map(
|
|
3817
|
-
(s, i) => ` ${i + 1}. ${s.headline || "(no headline)"}${s.emphasisWords?.length ? ` [emphasis: ${s.emphasisWords.join(", ")}]` : ""}`
|
|
3818
|
-
),
|
|
3819
|
-
"",
|
|
3820
|
-
"Next: Use generate_image for each slide, then schedule_post with media_urls to publish as Instagram carousel."
|
|
3821
|
-
];
|
|
3822
|
-
return {
|
|
3823
|
-
content: [{ type: "text", text: lines.join("\n") }]
|
|
3824
|
-
};
|
|
3825
|
-
}
|
|
3826
|
-
);
|
|
3827
|
-
}
|
|
3828
|
-
|
|
3829
|
-
// src/tools/distribution.ts
|
|
3830
|
-
import { z as z3 } from "zod";
|
|
3831
|
-
import { createHash } from "node:crypto";
|
|
3832
|
-
|
|
3833
|
-
// src/lib/sanitize-error.ts
|
|
3834
|
-
var ERROR_PATTERNS = [
|
|
3835
|
-
// Postgres / PostgREST
|
|
3836
|
-
[/PGRST301|permission denied/i, "Access denied. Check your account permissions."],
|
|
3837
|
-
[/42P01|does not exist/i, "Service temporarily unavailable. Please try again."],
|
|
3838
|
-
[/23505|unique.*constraint|duplicate key/i, "A duplicate record already exists."],
|
|
3839
|
-
[/23503|foreign key/i, "Referenced record not found."],
|
|
3840
|
-
// Gemini / Google AI
|
|
3841
|
-
[/google.*api.*key|googleapis\.com.*40[13]/i, "Content generation failed. Please try again."],
|
|
3842
|
-
[
|
|
3843
|
-
/RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
|
|
3844
|
-
"AI service rate limit reached. Please wait and retry."
|
|
3845
|
-
],
|
|
3846
|
-
[
|
|
3847
|
-
/SAFETY|prompt.*blocked|content.*filter/i,
|
|
3848
|
-
"Content was blocked by the AI safety filter. Try rephrasing."
|
|
3849
|
-
],
|
|
3850
|
-
[/gemini.*error|generativelanguage/i, "Content generation failed. Please try again."],
|
|
3851
|
-
// Kie.ai
|
|
3852
|
-
[/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
|
|
3853
|
-
// Stripe
|
|
3854
|
-
[/stripe.*api|sk_live_|sk_test_/i, "Payment processing error. Please try again."],
|
|
3855
|
-
// Network / fetch
|
|
3856
|
-
[
|
|
3857
|
-
/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
|
|
3858
|
-
"External service unavailable. Please try again."
|
|
3859
|
-
],
|
|
3860
|
-
[/fetch failed|network error|abort.*timeout/i, "Network request failed. Please try again."],
|
|
3861
|
-
[/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
|
|
3862
|
-
// Supabase Edge Function internals
|
|
3863
|
-
[/FunctionsHttpError|non-2xx status/i, "Backend service error. Please try again."],
|
|
3864
|
-
[/JWT|token.*expired|token.*invalid/i, "Authentication expired. Please re-authenticate."],
|
|
3865
|
-
// Generic sensitive patterns (API keys, URLs with secrets)
|
|
3866
|
-
[/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i, "An internal error occurred. Please try again."]
|
|
3867
|
-
];
|
|
3868
|
-
function sanitizeError(error) {
|
|
3869
|
-
const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
3870
|
-
for (const [pattern, userMessage] of ERROR_PATTERNS) {
|
|
3871
|
-
if (pattern.test(msg)) {
|
|
3872
|
-
return userMessage;
|
|
4122
|
+
2
|
|
4123
|
+
)
|
|
4124
|
+
}
|
|
4125
|
+
]
|
|
4126
|
+
};
|
|
4127
|
+
}
|
|
4128
|
+
const lines = [
|
|
4129
|
+
`Carousel generated successfully.`,
|
|
4130
|
+
` ID: ${data.carousel.id}`,
|
|
4131
|
+
` Template: ${templateId}`,
|
|
4132
|
+
` Style: ${resolvedStyle}`,
|
|
4133
|
+
` Slides: ${data.carousel.slides.length}`,
|
|
4134
|
+
` Credits: ${creditsUsed}`,
|
|
4135
|
+
"",
|
|
4136
|
+
"Slides:",
|
|
4137
|
+
...data.carousel.slides.map(
|
|
4138
|
+
(s, i) => ` ${i + 1}. ${s.headline || "(no headline)"}${s.emphasisWords?.length ? ` [emphasis: ${s.emphasisWords.join(", ")}]` : ""}`
|
|
4139
|
+
),
|
|
4140
|
+
"",
|
|
4141
|
+
"Next: Use generate_image for each slide, then schedule_post with media_urls to publish as Instagram carousel."
|
|
4142
|
+
];
|
|
4143
|
+
return {
|
|
4144
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
4145
|
+
};
|
|
3873
4146
|
}
|
|
3874
|
-
|
|
3875
|
-
return "An unexpected error occurred. Please try again.";
|
|
4147
|
+
);
|
|
3876
4148
|
}
|
|
3877
4149
|
|
|
4150
|
+
// src/tools/distribution.ts
|
|
4151
|
+
init_edge_function();
|
|
4152
|
+
import { z as z3 } from "zod";
|
|
4153
|
+
import { createHash } from "node:crypto";
|
|
4154
|
+
|
|
3878
4155
|
// src/lib/ssrf.ts
|
|
3879
4156
|
import { promises as dnsPromises } from "node:dns";
|
|
3880
4157
|
var BLOCKED_IP_PATTERNS = [
|
|
@@ -4153,6 +4430,116 @@ function evaluateQuality(input) {
|
|
|
4153
4430
|
};
|
|
4154
4431
|
}
|
|
4155
4432
|
|
|
4433
|
+
// src/lib/connected-account-routing.ts
|
|
4434
|
+
init_edge_function();
|
|
4435
|
+
var PLATFORM_CASE_MAP = {
|
|
4436
|
+
youtube: "YouTube",
|
|
4437
|
+
tiktok: "TikTok",
|
|
4438
|
+
instagram: "Instagram",
|
|
4439
|
+
twitter: "Twitter",
|
|
4440
|
+
x: "Twitter",
|
|
4441
|
+
linkedin: "LinkedIn",
|
|
4442
|
+
facebook: "Facebook",
|
|
4443
|
+
threads: "Threads",
|
|
4444
|
+
bluesky: "Bluesky"
|
|
4445
|
+
};
|
|
4446
|
+
function canonicalPlatform(value) {
|
|
4447
|
+
const normalized = value.trim().toLowerCase();
|
|
4448
|
+
return normalized === "x" ? "twitter" : normalized;
|
|
4449
|
+
}
|
|
4450
|
+
function providerPlatform(value) {
|
|
4451
|
+
return PLATFORM_CASE_MAP[canonicalPlatform(value)] ?? value;
|
|
4452
|
+
}
|
|
4453
|
+
function isUsable(account) {
|
|
4454
|
+
const status = account.effective_status ?? account.status;
|
|
4455
|
+
return status === "active" || status === "expires_soon";
|
|
4456
|
+
}
|
|
4457
|
+
function normalizeRequestedIds(requested) {
|
|
4458
|
+
const ids = /* @__PURE__ */ new Map();
|
|
4459
|
+
for (const [platform2, accountId] of Object.entries(requested ?? {})) {
|
|
4460
|
+
const canonical = canonicalPlatform(platform2);
|
|
4461
|
+
const existing = ids.get(canonical);
|
|
4462
|
+
if (existing && existing !== accountId) {
|
|
4463
|
+
return {
|
|
4464
|
+
error: `Conflicting account IDs were supplied for ${platform2} and its platform alias.`
|
|
4465
|
+
};
|
|
4466
|
+
}
|
|
4467
|
+
ids.set(canonical, accountId);
|
|
4468
|
+
}
|
|
4469
|
+
return { ids };
|
|
4470
|
+
}
|
|
4471
|
+
async function resolveConnectedAccountRouting(input) {
|
|
4472
|
+
const normalizedRequested = normalizeRequestedIds(input.requestedAccountIds);
|
|
4473
|
+
if (normalizedRequested.error) return { error: normalizedRequested.error };
|
|
4474
|
+
const targetPlatforms = new Set(input.platforms.map(canonicalPlatform));
|
|
4475
|
+
for (const requestedPlatform of normalizedRequested.ids?.keys() ?? []) {
|
|
4476
|
+
if (!targetPlatforms.has(requestedPlatform)) {
|
|
4477
|
+
return {
|
|
4478
|
+
error: `An account ID was supplied for untargeted platform ${requestedPlatform}.`
|
|
4479
|
+
};
|
|
4480
|
+
}
|
|
4481
|
+
}
|
|
4482
|
+
const { data, error } = await callEdgeFunction(
|
|
4483
|
+
"mcp-data",
|
|
4484
|
+
{
|
|
4485
|
+
action: "connected-accounts",
|
|
4486
|
+
projectId: input.projectId,
|
|
4487
|
+
project_id: input.projectId
|
|
4488
|
+
},
|
|
4489
|
+
{ timeoutMs: 1e4 }
|
|
4490
|
+
);
|
|
4491
|
+
if (error || !Array.isArray(data?.accounts)) {
|
|
4492
|
+
return {
|
|
4493
|
+
error: `Connected-account verification failed: ${error ?? data?.error ?? "no account inventory returned"}.`
|
|
4494
|
+
};
|
|
4495
|
+
}
|
|
4496
|
+
const connectedAccountIds = {};
|
|
4497
|
+
for (const platform2 of input.platforms) {
|
|
4498
|
+
const canonical = canonicalPlatform(platform2);
|
|
4499
|
+
const displayPlatform = providerPlatform(platform2);
|
|
4500
|
+
const platformAccounts = data.accounts.filter(
|
|
4501
|
+
(account) => canonicalPlatform(account.platform) === canonical && account.project_id === input.projectId && isUsable(account)
|
|
4502
|
+
);
|
|
4503
|
+
const requestedId = normalizedRequested.ids?.get(canonical);
|
|
4504
|
+
let selected;
|
|
4505
|
+
if (requestedId) {
|
|
4506
|
+
selected = data.accounts.find((account) => account.id === requestedId);
|
|
4507
|
+
if (!selected) {
|
|
4508
|
+
return {
|
|
4509
|
+
error: `${displayPlatform}: account ${requestedId} is not available for project_id ${input.projectId}.`
|
|
4510
|
+
};
|
|
4511
|
+
}
|
|
4512
|
+
if (canonicalPlatform(selected.platform) !== canonical) {
|
|
4513
|
+
return {
|
|
4514
|
+
error: `${displayPlatform}: account ${requestedId} belongs to ${selected.platform}.`
|
|
4515
|
+
};
|
|
4516
|
+
}
|
|
4517
|
+
if (selected.project_id !== input.projectId) {
|
|
4518
|
+
return {
|
|
4519
|
+
error: `${displayPlatform}: account ${requestedId} is not bound to project_id ${input.projectId}.`
|
|
4520
|
+
};
|
|
4521
|
+
}
|
|
4522
|
+
if (!isUsable(selected)) {
|
|
4523
|
+
return {
|
|
4524
|
+
error: `${displayPlatform}: account ${requestedId} is ${selected.effective_status ?? selected.status}.`
|
|
4525
|
+
};
|
|
4526
|
+
}
|
|
4527
|
+
} else if (platformAccounts.length === 1) {
|
|
4528
|
+
selected = platformAccounts[0];
|
|
4529
|
+
} else if (platformAccounts.length === 0) {
|
|
4530
|
+
return {
|
|
4531
|
+
error: `${displayPlatform}: no active account is bound to project_id ${input.projectId}.`
|
|
4532
|
+
};
|
|
4533
|
+
} else {
|
|
4534
|
+
return {
|
|
4535
|
+
error: `${displayPlatform}: multiple active accounts are bound to project_id ${input.projectId}; pass the exact account ID returned by list_connected_accounts.`
|
|
4536
|
+
};
|
|
4537
|
+
}
|
|
4538
|
+
connectedAccountIds[displayPlatform] = selected.id;
|
|
4539
|
+
}
|
|
4540
|
+
return { connectedAccountIds };
|
|
4541
|
+
}
|
|
4542
|
+
|
|
4156
4543
|
// src/tools/distribution.ts
|
|
4157
4544
|
function snakeToCamel(obj) {
|
|
4158
4545
|
const result = {};
|
|
@@ -4170,11 +4557,16 @@ function convertPlatformMetadata(meta) {
|
|
|
4170
4557
|
}
|
|
4171
4558
|
return converted;
|
|
4172
4559
|
}
|
|
4173
|
-
var
|
|
4560
|
+
var PLATFORM_CASE_MAP2 = {
|
|
4174
4561
|
youtube: "YouTube",
|
|
4175
4562
|
tiktok: "TikTok",
|
|
4176
4563
|
instagram: "Instagram",
|
|
4177
4564
|
twitter: "Twitter",
|
|
4565
|
+
// 'x' is the platform's current branding but connected_account_routing.ts
|
|
4566
|
+
// (and the DB convention) still key on 'Twitter' — keep both aliases
|
|
4567
|
+
// resolving to the same case so schedule_content_plan's platform:'x' posts
|
|
4568
|
+
// don't fall through to an undefined binding (F8, 2026-07-15).
|
|
4569
|
+
x: "Twitter",
|
|
4178
4570
|
linkedin: "LinkedIn",
|
|
4179
4571
|
facebook: "Facebook",
|
|
4180
4572
|
threads: "Threads",
|
|
@@ -4296,21 +4688,6 @@ async function validatePublishMediaUrl(url) {
|
|
|
4296
4688
|
function accountEffectiveStatus(account) {
|
|
4297
4689
|
return account.effective_status || account.status;
|
|
4298
4690
|
}
|
|
4299
|
-
function isUsableAccount(account) {
|
|
4300
|
-
const status = accountEffectiveStatus(account);
|
|
4301
|
-
return status === "active" || status === "expires_soon";
|
|
4302
|
-
}
|
|
4303
|
-
function formatAccountChoice(account) {
|
|
4304
|
-
const name = account.username ? `@${account.username}` : "unnamed";
|
|
4305
|
-
const project = account.project_id ? `project_id=${account.project_id}` : "project_id=unassigned";
|
|
4306
|
-
const refresh = account.has_refresh_token ? "OAuth 2.0 refresh" : "no refresh token";
|
|
4307
|
-
return `${account.id} (${name}, ${project}, ${accountEffectiveStatus(account)}, ${refresh})`;
|
|
4308
|
-
}
|
|
4309
|
-
function requestedAccountIdForPlatform(accountId, accountIds, platform2) {
|
|
4310
|
-
if (accountId) return accountId;
|
|
4311
|
-
if (!accountIds) return void 0;
|
|
4312
|
-
return accountIds[platform2] || accountIds[platform2.toLowerCase()];
|
|
4313
|
-
}
|
|
4314
4691
|
async function rehostExternalUrl(mediaUrl, projectId) {
|
|
4315
4692
|
const ssrf = await validateUrlForSSRF(mediaUrl);
|
|
4316
4693
|
if (!ssrf.isValid) {
|
|
@@ -4446,17 +4823,17 @@ function registerDistributionTools(server) {
|
|
|
4446
4823
|
schedule_at: z3.string().optional().describe(
|
|
4447
4824
|
'ISO 8601 UTC datetime for scheduled posting (e.g. "2026-03-20T14:00:00Z"). Omit to post immediately. Must be in the future.'
|
|
4448
4825
|
),
|
|
4449
|
-
project_id: z3.string().optional().describe(
|
|
4826
|
+
project_id: z3.string().uuid().optional().describe(
|
|
4450
4827
|
"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."
|
|
4451
4828
|
),
|
|
4452
4829
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
|
|
4453
4830
|
attribution: z3.boolean().optional().describe(
|
|
4454
4831
|
'If true, appends "Created with Social Neuron" to the caption. Default: false.'
|
|
4455
4832
|
),
|
|
4456
|
-
account_id: z3.string().optional().describe(
|
|
4457
|
-
"Connected account ID to post from.
|
|
4833
|
+
account_id: z3.string().uuid().optional().describe(
|
|
4834
|
+
"Connected account ID to post from. Optional when the resolved project has exactly one active account for the target platform \u2014 it is auto-bound. Required (with a clear error listing candidates) when multiple accounts exist for the same platform. Use list_connected_accounts to find the right ID. The account must be active and bound to the exact project_id."
|
|
4458
4835
|
),
|
|
4459
|
-
account_ids: z3.record(z3.string(), z3.string()).optional().describe(
|
|
4836
|
+
account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
|
|
4460
4837
|
'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.'
|
|
4461
4838
|
),
|
|
4462
4839
|
auto_rehost: z3.boolean().optional().describe(
|
|
@@ -4500,6 +4877,45 @@ function registerDistributionTools(server) {
|
|
|
4500
4877
|
isError: true
|
|
4501
4878
|
};
|
|
4502
4879
|
}
|
|
4880
|
+
const projectResolution = await resolveProjectForConnectedAccountTool(
|
|
4881
|
+
project_id,
|
|
4882
|
+
platforms
|
|
4883
|
+
);
|
|
4884
|
+
if (!projectResolution.projectId) {
|
|
4885
|
+
return {
|
|
4886
|
+
content: [
|
|
4887
|
+
{
|
|
4888
|
+
type: "text",
|
|
4889
|
+
text: projectResolution.error ?? "A project_id is required for publishing. Configure an explicit project or use an API key that is scoped to exactly one project."
|
|
4890
|
+
}
|
|
4891
|
+
],
|
|
4892
|
+
isError: true
|
|
4893
|
+
};
|
|
4894
|
+
}
|
|
4895
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
4896
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
4897
|
+
if (account_id && account_ids) {
|
|
4898
|
+
return {
|
|
4899
|
+
content: [
|
|
4900
|
+
{
|
|
4901
|
+
type: "text",
|
|
4902
|
+
text: "Pass either account_id or account_ids, not both."
|
|
4903
|
+
}
|
|
4904
|
+
],
|
|
4905
|
+
isError: true
|
|
4906
|
+
};
|
|
4907
|
+
}
|
|
4908
|
+
if (account_id && platforms.length !== 1) {
|
|
4909
|
+
return {
|
|
4910
|
+
content: [
|
|
4911
|
+
{
|
|
4912
|
+
type: "text",
|
|
4913
|
+
text: "account_id is valid only for a single target platform. Use account_ids for multi-platform publishing."
|
|
4914
|
+
}
|
|
4915
|
+
],
|
|
4916
|
+
isError: true
|
|
4917
|
+
};
|
|
4918
|
+
}
|
|
4503
4919
|
const userId = await getDefaultUserId();
|
|
4504
4920
|
const rateLimit = checkRateLimit("posting", `schedule_post:${userId}`);
|
|
4505
4921
|
if (!rateLimit.allowed) {
|
|
@@ -4604,11 +5020,16 @@ function registerDistributionTools(server) {
|
|
|
4604
5020
|
}
|
|
4605
5021
|
const resolvedJobs = resolved;
|
|
4606
5022
|
resolvedMediaUrls = resolvedJobs.map((item) => item.url);
|
|
4607
|
-
resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map(
|
|
5023
|
+
resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map(
|
|
5024
|
+
(item) => item.trustedR2
|
|
5025
|
+
);
|
|
4608
5026
|
}
|
|
4609
5027
|
const shouldRehost = auto_rehost !== false;
|
|
4610
5028
|
if (shouldRehost && resolvedMediaUrl && !resolvedMediaUrlIsTrustedR2) {
|
|
4611
|
-
const rehost = await rehostExternalUrl(
|
|
5029
|
+
const rehost = await rehostExternalUrl(
|
|
5030
|
+
resolvedMediaUrl,
|
|
5031
|
+
resolvedProjectId
|
|
5032
|
+
);
|
|
4612
5033
|
if ("error" in rehost) {
|
|
4613
5034
|
return {
|
|
4614
5035
|
content: [
|
|
@@ -4626,7 +5047,7 @@ function registerDistributionTools(server) {
|
|
|
4626
5047
|
if (shouldRehost && resolvedMediaUrls && resolvedMediaUrls.length > 0) {
|
|
4627
5048
|
const rehosted = await Promise.all(
|
|
4628
5049
|
resolvedMediaUrls.map(
|
|
4629
|
-
(u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u,
|
|
5050
|
+
(u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, resolvedProjectId)
|
|
4630
5051
|
)
|
|
4631
5052
|
);
|
|
4632
5053
|
const failIdx = rehosted.findIndex((r) => "error" in r);
|
|
@@ -4693,7 +5114,7 @@ function registerDistributionTools(server) {
|
|
|
4693
5114
|
}
|
|
4694
5115
|
}
|
|
4695
5116
|
const normalizedPlatforms = platforms.map(
|
|
4696
|
-
(p) =>
|
|
5117
|
+
(p) => PLATFORM_CASE_MAP2[p.toLowerCase()] || p
|
|
4697
5118
|
);
|
|
4698
5119
|
const blockedPlatforms = normalizedPlatforms.filter(
|
|
4699
5120
|
(p) => MCP_NOT_LIVE_FOR_POSTING.has(p)
|
|
@@ -4709,92 +5130,27 @@ function registerDistributionTools(server) {
|
|
|
4709
5130
|
isError: true
|
|
4710
5131
|
};
|
|
4711
5132
|
}
|
|
4712
|
-
|
|
4713
|
-
"mcp-data",
|
|
4714
|
-
{
|
|
4715
|
-
action: "connected-accounts",
|
|
4716
|
-
...project_id ? { projectId: project_id, project_id } : {}
|
|
4717
|
-
},
|
|
4718
|
-
{ timeoutMs: 1e4 }
|
|
4719
|
-
);
|
|
4720
|
-
if (accountsData?.accounts) {
|
|
4721
|
-
const accounts = accountsData.accounts;
|
|
4722
|
-
const issues = [];
|
|
4723
|
-
for (const platform2 of normalizedPlatforms) {
|
|
4724
|
-
const platformAccounts = accounts.filter(
|
|
4725
|
-
(a) => a.platform.toLowerCase() === platform2.toLowerCase() && isUsableAccount(a)
|
|
4726
|
-
);
|
|
4727
|
-
const requestedAccountId = requestedAccountIdForPlatform(
|
|
4728
|
-
account_id,
|
|
4729
|
-
account_ids,
|
|
4730
|
-
platform2
|
|
4731
|
-
);
|
|
4732
|
-
if (requestedAccountId) {
|
|
4733
|
-
const selected = accounts.find((a) => a.id === requestedAccountId);
|
|
4734
|
-
if (!selected) {
|
|
4735
|
-
issues.push(
|
|
4736
|
-
`${platform2}: 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.`
|
|
4737
|
-
);
|
|
4738
|
-
} else if (selected.platform.toLowerCase() !== platform2.toLowerCase()) {
|
|
4739
|
-
issues.push(
|
|
4740
|
-
`${platform2}: Account "${requestedAccountId}" belongs to ${selected.platform}, not ${platform2}.`
|
|
4741
|
-
);
|
|
4742
|
-
} else if (!isUsableAccount(selected)) {
|
|
4743
|
-
issues.push(
|
|
4744
|
-
`${platform2}: Account "${selected.username || selected.id}" is ${accountEffectiveStatus(selected)}. Reconnect at socialneuron.com/settings/connections.`
|
|
4745
|
-
);
|
|
4746
|
-
} else if (project_id && selected.project_id && selected.project_id !== project_id) {
|
|
4747
|
-
issues.push(
|
|
4748
|
-
`${platform2}: 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.`
|
|
4749
|
-
);
|
|
4750
|
-
}
|
|
4751
|
-
continue;
|
|
4752
|
-
}
|
|
4753
|
-
if (platformAccounts.length === 0) {
|
|
4754
|
-
issues.push(
|
|
4755
|
-
`${platform2}: 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="${platform2.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.`
|
|
4756
|
-
);
|
|
4757
|
-
} else if (platformAccounts.length > 1) {
|
|
4758
|
-
const accountList = platformAccounts.map((a) => ` - ${formatAccountChoice(a)}`).join("\n");
|
|
4759
|
-
issues.push(
|
|
4760
|
-
`${platform2}: Multiple accounts found. Specify account_id or account_ids to choose:
|
|
4761
|
-
${accountList}`
|
|
4762
|
-
);
|
|
4763
|
-
} else if (platformAccounts.length === 1) {
|
|
4764
|
-
const acct = platformAccounts[0];
|
|
4765
|
-
if (acct.expires_at && new Date(acct.expires_at) < /* @__PURE__ */ new Date() && !acct.has_refresh_token) {
|
|
4766
|
-
issues.push(
|
|
4767
|
-
`${platform2}: Account "${acct.username || acct.id}" has expired OAuth and no refresh token. Reconnect at socialneuron.com/settings/connections.`
|
|
4768
|
-
);
|
|
4769
|
-
}
|
|
4770
|
-
}
|
|
4771
|
-
}
|
|
4772
|
-
if (issues.length > 0) {
|
|
4773
|
-
return {
|
|
4774
|
-
content: [
|
|
4775
|
-
{
|
|
4776
|
-
type: "text",
|
|
4777
|
-
text: `Cannot post \u2014 account issues found:
|
|
4778
|
-
|
|
4779
|
-
${issues.join("\n\n")}`
|
|
4780
|
-
}
|
|
4781
|
-
],
|
|
4782
|
-
isError: true
|
|
4783
|
-
};
|
|
4784
|
-
}
|
|
4785
|
-
}
|
|
4786
|
-
let connectedAccountIds;
|
|
5133
|
+
let requestedAccountIds;
|
|
4787
5134
|
if (account_id) {
|
|
4788
|
-
|
|
4789
|
-
for (const p of normalizedPlatforms) {
|
|
4790
|
-
connectedAccountIds[p] = account_id;
|
|
4791
|
-
}
|
|
5135
|
+
requestedAccountIds = { [normalizedPlatforms[0]]: account_id };
|
|
4792
5136
|
} else if (account_ids) {
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
5137
|
+
requestedAccountIds = account_ids;
|
|
5138
|
+
}
|
|
5139
|
+
const routing = await resolveConnectedAccountRouting({
|
|
5140
|
+
projectId: resolvedProjectId,
|
|
5141
|
+
platforms: normalizedPlatforms,
|
|
5142
|
+
requestedAccountIds
|
|
5143
|
+
});
|
|
5144
|
+
if (routing.error || !routing.connectedAccountIds) {
|
|
5145
|
+
return {
|
|
5146
|
+
content: [
|
|
5147
|
+
{
|
|
5148
|
+
type: "text",
|
|
5149
|
+
text: `Cannot post \u2014 ${routing.error ?? "exact connected-account routing could not be established."}`
|
|
5150
|
+
}
|
|
5151
|
+
],
|
|
5152
|
+
isError: true
|
|
5153
|
+
};
|
|
4798
5154
|
}
|
|
4799
5155
|
let finalCaption = caption;
|
|
4800
5156
|
if (attribution && finalCaption) {
|
|
@@ -4848,8 +5204,9 @@ Created with Social Neuron`;
|
|
|
4848
5204
|
title,
|
|
4849
5205
|
hashtags,
|
|
4850
5206
|
scheduledAt: schedule_at,
|
|
4851
|
-
projectId:
|
|
4852
|
-
|
|
5207
|
+
projectId: resolvedProjectId,
|
|
5208
|
+
project_id: resolvedProjectId,
|
|
5209
|
+
connectedAccountIds: routing.connectedAccountIds,
|
|
4853
5210
|
...normalizedPlatformMetadata ? {
|
|
4854
5211
|
platformMetadata: convertPlatformMetadata(
|
|
4855
5212
|
normalizedPlatformMetadata
|
|
@@ -4884,10 +5241,14 @@ Created with Social Neuron`;
|
|
|
4884
5241
|
isError: true
|
|
4885
5242
|
};
|
|
4886
5243
|
}
|
|
5244
|
+
const responseData = projectAutoResolvedNote ? { ...data, project_auto_resolved: projectAutoResolvedNote } : data;
|
|
4887
5245
|
const lines = [
|
|
4888
5246
|
data.success ? "Post scheduled successfully." : "Post scheduling had errors.",
|
|
4889
5247
|
`Scheduled for: ${data.scheduledAt}`
|
|
4890
5248
|
];
|
|
5249
|
+
if (projectAutoResolvedNote) {
|
|
5250
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
5251
|
+
}
|
|
4891
5252
|
if (tiktokAutoInboxApplied) {
|
|
4892
5253
|
lines.push(
|
|
4893
5254
|
"",
|
|
@@ -4905,7 +5266,7 @@ Created with Social Neuron`;
|
|
|
4905
5266
|
}
|
|
4906
5267
|
}
|
|
4907
5268
|
if (format === "json") {
|
|
4908
|
-
const structuredContent = asEnvelope3(
|
|
5269
|
+
const structuredContent = asEnvelope3(responseData);
|
|
4909
5270
|
return {
|
|
4910
5271
|
structuredContent,
|
|
4911
5272
|
content: [
|
|
@@ -4918,7 +5279,7 @@ Created with Social Neuron`;
|
|
|
4918
5279
|
};
|
|
4919
5280
|
}
|
|
4920
5281
|
return {
|
|
4921
|
-
structuredContent: asEnvelope3(
|
|
5282
|
+
structuredContent: asEnvelope3(responseData),
|
|
4922
5283
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
4923
5284
|
isError: !data.success
|
|
4924
5285
|
};
|
|
@@ -4932,7 +5293,9 @@ Created with Social Neuron`;
|
|
|
4932
5293
|
project_id: z3.string().uuid().optional().describe(
|
|
4933
5294
|
"Brand/project ID that owns the post. Defaults to the authenticated key's project or the account default."
|
|
4934
5295
|
),
|
|
4935
|
-
scheduled_at: z3.string().datetime({ offset: true }).describe(
|
|
5296
|
+
scheduled_at: z3.string().datetime({ offset: true }).describe(
|
|
5297
|
+
"New future publish time as an ISO 8601 datetime with timezone."
|
|
5298
|
+
),
|
|
4936
5299
|
expected_scheduled_at: z3.string().datetime({ offset: true }).optional().describe(
|
|
4937
5300
|
"Optional current schedule timestamp. If it changed since you read it, the update is rejected instead of silently overwriting it."
|
|
4938
5301
|
),
|
|
@@ -4988,7 +5351,11 @@ Created with Social Neuron`;
|
|
|
4988
5351
|
projectId: resolvedProjectId,
|
|
4989
5352
|
project_id: resolvedProjectId,
|
|
4990
5353
|
scheduled_at: next.toISOString(),
|
|
4991
|
-
...expected_scheduled_at ? {
|
|
5354
|
+
...expected_scheduled_at ? {
|
|
5355
|
+
expected_scheduled_at: new Date(
|
|
5356
|
+
expected_scheduled_at
|
|
5357
|
+
).toISOString()
|
|
5358
|
+
} : {}
|
|
4992
5359
|
});
|
|
4993
5360
|
if (error || !result?.success) {
|
|
4994
5361
|
const code = result?.error ?? error ?? "reschedule_failed";
|
|
@@ -5021,17 +5388,34 @@ Created with Social Neuron`;
|
|
|
5021
5388
|
"list_connected_accounts",
|
|
5022
5389
|
"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.",
|
|
5023
5390
|
{
|
|
5024
|
-
project_id: z3.string().optional().describe(
|
|
5391
|
+
project_id: z3.string().uuid().optional().describe(
|
|
5025
5392
|
"Brand/project ID to scope connected accounts. Use the same project_id when calling schedule_post."
|
|
5026
5393
|
),
|
|
5027
|
-
include_all: z3.boolean().optional().describe(
|
|
5394
|
+
include_all: z3.boolean().optional().describe(
|
|
5395
|
+
"If true, include expired or inactive accounts as well as usable accounts."
|
|
5396
|
+
),
|
|
5028
5397
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
5029
5398
|
},
|
|
5030
5399
|
async ({ project_id, include_all, response_format }) => {
|
|
5031
5400
|
const format = response_format ?? "text";
|
|
5401
|
+
const projectResolution = await resolveProjectForConnectedAccountTool(project_id);
|
|
5402
|
+
if (!projectResolution.projectId) {
|
|
5403
|
+
return {
|
|
5404
|
+
content: [
|
|
5405
|
+
{
|
|
5406
|
+
type: "text",
|
|
5407
|
+
text: projectResolution.error ?? "A project_id is required to list connected accounts. Configure an explicit project or use an API key scoped to exactly one project."
|
|
5408
|
+
}
|
|
5409
|
+
],
|
|
5410
|
+
isError: true
|
|
5411
|
+
};
|
|
5412
|
+
}
|
|
5413
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
5414
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
5032
5415
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
5033
5416
|
action: "connected-accounts",
|
|
5034
|
-
|
|
5417
|
+
projectId: resolvedProjectId,
|
|
5418
|
+
project_id: resolvedProjectId,
|
|
5035
5419
|
...include_all ? { includeAll: true } : {}
|
|
5036
5420
|
});
|
|
5037
5421
|
if (efError || !result?.success) {
|
|
@@ -5045,10 +5429,27 @@ Created with Social Neuron`;
|
|
|
5045
5429
|
isError: true
|
|
5046
5430
|
};
|
|
5047
5431
|
}
|
|
5048
|
-
const
|
|
5432
|
+
const parsedAccounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
|
|
5433
|
+
if (parsedAccounts.some(
|
|
5434
|
+
(account) => account.project_id !== resolvedProjectId
|
|
5435
|
+
)) {
|
|
5436
|
+
return {
|
|
5437
|
+
content: [
|
|
5438
|
+
{
|
|
5439
|
+
type: "text",
|
|
5440
|
+
text: "Connected-account project attestation failed. No account inventory was returned."
|
|
5441
|
+
}
|
|
5442
|
+
],
|
|
5443
|
+
isError: true
|
|
5444
|
+
};
|
|
5445
|
+
}
|
|
5446
|
+
const accounts = parsedAccounts;
|
|
5049
5447
|
if (accounts.length === 0) {
|
|
5050
5448
|
if (format === "json") {
|
|
5051
|
-
const structuredContent = asEnvelope3({
|
|
5449
|
+
const structuredContent = asEnvelope3({
|
|
5450
|
+
accounts: [],
|
|
5451
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
5452
|
+
});
|
|
5052
5453
|
return {
|
|
5053
5454
|
structuredContent,
|
|
5054
5455
|
content: [
|
|
@@ -5063,13 +5464,15 @@ Created with Social Neuron`;
|
|
|
5063
5464
|
content: [
|
|
5064
5465
|
{
|
|
5065
5466
|
type: "text",
|
|
5066
|
-
text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections."
|
|
5467
|
+
text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections." + (projectAutoResolvedNote ? `
|
|
5468
|
+
|
|
5469
|
+
Note: ${projectAutoResolvedNote}` : "")
|
|
5067
5470
|
}
|
|
5068
5471
|
]
|
|
5069
5472
|
};
|
|
5070
5473
|
}
|
|
5071
5474
|
const lines = [
|
|
5072
|
-
`${accounts.length} connected account(s)
|
|
5475
|
+
`${accounts.length} connected account(s) for project ${resolvedProjectId}:`,
|
|
5073
5476
|
""
|
|
5074
5477
|
];
|
|
5075
5478
|
for (const account of accounts) {
|
|
@@ -5081,8 +5484,14 @@ Created with Social Neuron`;
|
|
|
5081
5484
|
` ${platformLower}: ${name} | id=${account.id} | ${project} | status=${status} (connected ${account.created_at.split("T")[0]})`
|
|
5082
5485
|
);
|
|
5083
5486
|
}
|
|
5487
|
+
if (projectAutoResolvedNote) {
|
|
5488
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
5489
|
+
}
|
|
5084
5490
|
if (format === "json") {
|
|
5085
|
-
const structuredContent = asEnvelope3({
|
|
5491
|
+
const structuredContent = asEnvelope3({
|
|
5492
|
+
accounts,
|
|
5493
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
5494
|
+
});
|
|
5086
5495
|
return {
|
|
5087
5496
|
structuredContent,
|
|
5088
5497
|
content: [
|
|
@@ -5277,7 +5686,10 @@ Created with Social Neuron`;
|
|
|
5277
5686
|
if (!Number.isFinite(startDate.getTime())) {
|
|
5278
5687
|
return {
|
|
5279
5688
|
content: [
|
|
5280
|
-
{
|
|
5689
|
+
{
|
|
5690
|
+
type: "text",
|
|
5691
|
+
text: "start_after must be a valid ISO datetime."
|
|
5692
|
+
}
|
|
5281
5693
|
],
|
|
5282
5694
|
isError: true
|
|
5283
5695
|
};
|
|
@@ -5378,11 +5790,14 @@ Created with Social Neuron`;
|
|
|
5378
5790
|
"Schedule all posts in a content plan. Optionally auto-assigns time slots and runs quality checks before scheduling. Supports dry-run mode.",
|
|
5379
5791
|
{
|
|
5380
5792
|
plan: z3.object({
|
|
5793
|
+
project_id: z3.string().uuid().optional(),
|
|
5794
|
+
account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe("Exact connected-account ID per platform."),
|
|
5381
5795
|
posts: z3.array(
|
|
5382
5796
|
z3.object({
|
|
5383
5797
|
id: z3.string(),
|
|
5384
5798
|
caption: z3.string(),
|
|
5385
5799
|
platform: z3.string(),
|
|
5800
|
+
connected_account_id: z3.string().uuid().optional(),
|
|
5386
5801
|
title: z3.string().optional(),
|
|
5387
5802
|
media_url: z3.string().optional(),
|
|
5388
5803
|
schedule_at: z3.string().optional(),
|
|
@@ -5390,6 +5805,12 @@ Created with Social Neuron`;
|
|
|
5390
5805
|
})
|
|
5391
5806
|
)
|
|
5392
5807
|
}).passthrough().optional(),
|
|
5808
|
+
project_id: z3.string().uuid().optional().describe(
|
|
5809
|
+
"Exact brand/project ID. Defaults only when the authenticated user has one project."
|
|
5810
|
+
),
|
|
5811
|
+
account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
|
|
5812
|
+
"Exact connected-account ID per platform for every post in the plan."
|
|
5813
|
+
),
|
|
5393
5814
|
plan_id: z3.string().uuid().optional().describe("Persisted content plan ID from content_plans table"),
|
|
5394
5815
|
auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
|
|
5395
5816
|
dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
|
|
@@ -5406,6 +5827,8 @@ Created with Social Neuron`;
|
|
|
5406
5827
|
async ({
|
|
5407
5828
|
plan,
|
|
5408
5829
|
plan_id,
|
|
5830
|
+
project_id,
|
|
5831
|
+
account_ids,
|
|
5409
5832
|
auto_slot,
|
|
5410
5833
|
dry_run,
|
|
5411
5834
|
response_format,
|
|
@@ -5415,9 +5838,11 @@ Created with Social Neuron`;
|
|
|
5415
5838
|
idempotency_seed
|
|
5416
5839
|
}) => {
|
|
5417
5840
|
try {
|
|
5841
|
+
const effectiveBatchSize = batch_size ?? 4;
|
|
5418
5842
|
let workingPlan = plan;
|
|
5419
5843
|
let effectivePlanId = plan_id;
|
|
5420
|
-
let effectiveProjectId;
|
|
5844
|
+
let effectiveProjectId = project_id;
|
|
5845
|
+
let projectAutoResolvedNote;
|
|
5421
5846
|
let approvalSummary;
|
|
5422
5847
|
if (!workingPlan && plan_id) {
|
|
5423
5848
|
const { data: planResult, error: planError } = await callEdgeFunction("mcp-data", { action: "get-content-plan", plan_id });
|
|
@@ -5464,7 +5889,18 @@ Created with Social Neuron`;
|
|
|
5464
5889
|
posts: postsFromPayload
|
|
5465
5890
|
};
|
|
5466
5891
|
effectivePlanId = stored.id;
|
|
5467
|
-
effectiveProjectId
|
|
5892
|
+
if (effectiveProjectId && stored.project_id && effectiveProjectId !== stored.project_id) {
|
|
5893
|
+
return {
|
|
5894
|
+
content: [
|
|
5895
|
+
{
|
|
5896
|
+
type: "text",
|
|
5897
|
+
text: `project_id ${effectiveProjectId} does not own plan ${plan_id}.`
|
|
5898
|
+
}
|
|
5899
|
+
],
|
|
5900
|
+
isError: true
|
|
5901
|
+
};
|
|
5902
|
+
}
|
|
5903
|
+
effectiveProjectId = stored.project_id ?? effectiveProjectId;
|
|
5468
5904
|
}
|
|
5469
5905
|
if (!workingPlan) {
|
|
5470
5906
|
return {
|
|
@@ -5477,10 +5913,35 @@ Created with Social Neuron`;
|
|
|
5477
5913
|
isError: true
|
|
5478
5914
|
};
|
|
5479
5915
|
}
|
|
5916
|
+
const planProjectId = workingPlan.project_id;
|
|
5917
|
+
if (effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0 && effectiveProjectId !== planProjectId) {
|
|
5918
|
+
return {
|
|
5919
|
+
content: [
|
|
5920
|
+
{
|
|
5921
|
+
type: "text",
|
|
5922
|
+
text: `Conflicting project_id values were supplied for the plan (${planProjectId}) and request (${effectiveProjectId}).`
|
|
5923
|
+
}
|
|
5924
|
+
],
|
|
5925
|
+
isError: true
|
|
5926
|
+
};
|
|
5927
|
+
}
|
|
5928
|
+
if (!effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0) {
|
|
5929
|
+
effectiveProjectId = planProjectId;
|
|
5930
|
+
}
|
|
5480
5931
|
if (!effectiveProjectId) {
|
|
5481
|
-
const
|
|
5482
|
-
|
|
5483
|
-
|
|
5932
|
+
const projectResolution = await resolveProjectForConnectedAccountTool();
|
|
5933
|
+
effectiveProjectId = projectResolution.projectId;
|
|
5934
|
+
projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
5935
|
+
if (!effectiveProjectId) {
|
|
5936
|
+
return {
|
|
5937
|
+
content: [
|
|
5938
|
+
{
|
|
5939
|
+
type: "text",
|
|
5940
|
+
text: projectResolution.error ?? "A project_id is required to schedule a content plan. Configure an explicit project or use an API key scoped to exactly one project."
|
|
5941
|
+
}
|
|
5942
|
+
],
|
|
5943
|
+
isError: true
|
|
5944
|
+
};
|
|
5484
5945
|
}
|
|
5485
5946
|
}
|
|
5486
5947
|
if (effectivePlanId) {
|
|
@@ -5695,10 +6156,65 @@ Created with Social Neuron`;
|
|
|
5695
6156
|
`Summary: ${passed}/${workingPlan.posts.length} passed quality check`
|
|
5696
6157
|
);
|
|
5697
6158
|
return {
|
|
5698
|
-
content: [{ type: "text", text: lines2.join("\n") }],
|
|
5699
|
-
isError: false
|
|
6159
|
+
content: [{ type: "text", text: lines2.join("\n") }],
|
|
6160
|
+
isError: false
|
|
6161
|
+
};
|
|
6162
|
+
}
|
|
6163
|
+
const embeddedAccountIds = workingPlan.account_ids ?? {};
|
|
6164
|
+
for (const [platform2, accountId] of Object.entries(account_ids ?? {})) {
|
|
6165
|
+
if (embeddedAccountIds[platform2] && embeddedAccountIds[platform2] !== accountId) {
|
|
6166
|
+
return {
|
|
6167
|
+
content: [
|
|
6168
|
+
{
|
|
6169
|
+
type: "text",
|
|
6170
|
+
text: `Conflicting account_ids values were supplied for ${platform2}.`
|
|
6171
|
+
}
|
|
6172
|
+
],
|
|
6173
|
+
isError: true
|
|
6174
|
+
};
|
|
6175
|
+
}
|
|
6176
|
+
}
|
|
6177
|
+
const requestedPlanAccountIds = {
|
|
6178
|
+
...embeddedAccountIds,
|
|
6179
|
+
...account_ids ?? {}
|
|
6180
|
+
};
|
|
6181
|
+
for (const post of workingPlan.posts) {
|
|
6182
|
+
if (!post.connected_account_id) continue;
|
|
6183
|
+
const key = post.platform.toLowerCase() === "x" ? "twitter" : post.platform.toLowerCase();
|
|
6184
|
+
const existing = requestedPlanAccountIds[key];
|
|
6185
|
+
if (existing && existing !== post.connected_account_id) {
|
|
6186
|
+
return {
|
|
6187
|
+
content: [
|
|
6188
|
+
{
|
|
6189
|
+
type: "text",
|
|
6190
|
+
text: `Plan contains conflicting connected_account_id values for ${post.platform}. Use separate plans when scheduling the same platform through different accounts.`
|
|
6191
|
+
}
|
|
6192
|
+
],
|
|
6193
|
+
isError: true
|
|
6194
|
+
};
|
|
6195
|
+
}
|
|
6196
|
+
requestedPlanAccountIds[key] = post.connected_account_id;
|
|
6197
|
+
}
|
|
6198
|
+
const planPlatforms = Array.from(
|
|
6199
|
+
new Set(workingPlan.posts.map((post) => post.platform))
|
|
6200
|
+
);
|
|
6201
|
+
const planRouting = await resolveConnectedAccountRouting({
|
|
6202
|
+
projectId: effectiveProjectId,
|
|
6203
|
+
platforms: planPlatforms,
|
|
6204
|
+
requestedAccountIds: requestedPlanAccountIds
|
|
6205
|
+
});
|
|
6206
|
+
if (planRouting.error || !planRouting.connectedAccountIds) {
|
|
6207
|
+
return {
|
|
6208
|
+
content: [
|
|
6209
|
+
{
|
|
6210
|
+
type: "text",
|
|
6211
|
+
text: `Cannot schedule plan \u2014 ${planRouting.error ?? "exact connected-account routing could not be established."}`
|
|
6212
|
+
}
|
|
6213
|
+
],
|
|
6214
|
+
isError: true
|
|
5700
6215
|
};
|
|
5701
6216
|
}
|
|
6217
|
+
const verifiedPlanAccountIds = planRouting.connectedAccountIds;
|
|
5702
6218
|
let scheduled = 0;
|
|
5703
6219
|
let failed = 0;
|
|
5704
6220
|
const results = [];
|
|
@@ -5727,7 +6243,7 @@ Created with Social Neuron`;
|
|
|
5727
6243
|
retryable: false
|
|
5728
6244
|
};
|
|
5729
6245
|
}
|
|
5730
|
-
const normalizedPlatform =
|
|
6246
|
+
const normalizedPlatform = PLATFORM_CASE_MAP2[post.platform.toLowerCase()] ?? post.platform;
|
|
5731
6247
|
const idempotencyKey = buildIdempotencyKey(post);
|
|
5732
6248
|
const { data, error } = await callEdgeFunction(
|
|
5733
6249
|
"schedule-post",
|
|
@@ -5738,6 +6254,13 @@ Created with Social Neuron`;
|
|
|
5738
6254
|
mediaUrl: post.media_url,
|
|
5739
6255
|
scheduledAt: post.schedule_at,
|
|
5740
6256
|
hashtags: post.hashtags,
|
|
6257
|
+
...effectiveProjectId ? {
|
|
6258
|
+
projectId: effectiveProjectId,
|
|
6259
|
+
project_id: effectiveProjectId
|
|
6260
|
+
} : {},
|
|
6261
|
+
connectedAccountIds: {
|
|
6262
|
+
[normalizedPlatform]: verifiedPlanAccountIds[normalizedPlatform]
|
|
6263
|
+
},
|
|
5741
6264
|
...effectivePlanId ? { planId: effectivePlanId } : {},
|
|
5742
6265
|
idempotencyKey
|
|
5743
6266
|
},
|
|
@@ -5796,7 +6319,7 @@ Created with Social Neuron`;
|
|
|
5796
6319
|
const platformBatches = Array.from(grouped.entries()).map(
|
|
5797
6320
|
async ([platform2, platformPosts]) => {
|
|
5798
6321
|
const platformResults = [];
|
|
5799
|
-
const batches = chunk(platformPosts,
|
|
6322
|
+
const batches = chunk(platformPosts, effectiveBatchSize);
|
|
5800
6323
|
for (const batch of batches) {
|
|
5801
6324
|
const settled = await Promise.allSettled(
|
|
5802
6325
|
batch.map((post) => scheduleOne(post))
|
|
@@ -5857,7 +6380,8 @@ Created with Social Neuron`;
|
|
|
5857
6380
|
total_posts: workingPlan.posts.length,
|
|
5858
6381
|
scheduled,
|
|
5859
6382
|
failed
|
|
5860
|
-
}
|
|
6383
|
+
},
|
|
6384
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
5861
6385
|
}),
|
|
5862
6386
|
null,
|
|
5863
6387
|
2
|
|
@@ -5881,6 +6405,9 @@ Created with Social Neuron`;
|
|
|
5881
6405
|
lines.push(
|
|
5882
6406
|
`Scheduled: ${scheduled}/${workingPlan.posts.length} | Failed: ${failed}/${workingPlan.posts.length}`
|
|
5883
6407
|
);
|
|
6408
|
+
if (projectAutoResolvedNote) {
|
|
6409
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
6410
|
+
}
|
|
5884
6411
|
return {
|
|
5885
6412
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
5886
6413
|
isError: failed > 0
|
|
@@ -5902,6 +6429,7 @@ Created with Social Neuron`;
|
|
|
5902
6429
|
}
|
|
5903
6430
|
|
|
5904
6431
|
// src/tools/media.ts
|
|
6432
|
+
init_edge_function();
|
|
5905
6433
|
import { z as z4 } from "zod";
|
|
5906
6434
|
import { readFile } from "node:fs/promises";
|
|
5907
6435
|
import { basename, extname } from "node:path";
|
|
@@ -6371,6 +6899,7 @@ function registerMediaTools(server) {
|
|
|
6371
6899
|
|
|
6372
6900
|
// src/tools/analytics.ts
|
|
6373
6901
|
init_supabase();
|
|
6902
|
+
init_edge_function();
|
|
6374
6903
|
import { z as z5 } from "zod";
|
|
6375
6904
|
function asEnvelope4(data) {
|
|
6376
6905
|
return {
|
|
@@ -6402,15 +6931,35 @@ function registerAnalyticsTools(server) {
|
|
|
6402
6931
|
content_id: z5.string().uuid().optional().describe(
|
|
6403
6932
|
"Filter to a specific content_history ID to see performance of one piece of content."
|
|
6404
6933
|
),
|
|
6405
|
-
project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
6934
|
+
project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
|
|
6406
6935
|
limit: z5.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
|
|
6407
6936
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6408
6937
|
},
|
|
6409
|
-
async ({
|
|
6938
|
+
async ({
|
|
6939
|
+
platform: platform2,
|
|
6940
|
+
days,
|
|
6941
|
+
content_id,
|
|
6942
|
+
project_id,
|
|
6943
|
+
limit,
|
|
6944
|
+
response_format
|
|
6945
|
+
}) => {
|
|
6410
6946
|
const format = response_format ?? "text";
|
|
6411
6947
|
const lookbackDays = days ?? 30;
|
|
6412
6948
|
const maxPosts = limit ?? 20;
|
|
6413
|
-
const
|
|
6949
|
+
const projectResolution = await resolveProjectStrict(project_id);
|
|
6950
|
+
if (!projectResolution.projectId) {
|
|
6951
|
+
return {
|
|
6952
|
+
content: [
|
|
6953
|
+
{
|
|
6954
|
+
type: "text",
|
|
6955
|
+
text: projectResolution.error ?? "A project_id is required to fetch analytics. Configure an explicit project or use an API key scoped to exactly one project."
|
|
6956
|
+
}
|
|
6957
|
+
],
|
|
6958
|
+
isError: true
|
|
6959
|
+
};
|
|
6960
|
+
}
|
|
6961
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
6962
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
6414
6963
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
6415
6964
|
action: "analytics",
|
|
6416
6965
|
platform: platform2,
|
|
@@ -6421,11 +6970,17 @@ function registerAnalyticsTools(server) {
|
|
|
6421
6970
|
limit: Math.min(maxPosts * 5, 100),
|
|
6422
6971
|
latestOnly: true,
|
|
6423
6972
|
contentId: content_id,
|
|
6424
|
-
|
|
6973
|
+
projectId: resolvedProjectId,
|
|
6974
|
+
project_id: resolvedProjectId
|
|
6425
6975
|
});
|
|
6426
6976
|
if (efError) {
|
|
6427
6977
|
return {
|
|
6428
|
-
content: [
|
|
6978
|
+
content: [
|
|
6979
|
+
{
|
|
6980
|
+
type: "text",
|
|
6981
|
+
text: `Failed to fetch analytics: ${efError}`
|
|
6982
|
+
}
|
|
6983
|
+
],
|
|
6429
6984
|
isError: true
|
|
6430
6985
|
};
|
|
6431
6986
|
}
|
|
@@ -6445,7 +7000,8 @@ function registerAnalyticsTools(server) {
|
|
|
6445
7000
|
totalViews: 0,
|
|
6446
7001
|
totalEngagement: 0,
|
|
6447
7002
|
postCount: 0,
|
|
6448
|
-
posts: []
|
|
7003
|
+
posts: [],
|
|
7004
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
6449
7005
|
});
|
|
6450
7006
|
return {
|
|
6451
7007
|
structuredContent,
|
|
@@ -6461,7 +7017,9 @@ function registerAnalyticsTools(server) {
|
|
|
6461
7017
|
content: [
|
|
6462
7018
|
{
|
|
6463
7019
|
type: "text",
|
|
6464
|
-
text: `No analytics data found for the last ${lookbackDays} days${platform2 ? ` on ${platform2}` : ""}.`
|
|
7020
|
+
text: `No analytics data found for the last ${lookbackDays} days${platform2 ? ` on ${platform2}` : ""}.` + (projectAutoResolvedNote ? `
|
|
7021
|
+
|
|
7022
|
+
Note: ${projectAutoResolvedNote}` : "")
|
|
6465
7023
|
}
|
|
6466
7024
|
]
|
|
6467
7025
|
};
|
|
@@ -6493,20 +7051,27 @@ function registerAnalyticsTools(server) {
|
|
|
6493
7051
|
postCount: posts.length,
|
|
6494
7052
|
posts
|
|
6495
7053
|
};
|
|
6496
|
-
return formatAnalytics(
|
|
7054
|
+
return formatAnalytics(
|
|
7055
|
+
summary,
|
|
7056
|
+
lookbackDays,
|
|
7057
|
+
format,
|
|
7058
|
+
projectAutoResolvedNote
|
|
7059
|
+
);
|
|
6497
7060
|
}
|
|
6498
7061
|
);
|
|
6499
7062
|
server.tool(
|
|
6500
7063
|
"refresh_platform_analytics",
|
|
6501
7064
|
"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.",
|
|
6502
7065
|
{
|
|
6503
|
-
project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
7066
|
+
project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
|
|
6504
7067
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6505
7068
|
},
|
|
6506
7069
|
async ({ project_id, response_format }) => {
|
|
6507
7070
|
const format = response_format ?? "text";
|
|
6508
7071
|
const userId = await getDefaultUserId();
|
|
6509
|
-
const
|
|
7072
|
+
const projectResolution = await resolveProjectStrict(project_id);
|
|
7073
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
7074
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
6510
7075
|
const rateLimit = checkRateLimit(
|
|
6511
7076
|
"posting",
|
|
6512
7077
|
`refresh_platform_analytics:${userId}:${resolvedProjectId ?? "default"}`
|
|
@@ -6522,25 +7087,48 @@ function registerAnalyticsTools(server) {
|
|
|
6522
7087
|
isError: true
|
|
6523
7088
|
};
|
|
6524
7089
|
}
|
|
7090
|
+
if (!resolvedProjectId) {
|
|
7091
|
+
return {
|
|
7092
|
+
content: [
|
|
7093
|
+
{
|
|
7094
|
+
type: "text",
|
|
7095
|
+
text: projectResolution.error ?? "A project_id is required to refresh analytics. Configure an explicit project or use an API key scoped to exactly one project."
|
|
7096
|
+
}
|
|
7097
|
+
],
|
|
7098
|
+
isError: true
|
|
7099
|
+
};
|
|
7100
|
+
}
|
|
6525
7101
|
const { data, error } = await callEdgeFunction("fetch-analytics", {
|
|
6526
7102
|
userId,
|
|
6527
|
-
|
|
7103
|
+
projectId: resolvedProjectId,
|
|
7104
|
+
project_id: resolvedProjectId
|
|
6528
7105
|
});
|
|
6529
7106
|
if (error) {
|
|
6530
7107
|
return {
|
|
6531
|
-
content: [
|
|
7108
|
+
content: [
|
|
7109
|
+
{
|
|
7110
|
+
type: "text",
|
|
7111
|
+
text: `Error refreshing analytics: ${error}`
|
|
7112
|
+
}
|
|
7113
|
+
],
|
|
6532
7114
|
isError: true
|
|
6533
7115
|
};
|
|
6534
7116
|
}
|
|
6535
7117
|
const result = data;
|
|
6536
7118
|
if (!result.success) {
|
|
6537
7119
|
return {
|
|
6538
|
-
content: [
|
|
7120
|
+
content: [
|
|
7121
|
+
{ type: "text", text: "Analytics refresh failed." }
|
|
7122
|
+
],
|
|
6539
7123
|
isError: true
|
|
6540
7124
|
};
|
|
6541
7125
|
}
|
|
6542
|
-
const queued = (result.results ?? []).filter(
|
|
6543
|
-
|
|
7126
|
+
const queued = (result.results ?? []).filter(
|
|
7127
|
+
(r) => r.status === "queued"
|
|
7128
|
+
).length;
|
|
7129
|
+
const errored = (result.results ?? []).filter(
|
|
7130
|
+
(r) => r.status === "error"
|
|
7131
|
+
).length;
|
|
6544
7132
|
const lines = [
|
|
6545
7133
|
`Analytics refresh triggered successfully.`,
|
|
6546
7134
|
` Posts processed: ${result.postsProcessed}`,
|
|
@@ -6549,13 +7137,17 @@ function registerAnalyticsTools(server) {
|
|
|
6549
7137
|
if (errored > 0) {
|
|
6550
7138
|
lines.push(` Errors: ${errored}`);
|
|
6551
7139
|
}
|
|
7140
|
+
if (projectAutoResolvedNote) {
|
|
7141
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
7142
|
+
}
|
|
6552
7143
|
if (format === "json") {
|
|
6553
7144
|
const structuredContent = asEnvelope4({
|
|
6554
7145
|
success: true,
|
|
6555
7146
|
postsProcessed: result.postsProcessed,
|
|
6556
7147
|
queued,
|
|
6557
7148
|
errored,
|
|
6558
|
-
projectId: resolvedProjectId ?? null
|
|
7149
|
+
projectId: resolvedProjectId ?? null,
|
|
7150
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
6559
7151
|
});
|
|
6560
7152
|
return {
|
|
6561
7153
|
structuredContent,
|
|
@@ -6571,12 +7163,21 @@ function registerAnalyticsTools(server) {
|
|
|
6571
7163
|
}
|
|
6572
7164
|
);
|
|
6573
7165
|
}
|
|
6574
|
-
function formatAnalytics(summary, days, format) {
|
|
6575
|
-
const structuredContent = asEnvelope4({
|
|
7166
|
+
function formatAnalytics(summary, days, format, projectAutoResolvedNote) {
|
|
7167
|
+
const structuredContent = asEnvelope4({
|
|
7168
|
+
...summary,
|
|
7169
|
+
days,
|
|
7170
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
7171
|
+
});
|
|
6576
7172
|
if (format === "json") {
|
|
6577
7173
|
return {
|
|
6578
7174
|
structuredContent,
|
|
6579
|
-
content: [
|
|
7175
|
+
content: [
|
|
7176
|
+
{
|
|
7177
|
+
type: "text",
|
|
7178
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
7179
|
+
}
|
|
7180
|
+
]
|
|
6580
7181
|
};
|
|
6581
7182
|
}
|
|
6582
7183
|
const lines = [
|
|
@@ -6600,6 +7201,9 @@ function formatAnalytics(summary, days, format) {
|
|
|
6600
7201
|
lines.push(line);
|
|
6601
7202
|
}
|
|
6602
7203
|
}
|
|
7204
|
+
if (projectAutoResolvedNote) {
|
|
7205
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
7206
|
+
}
|
|
6603
7207
|
return {
|
|
6604
7208
|
structuredContent,
|
|
6605
7209
|
content: [{ type: "text", text: lines.join("\n") }]
|
|
@@ -6607,8 +7211,9 @@ function formatAnalytics(summary, days, format) {
|
|
|
6607
7211
|
}
|
|
6608
7212
|
|
|
6609
7213
|
// src/tools/brand.ts
|
|
6610
|
-
|
|
7214
|
+
init_edge_function();
|
|
6611
7215
|
init_supabase();
|
|
7216
|
+
import { z as z6 } from "zod";
|
|
6612
7217
|
|
|
6613
7218
|
// src/lib/brandUrlInput.ts
|
|
6614
7219
|
var PLATFORM_ALIASES = {
|
|
@@ -7348,6 +7953,7 @@ import { z as z8 } from "zod";
|
|
|
7348
7953
|
import { resolve as resolve2 } from "node:path";
|
|
7349
7954
|
import { mkdir as mkdir2 } from "node:fs/promises";
|
|
7350
7955
|
init_supabase();
|
|
7956
|
+
init_edge_function();
|
|
7351
7957
|
var COMPOSITIONS = [
|
|
7352
7958
|
{
|
|
7353
7959
|
id: "CaptionedClip",
|
|
@@ -7699,8 +8305,9 @@ function registerRemotionTools(server) {
|
|
|
7699
8305
|
}
|
|
7700
8306
|
|
|
7701
8307
|
// src/tools/insights.ts
|
|
7702
|
-
|
|
8308
|
+
init_edge_function();
|
|
7703
8309
|
init_supabase();
|
|
8310
|
+
import { z as z9 } from "zod";
|
|
7704
8311
|
var MAX_INSIGHT_AGE_DAYS = 30;
|
|
7705
8312
|
var PLATFORM_ENUM = [
|
|
7706
8313
|
"youtube",
|
|
@@ -7956,6 +8563,8 @@ function registerInsightsTools(server) {
|
|
|
7956
8563
|
}
|
|
7957
8564
|
|
|
7958
8565
|
// src/tools/youtube-analytics.ts
|
|
8566
|
+
init_edge_function();
|
|
8567
|
+
init_supabase();
|
|
7959
8568
|
import { z as z10 } from "zod";
|
|
7960
8569
|
function asEnvelope7(data) {
|
|
7961
8570
|
return {
|
|
@@ -7977,15 +8586,64 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
7977
8586
|
start_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Start date in YYYY-MM-DD format."),
|
|
7978
8587
|
end_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("End date in YYYY-MM-DD format."),
|
|
7979
8588
|
video_id: z10.string().optional().describe('YouTube video ID. Required when action is "video".'),
|
|
7980
|
-
max_results: z10.number().min(1).max(50).optional().describe(
|
|
8589
|
+
max_results: z10.number().min(1).max(50).optional().describe(
|
|
8590
|
+
'Max videos to return for "topVideos" action. Defaults to 10.'
|
|
8591
|
+
),
|
|
8592
|
+
connected_account_id: z10.string().uuid().optional().describe(
|
|
8593
|
+
"Exact YouTube connected-account ID from list_connections. Optional when exactly one active YouTube account is bound to the resolved project \u2014 auto-resolved. Required (with a clear list of candidates) when the project has multiple YouTube accounts."
|
|
8594
|
+
),
|
|
8595
|
+
project_id: z10.string().uuid().optional().describe(
|
|
8596
|
+
"Exact brand/project ID. Defaults only when the authenticated user has one project."
|
|
8597
|
+
),
|
|
7981
8598
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
7982
8599
|
},
|
|
7983
|
-
async ({
|
|
8600
|
+
async ({
|
|
8601
|
+
action,
|
|
8602
|
+
start_date,
|
|
8603
|
+
end_date,
|
|
8604
|
+
video_id,
|
|
8605
|
+
max_results,
|
|
8606
|
+
connected_account_id,
|
|
8607
|
+
project_id,
|
|
8608
|
+
response_format
|
|
8609
|
+
}) => {
|
|
7984
8610
|
const format = response_format ?? "text";
|
|
7985
8611
|
if (action === "video" && !video_id) {
|
|
7986
8612
|
return {
|
|
7987
8613
|
content: [
|
|
7988
|
-
{
|
|
8614
|
+
{
|
|
8615
|
+
type: "text",
|
|
8616
|
+
text: 'Error: video_id is required when action is "video".'
|
|
8617
|
+
}
|
|
8618
|
+
],
|
|
8619
|
+
isError: true
|
|
8620
|
+
};
|
|
8621
|
+
}
|
|
8622
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
8623
|
+
if (!resolvedProjectId) {
|
|
8624
|
+
return {
|
|
8625
|
+
content: [
|
|
8626
|
+
{
|
|
8627
|
+
type: "text",
|
|
8628
|
+
text: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
|
|
8629
|
+
}
|
|
8630
|
+
],
|
|
8631
|
+
isError: true
|
|
8632
|
+
};
|
|
8633
|
+
}
|
|
8634
|
+
const routing = await resolveConnectedAccountRouting({
|
|
8635
|
+
projectId: resolvedProjectId,
|
|
8636
|
+
platforms: ["youtube"],
|
|
8637
|
+
requestedAccountIds: connected_account_id ? { youtube: connected_account_id } : void 0
|
|
8638
|
+
});
|
|
8639
|
+
const resolvedAccountId = routing.connectedAccountIds?.YouTube;
|
|
8640
|
+
if (routing.error || !resolvedAccountId) {
|
|
8641
|
+
return {
|
|
8642
|
+
content: [
|
|
8643
|
+
{
|
|
8644
|
+
type: "text",
|
|
8645
|
+
text: routing.error ?? "YouTube: exact connected-account routing could not be established."
|
|
8646
|
+
}
|
|
7989
8647
|
],
|
|
7990
8648
|
isError: true
|
|
7991
8649
|
};
|
|
@@ -7995,11 +8653,19 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
7995
8653
|
startDate: start_date,
|
|
7996
8654
|
endDate: end_date,
|
|
7997
8655
|
videoId: video_id,
|
|
7998
|
-
maxResults: max_results ?? 10
|
|
8656
|
+
maxResults: max_results ?? 10,
|
|
8657
|
+
projectId: resolvedProjectId,
|
|
8658
|
+
project_id: resolvedProjectId,
|
|
8659
|
+
connectedAccountId: resolvedAccountId
|
|
7999
8660
|
});
|
|
8000
8661
|
if (error) {
|
|
8001
8662
|
return {
|
|
8002
|
-
content: [
|
|
8663
|
+
content: [
|
|
8664
|
+
{
|
|
8665
|
+
type: "text",
|
|
8666
|
+
text: `YouTube Analytics error: ${error}`
|
|
8667
|
+
}
|
|
8668
|
+
],
|
|
8003
8669
|
isError: true
|
|
8004
8670
|
};
|
|
8005
8671
|
}
|
|
@@ -8012,7 +8678,12 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
8012
8678
|
{
|
|
8013
8679
|
type: "text",
|
|
8014
8680
|
text: JSON.stringify(
|
|
8015
|
-
asEnvelope7({
|
|
8681
|
+
asEnvelope7({
|
|
8682
|
+
action,
|
|
8683
|
+
startDate: start_date,
|
|
8684
|
+
endDate: end_date,
|
|
8685
|
+
analytics: a
|
|
8686
|
+
}),
|
|
8016
8687
|
null,
|
|
8017
8688
|
2
|
|
8018
8689
|
)
|
|
@@ -8039,7 +8710,10 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
8039
8710
|
if (days.length === 0) {
|
|
8040
8711
|
return {
|
|
8041
8712
|
content: [
|
|
8042
|
-
{
|
|
8713
|
+
{
|
|
8714
|
+
type: "text",
|
|
8715
|
+
text: "No daily analytics data found for this period."
|
|
8716
|
+
}
|
|
8043
8717
|
]
|
|
8044
8718
|
};
|
|
8045
8719
|
}
|
|
@@ -8062,7 +8736,10 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
8062
8736
|
]
|
|
8063
8737
|
};
|
|
8064
8738
|
}
|
|
8065
|
-
const lines = [
|
|
8739
|
+
const lines = [
|
|
8740
|
+
`YouTube Daily Analytics (${start_date} to ${end_date}):`,
|
|
8741
|
+
""
|
|
8742
|
+
];
|
|
8066
8743
|
for (const d of days) {
|
|
8067
8744
|
lines.push(
|
|
8068
8745
|
` ${d.date}: ${d.views.toLocaleString()} views, ${d.watchTimeMinutes.toLocaleString()} min watch, +${d.subscribersGained} subs, ${d.likes} likes, ${d.comments} comments`
|
|
@@ -8109,7 +8786,12 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
8109
8786
|
const videos = result.topVideos ?? [];
|
|
8110
8787
|
if (videos.length === 0) {
|
|
8111
8788
|
return {
|
|
8112
|
-
content: [
|
|
8789
|
+
content: [
|
|
8790
|
+
{
|
|
8791
|
+
type: "text",
|
|
8792
|
+
text: "No top videos found for this period."
|
|
8793
|
+
}
|
|
8794
|
+
]
|
|
8113
8795
|
};
|
|
8114
8796
|
}
|
|
8115
8797
|
if (format === "json") {
|
|
@@ -8131,7 +8813,10 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
8131
8813
|
]
|
|
8132
8814
|
};
|
|
8133
8815
|
}
|
|
8134
|
-
const lines = [
|
|
8816
|
+
const lines = [
|
|
8817
|
+
`Top ${videos.length} YouTube Videos (${start_date} to ${end_date}):`,
|
|
8818
|
+
""
|
|
8819
|
+
];
|
|
8135
8820
|
for (let i = 0; i < videos.length; i++) {
|
|
8136
8821
|
const v = videos[i];
|
|
8137
8822
|
lines.push(
|
|
@@ -8144,17 +8829,25 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
8144
8829
|
}
|
|
8145
8830
|
if (format === "json") {
|
|
8146
8831
|
return {
|
|
8147
|
-
content: [
|
|
8832
|
+
content: [
|
|
8833
|
+
{
|
|
8834
|
+
type: "text",
|
|
8835
|
+
text: JSON.stringify(asEnvelope7(result), null, 2)
|
|
8836
|
+
}
|
|
8837
|
+
]
|
|
8148
8838
|
};
|
|
8149
8839
|
}
|
|
8150
8840
|
return {
|
|
8151
|
-
content: [
|
|
8841
|
+
content: [
|
|
8842
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
8843
|
+
]
|
|
8152
8844
|
};
|
|
8153
8845
|
}
|
|
8154
8846
|
);
|
|
8155
8847
|
}
|
|
8156
8848
|
|
|
8157
8849
|
// src/tools/comments.ts
|
|
8850
|
+
init_edge_function();
|
|
8158
8851
|
import { z as z11 } from "zod";
|
|
8159
8852
|
init_supabase();
|
|
8160
8853
|
function asEnvelope8(data) {
|
|
@@ -8166,6 +8859,32 @@ function asEnvelope8(data) {
|
|
|
8166
8859
|
data
|
|
8167
8860
|
};
|
|
8168
8861
|
}
|
|
8862
|
+
async function exactYouTubeRoute(projectId, connectedAccountId) {
|
|
8863
|
+
const resolvedProjectId = projectId ?? await getDefaultProjectId() ?? void 0;
|
|
8864
|
+
if (!resolvedProjectId) {
|
|
8865
|
+
return {
|
|
8866
|
+
error: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
|
|
8867
|
+
};
|
|
8868
|
+
}
|
|
8869
|
+
const routing = await resolveConnectedAccountRouting({
|
|
8870
|
+
projectId: resolvedProjectId,
|
|
8871
|
+
platforms: ["youtube"],
|
|
8872
|
+
requestedAccountIds: connectedAccountId ? { youtube: connectedAccountId } : void 0
|
|
8873
|
+
});
|
|
8874
|
+
const resolvedAccountId = routing.connectedAccountIds?.YouTube;
|
|
8875
|
+
if (routing.error || !resolvedAccountId) {
|
|
8876
|
+
return {
|
|
8877
|
+
error: routing.error ?? "YouTube: exact connected-account routing could not be established."
|
|
8878
|
+
};
|
|
8879
|
+
}
|
|
8880
|
+
return {
|
|
8881
|
+
projectId: resolvedProjectId,
|
|
8882
|
+
connectedAccountId: resolvedAccountId
|
|
8883
|
+
};
|
|
8884
|
+
}
|
|
8885
|
+
var PROJECT_ID_SCHEMA = z11.string().uuid().optional().describe(
|
|
8886
|
+
"Exact brand/project ID. Defaults only when the authenticated user has one project."
|
|
8887
|
+
);
|
|
8169
8888
|
function registerCommentsTools(server) {
|
|
8170
8889
|
server.tool(
|
|
8171
8890
|
"list_comments",
|
|
@@ -8178,19 +8897,42 @@ function registerCommentsTools(server) {
|
|
|
8178
8897
|
page_token: z11.string().optional().describe(
|
|
8179
8898
|
"Pagination cursor from previous list_comments response nextPageToken field. Omit for first page of results."
|
|
8180
8899
|
),
|
|
8900
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
8901
|
+
"Exact YouTube connected-account ID from list_connections. Optional when exactly one active YouTube account is bound to the resolved project \u2014 auto-resolved. Required (with a clear list of candidates) when the project has multiple YouTube accounts."
|
|
8902
|
+
),
|
|
8903
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
8181
8904
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8182
8905
|
},
|
|
8183
|
-
async ({
|
|
8906
|
+
async ({
|
|
8907
|
+
video_id,
|
|
8908
|
+
max_results,
|
|
8909
|
+
page_token,
|
|
8910
|
+
connected_account_id,
|
|
8911
|
+
project_id,
|
|
8912
|
+
response_format
|
|
8913
|
+
}) => {
|
|
8184
8914
|
const format = response_format ?? "text";
|
|
8915
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
8916
|
+
if ("error" in route) {
|
|
8917
|
+
return {
|
|
8918
|
+
content: [{ type: "text", text: route.error }],
|
|
8919
|
+
isError: true
|
|
8920
|
+
};
|
|
8921
|
+
}
|
|
8185
8922
|
const { data, error } = await callEdgeFunction("youtube-comments", {
|
|
8186
8923
|
action: "list",
|
|
8187
8924
|
videoId: video_id,
|
|
8188
8925
|
maxResults: max_results ?? 50,
|
|
8189
|
-
pageToken: page_token
|
|
8926
|
+
pageToken: page_token,
|
|
8927
|
+
projectId: route.projectId,
|
|
8928
|
+
project_id: route.projectId,
|
|
8929
|
+
connectedAccountId: route.connectedAccountId
|
|
8190
8930
|
});
|
|
8191
8931
|
if (error) {
|
|
8192
8932
|
return {
|
|
8193
|
-
content: [
|
|
8933
|
+
content: [
|
|
8934
|
+
{ type: "text", text: `Error listing comments: ${error}` }
|
|
8935
|
+
],
|
|
8194
8936
|
isError: true
|
|
8195
8937
|
};
|
|
8196
8938
|
}
|
|
@@ -8202,7 +8944,10 @@ function registerCommentsTools(server) {
|
|
|
8202
8944
|
{
|
|
8203
8945
|
type: "text",
|
|
8204
8946
|
text: JSON.stringify(
|
|
8205
|
-
asEnvelope8({
|
|
8947
|
+
asEnvelope8({
|
|
8948
|
+
comments,
|
|
8949
|
+
nextPageToken: result.nextPageToken ?? null
|
|
8950
|
+
}),
|
|
8206
8951
|
null,
|
|
8207
8952
|
2
|
|
8208
8953
|
)
|
|
@@ -8239,12 +8984,31 @@ function registerCommentsTools(server) {
|
|
|
8239
8984
|
"reply_to_comment",
|
|
8240
8985
|
"Reply to a YouTube comment. Get the parent_id from list_comments results. Reply appears as the authenticated channel. Use for community engagement after checking list_comments for questions or feedback.",
|
|
8241
8986
|
{
|
|
8242
|
-
parent_id: z11.string().describe(
|
|
8987
|
+
parent_id: z11.string().describe(
|
|
8988
|
+
"The ID of the parent comment to reply to (from list_comments)."
|
|
8989
|
+
),
|
|
8243
8990
|
text: z11.string().min(1).describe("The reply text."),
|
|
8991
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
8992
|
+
"Exact YouTube connected-account ID from list_connections. Optional when exactly one active YouTube account is bound to the resolved project \u2014 auto-resolved. Required (with a clear list of candidates) when the project has multiple YouTube accounts."
|
|
8993
|
+
),
|
|
8994
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
8244
8995
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8245
8996
|
},
|
|
8246
|
-
async ({
|
|
8997
|
+
async ({
|
|
8998
|
+
parent_id,
|
|
8999
|
+
text,
|
|
9000
|
+
connected_account_id,
|
|
9001
|
+
project_id,
|
|
9002
|
+
response_format
|
|
9003
|
+
}) => {
|
|
8247
9004
|
const format = response_format ?? "text";
|
|
9005
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
9006
|
+
if ("error" in route) {
|
|
9007
|
+
return {
|
|
9008
|
+
content: [{ type: "text", text: route.error }],
|
|
9009
|
+
isError: true
|
|
9010
|
+
};
|
|
9011
|
+
}
|
|
8248
9012
|
const userId = await getDefaultUserId();
|
|
8249
9013
|
const rateLimit = checkRateLimit("posting", `reply_to_comment:${userId}`);
|
|
8250
9014
|
if (!rateLimit.allowed) {
|
|
@@ -8261,18 +9025,31 @@ function registerCommentsTools(server) {
|
|
|
8261
9025
|
const { data, error } = await callEdgeFunction("youtube-comments", {
|
|
8262
9026
|
action: "reply",
|
|
8263
9027
|
parentId: parent_id,
|
|
8264
|
-
text
|
|
9028
|
+
text,
|
|
9029
|
+
projectId: route.projectId,
|
|
9030
|
+
project_id: route.projectId,
|
|
9031
|
+
connectedAccountId: route.connectedAccountId
|
|
8265
9032
|
});
|
|
8266
9033
|
if (error) {
|
|
8267
9034
|
return {
|
|
8268
|
-
content: [
|
|
9035
|
+
content: [
|
|
9036
|
+
{
|
|
9037
|
+
type: "text",
|
|
9038
|
+
text: `Error replying to comment: ${error}`
|
|
9039
|
+
}
|
|
9040
|
+
],
|
|
8269
9041
|
isError: true
|
|
8270
9042
|
};
|
|
8271
9043
|
}
|
|
8272
9044
|
const result = data;
|
|
8273
9045
|
if (format === "json") {
|
|
8274
9046
|
return {
|
|
8275
|
-
content: [
|
|
9047
|
+
content: [
|
|
9048
|
+
{
|
|
9049
|
+
type: "text",
|
|
9050
|
+
text: JSON.stringify(asEnvelope8(result), null, 2)
|
|
9051
|
+
}
|
|
9052
|
+
]
|
|
8276
9053
|
};
|
|
8277
9054
|
}
|
|
8278
9055
|
return {
|
|
@@ -8293,10 +9070,27 @@ function registerCommentsTools(server) {
|
|
|
8293
9070
|
{
|
|
8294
9071
|
video_id: z11.string().describe("The YouTube video ID to comment on."),
|
|
8295
9072
|
text: z11.string().min(1).describe("The comment text."),
|
|
9073
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
9074
|
+
"Exact YouTube connected-account ID from list_connections. Optional when exactly one active YouTube account is bound to the resolved project \u2014 auto-resolved. Required (with a clear list of candidates) when the project has multiple YouTube accounts."
|
|
9075
|
+
),
|
|
9076
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
8296
9077
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8297
9078
|
},
|
|
8298
|
-
async ({
|
|
9079
|
+
async ({
|
|
9080
|
+
video_id,
|
|
9081
|
+
text,
|
|
9082
|
+
connected_account_id,
|
|
9083
|
+
project_id,
|
|
9084
|
+
response_format
|
|
9085
|
+
}) => {
|
|
8299
9086
|
const format = response_format ?? "text";
|
|
9087
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
9088
|
+
if ("error" in route) {
|
|
9089
|
+
return {
|
|
9090
|
+
content: [{ type: "text", text: route.error }],
|
|
9091
|
+
isError: true
|
|
9092
|
+
};
|
|
9093
|
+
}
|
|
8300
9094
|
const userId = await getDefaultUserId();
|
|
8301
9095
|
const rateLimit = checkRateLimit("posting", `post_comment:${userId}`);
|
|
8302
9096
|
if (!rateLimit.allowed) {
|
|
@@ -8313,18 +9107,28 @@ function registerCommentsTools(server) {
|
|
|
8313
9107
|
const { data, error } = await callEdgeFunction("youtube-comments", {
|
|
8314
9108
|
action: "post",
|
|
8315
9109
|
videoId: video_id,
|
|
8316
|
-
text
|
|
9110
|
+
text,
|
|
9111
|
+
projectId: route.projectId,
|
|
9112
|
+
project_id: route.projectId,
|
|
9113
|
+
connectedAccountId: route.connectedAccountId
|
|
8317
9114
|
});
|
|
8318
9115
|
if (error) {
|
|
8319
9116
|
return {
|
|
8320
|
-
content: [
|
|
9117
|
+
content: [
|
|
9118
|
+
{ type: "text", text: `Error posting comment: ${error}` }
|
|
9119
|
+
],
|
|
8321
9120
|
isError: true
|
|
8322
9121
|
};
|
|
8323
9122
|
}
|
|
8324
9123
|
const result = data;
|
|
8325
9124
|
if (format === "json") {
|
|
8326
9125
|
return {
|
|
8327
|
-
content: [
|
|
9126
|
+
content: [
|
|
9127
|
+
{
|
|
9128
|
+
type: "text",
|
|
9129
|
+
text: JSON.stringify(asEnvelope8(result), null, 2)
|
|
9130
|
+
}
|
|
9131
|
+
]
|
|
8328
9132
|
};
|
|
8329
9133
|
}
|
|
8330
9134
|
return {
|
|
@@ -8345,10 +9149,27 @@ function registerCommentsTools(server) {
|
|
|
8345
9149
|
{
|
|
8346
9150
|
comment_id: z11.string().describe("The comment ID to moderate."),
|
|
8347
9151
|
moderation_status: z11.enum(["published", "rejected"]).describe('"published" to approve, "rejected" to hide.'),
|
|
9152
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
9153
|
+
"Exact YouTube connected-account ID from list_connections. Optional when exactly one active YouTube account is bound to the resolved project \u2014 auto-resolved. Required (with a clear list of candidates) when the project has multiple YouTube accounts."
|
|
9154
|
+
),
|
|
9155
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
8348
9156
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8349
9157
|
},
|
|
8350
|
-
async ({
|
|
9158
|
+
async ({
|
|
9159
|
+
comment_id,
|
|
9160
|
+
moderation_status,
|
|
9161
|
+
connected_account_id,
|
|
9162
|
+
project_id,
|
|
9163
|
+
response_format
|
|
9164
|
+
}) => {
|
|
8351
9165
|
const format = response_format ?? "text";
|
|
9166
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
9167
|
+
if ("error" in route) {
|
|
9168
|
+
return {
|
|
9169
|
+
content: [{ type: "text", text: route.error }],
|
|
9170
|
+
isError: true
|
|
9171
|
+
};
|
|
9172
|
+
}
|
|
8352
9173
|
const userId = await getDefaultUserId();
|
|
8353
9174
|
const rateLimit = checkRateLimit("posting", `moderate_comment:${userId}`);
|
|
8354
9175
|
if (!rateLimit.allowed) {
|
|
@@ -8365,11 +9186,19 @@ function registerCommentsTools(server) {
|
|
|
8365
9186
|
const { error } = await callEdgeFunction("youtube-comments", {
|
|
8366
9187
|
action: "moderate",
|
|
8367
9188
|
commentId: comment_id,
|
|
8368
|
-
moderationStatus: moderation_status
|
|
9189
|
+
moderationStatus: moderation_status,
|
|
9190
|
+
projectId: route.projectId,
|
|
9191
|
+
project_id: route.projectId,
|
|
9192
|
+
connectedAccountId: route.connectedAccountId
|
|
8369
9193
|
});
|
|
8370
9194
|
if (error) {
|
|
8371
9195
|
return {
|
|
8372
|
-
content: [
|
|
9196
|
+
content: [
|
|
9197
|
+
{
|
|
9198
|
+
type: "text",
|
|
9199
|
+
text: `Error moderating comment: ${error}`
|
|
9200
|
+
}
|
|
9201
|
+
],
|
|
8373
9202
|
isError: true
|
|
8374
9203
|
};
|
|
8375
9204
|
}
|
|
@@ -8406,10 +9235,26 @@ function registerCommentsTools(server) {
|
|
|
8406
9235
|
"Delete a YouTube comment. Only works for comments owned by the authenticated channel.",
|
|
8407
9236
|
{
|
|
8408
9237
|
comment_id: z11.string().describe("The comment ID to delete."),
|
|
9238
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
9239
|
+
"Exact YouTube connected-account ID from list_connections. Optional when exactly one active YouTube account is bound to the resolved project \u2014 auto-resolved. Required (with a clear list of candidates) when the project has multiple YouTube accounts."
|
|
9240
|
+
),
|
|
9241
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
8409
9242
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8410
9243
|
},
|
|
8411
|
-
async ({
|
|
9244
|
+
async ({
|
|
9245
|
+
comment_id,
|
|
9246
|
+
connected_account_id,
|
|
9247
|
+
project_id,
|
|
9248
|
+
response_format
|
|
9249
|
+
}) => {
|
|
8412
9250
|
const format = response_format ?? "text";
|
|
9251
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
9252
|
+
if ("error" in route) {
|
|
9253
|
+
return {
|
|
9254
|
+
content: [{ type: "text", text: route.error }],
|
|
9255
|
+
isError: true
|
|
9256
|
+
};
|
|
9257
|
+
}
|
|
8413
9258
|
const userId = await getDefaultUserId();
|
|
8414
9259
|
const rateLimit = checkRateLimit("posting", `delete_comment:${userId}`);
|
|
8415
9260
|
if (!rateLimit.allowed) {
|
|
@@ -8425,11 +9270,16 @@ function registerCommentsTools(server) {
|
|
|
8425
9270
|
}
|
|
8426
9271
|
const { error } = await callEdgeFunction("youtube-comments", {
|
|
8427
9272
|
action: "delete",
|
|
8428
|
-
commentId: comment_id
|
|
9273
|
+
commentId: comment_id,
|
|
9274
|
+
projectId: route.projectId,
|
|
9275
|
+
project_id: route.projectId,
|
|
9276
|
+
connectedAccountId: route.connectedAccountId
|
|
8429
9277
|
});
|
|
8430
9278
|
if (error) {
|
|
8431
9279
|
return {
|
|
8432
|
-
content: [
|
|
9280
|
+
content: [
|
|
9281
|
+
{ type: "text", text: `Error deleting comment: ${error}` }
|
|
9282
|
+
],
|
|
8433
9283
|
isError: true
|
|
8434
9284
|
};
|
|
8435
9285
|
}
|
|
@@ -8438,13 +9288,22 @@ function registerCommentsTools(server) {
|
|
|
8438
9288
|
content: [
|
|
8439
9289
|
{
|
|
8440
9290
|
type: "text",
|
|
8441
|
-
text: JSON.stringify(
|
|
9291
|
+
text: JSON.stringify(
|
|
9292
|
+
asEnvelope8({ success: true, commentId: comment_id }),
|
|
9293
|
+
null,
|
|
9294
|
+
2
|
|
9295
|
+
)
|
|
8442
9296
|
}
|
|
8443
9297
|
]
|
|
8444
9298
|
};
|
|
8445
9299
|
}
|
|
8446
9300
|
return {
|
|
8447
|
-
content: [
|
|
9301
|
+
content: [
|
|
9302
|
+
{
|
|
9303
|
+
type: "text",
|
|
9304
|
+
text: `Comment ${comment_id} deleted successfully.`
|
|
9305
|
+
}
|
|
9306
|
+
]
|
|
8448
9307
|
};
|
|
8449
9308
|
}
|
|
8450
9309
|
);
|
|
@@ -8452,6 +9311,7 @@ function registerCommentsTools(server) {
|
|
|
8452
9311
|
|
|
8453
9312
|
// src/tools/ideation-context.ts
|
|
8454
9313
|
init_supabase();
|
|
9314
|
+
init_edge_function();
|
|
8455
9315
|
import { z as z12 } from "zod";
|
|
8456
9316
|
function asEnvelope9(data) {
|
|
8457
9317
|
return {
|
|
@@ -8524,6 +9384,7 @@ function registerIdeationContextTools(server) {
|
|
|
8524
9384
|
}
|
|
8525
9385
|
|
|
8526
9386
|
// src/tools/credits.ts
|
|
9387
|
+
init_edge_function();
|
|
8527
9388
|
import { z as z13 } from "zod";
|
|
8528
9389
|
function asEnvelope10(data) {
|
|
8529
9390
|
return {
|
|
@@ -8619,6 +9480,7 @@ Assets remaining: ${payload.remainingAssets ?? "unlimited"}`
|
|
|
8619
9480
|
|
|
8620
9481
|
// src/tools/loop-summary.ts
|
|
8621
9482
|
init_supabase();
|
|
9483
|
+
init_edge_function();
|
|
8622
9484
|
import { z as z14 } from "zod";
|
|
8623
9485
|
function asEnvelope11(data) {
|
|
8624
9486
|
return {
|
|
@@ -8692,6 +9554,7 @@ Next Action: ${payload.recommendedNextAction}`
|
|
|
8692
9554
|
}
|
|
8693
9555
|
|
|
8694
9556
|
// src/tools/usage.ts
|
|
9557
|
+
init_edge_function();
|
|
8695
9558
|
import { z as z15 } from "zod";
|
|
8696
9559
|
function asEnvelope12(data) {
|
|
8697
9560
|
return {
|
|
@@ -8764,6 +9627,7 @@ ${"=".repeat(40)}
|
|
|
8764
9627
|
}
|
|
8765
9628
|
|
|
8766
9629
|
// src/tools/autopilot.ts
|
|
9630
|
+
init_edge_function();
|
|
8767
9631
|
import { z as z16 } from "zod";
|
|
8768
9632
|
function asEnvelope13(data) {
|
|
8769
9633
|
return {
|
|
@@ -9062,6 +9926,7 @@ Active: ${is_active}`
|
|
|
9062
9926
|
}
|
|
9063
9927
|
|
|
9064
9928
|
// src/tools/recipes.ts
|
|
9929
|
+
init_edge_function();
|
|
9065
9930
|
import { z as z17 } from "zod";
|
|
9066
9931
|
function asEnvelope14(data) {
|
|
9067
9932
|
return {
|
|
@@ -9331,6 +10196,7 @@ ${JSON.stringify(run.outputs, null, 2)}
|
|
|
9331
10196
|
import { z as z18 } from "zod";
|
|
9332
10197
|
|
|
9333
10198
|
// src/lib/urlExtraction.ts
|
|
10199
|
+
init_edge_function();
|
|
9334
10200
|
function classifyYouTubeUrl(url) {
|
|
9335
10201
|
if (/youtube\.com\/watch|youtu\.be\//.test(url)) return "video";
|
|
9336
10202
|
if (/youtube\.com\/@/.test(url)) return "channel";
|
|
@@ -10194,6 +11060,7 @@ function registerVisualQualityTools(server) {
|
|
|
10194
11060
|
}
|
|
10195
11061
|
|
|
10196
11062
|
// src/tools/planning.ts
|
|
11063
|
+
init_edge_function();
|
|
10197
11064
|
import { z as z21 } from "zod";
|
|
10198
11065
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10199
11066
|
init_supabase();
|
|
@@ -10835,8 +11702,9 @@ ${rawText.slice(0, 1e3)}`
|
|
|
10835
11702
|
}
|
|
10836
11703
|
|
|
10837
11704
|
// src/tools/plan-approvals.ts
|
|
10838
|
-
|
|
11705
|
+
init_edge_function();
|
|
10839
11706
|
init_supabase();
|
|
11707
|
+
import { z as z22 } from "zod";
|
|
10840
11708
|
function asEnvelope19(data) {
|
|
10841
11709
|
return {
|
|
10842
11710
|
_meta: {
|
|
@@ -11065,6 +11933,14 @@ init_supabase();
|
|
|
11065
11933
|
init_request_context();
|
|
11066
11934
|
var KNOWLEDGE_BASE_URL = "https://socialneuron.com/for-developers";
|
|
11067
11935
|
var KNOWLEDGE_SEARCH_LIMIT = 10;
|
|
11936
|
+
function isHostedTransport() {
|
|
11937
|
+
return process.env.MCP_TRANSPORT === "http";
|
|
11938
|
+
}
|
|
11939
|
+
function isPubliclyDiscoverable(tool) {
|
|
11940
|
+
if (tool.internal || tool.hiddenFromPublicCount) return false;
|
|
11941
|
+
if (tool.localOnly && isHostedTransport()) return false;
|
|
11942
|
+
return true;
|
|
11943
|
+
}
|
|
11068
11944
|
var SearchOutputSchema = {
|
|
11069
11945
|
results: z23.array(
|
|
11070
11946
|
z23.object({
|
|
@@ -11137,7 +12013,8 @@ function toolKnowledgeDocument(tool) {
|
|
|
11137
12013
|
if (tool.task_intent) lines.push(`Task intent: ${tool.task_intent}`);
|
|
11138
12014
|
if (tool.use_when) lines.push(`Use when: ${tool.use_when}`);
|
|
11139
12015
|
if (tool.avoid_when) lines.push(`Avoid when: ${tool.avoid_when}`);
|
|
11140
|
-
if (tool.next_tools?.length)
|
|
12016
|
+
if (tool.next_tools?.length)
|
|
12017
|
+
lines.push(`Common next tools: ${tool.next_tools.join(", ")}`);
|
|
11141
12018
|
return {
|
|
11142
12019
|
id: `tool:${tool.name}`,
|
|
11143
12020
|
title: `MCP tool: ${tool.name}`,
|
|
@@ -11154,9 +12031,7 @@ function toolKnowledgeDocument(tool) {
|
|
|
11154
12031
|
function getKnowledgeDocuments() {
|
|
11155
12032
|
return [
|
|
11156
12033
|
...STATIC_KNOWLEDGE_DOCUMENTS,
|
|
11157
|
-
...TOOL_CATALOG.filter(
|
|
11158
|
-
toolKnowledgeDocument
|
|
11159
|
-
)
|
|
12034
|
+
...TOOL_CATALOG.filter(isPubliclyDiscoverable).map(toolKnowledgeDocument)
|
|
11160
12035
|
];
|
|
11161
12036
|
}
|
|
11162
12037
|
function tokenize(input) {
|
|
@@ -11210,7 +12085,9 @@ function registerDiscoveryTools(server) {
|
|
|
11210
12085
|
};
|
|
11211
12086
|
return {
|
|
11212
12087
|
structuredContent,
|
|
11213
|
-
content: [
|
|
12088
|
+
content: [
|
|
12089
|
+
{ type: "text", text: JSON.stringify(structuredContent) }
|
|
12090
|
+
]
|
|
11214
12091
|
};
|
|
11215
12092
|
}
|
|
11216
12093
|
);
|
|
@@ -11231,10 +12108,14 @@ function registerDiscoveryTools(server) {
|
|
|
11231
12108
|
}
|
|
11232
12109
|
},
|
|
11233
12110
|
async ({ id }) => {
|
|
11234
|
-
const doc = getKnowledgeDocuments().find(
|
|
12111
|
+
const doc = getKnowledgeDocuments().find(
|
|
12112
|
+
(candidate) => candidate.id === id
|
|
12113
|
+
);
|
|
11235
12114
|
if (!doc) {
|
|
11236
12115
|
return {
|
|
11237
|
-
content: [
|
|
12116
|
+
content: [
|
|
12117
|
+
{ type: "text", text: `Document not found: ${id}` }
|
|
12118
|
+
],
|
|
11238
12119
|
isError: true
|
|
11239
12120
|
};
|
|
11240
12121
|
}
|
|
@@ -11247,7 +12128,9 @@ function registerDiscoveryTools(server) {
|
|
|
11247
12128
|
};
|
|
11248
12129
|
return {
|
|
11249
12130
|
structuredContent,
|
|
11250
|
-
content: [
|
|
12131
|
+
content: [
|
|
12132
|
+
{ type: "text", text: JSON.stringify(structuredContent) }
|
|
12133
|
+
]
|
|
11251
12134
|
};
|
|
11252
12135
|
}
|
|
11253
12136
|
);
|
|
@@ -11256,7 +12139,9 @@ function registerDiscoveryTools(server) {
|
|
|
11256
12139
|
'Search available tools by name, description, module, or scope. Use "name" detail (~50 tokens) for quick lookup, "summary" (~500 tokens) for descriptions, "full" for complete input schemas. Start here if unsure which tool to call \u2014 filter by module (e.g. "planning", "content", "analytics") to narrow results.',
|
|
11257
12140
|
{
|
|
11258
12141
|
query: z23.string().optional().describe("Search query to filter tools by name or description"),
|
|
11259
|
-
module: z23.string().optional().describe(
|
|
12142
|
+
module: z23.string().optional().describe(
|
|
12143
|
+
'Filter by module name (e.g. "planning", "content", "analytics")'
|
|
12144
|
+
),
|
|
11260
12145
|
scope: z23.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
|
|
11261
12146
|
detail: z23.enum(["name", "summary", "full"]).default("summary").describe(
|
|
11262
12147
|
'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
|
|
@@ -11273,14 +12158,18 @@ function registerDiscoveryTools(server) {
|
|
|
11273
12158
|
if (query) {
|
|
11274
12159
|
results = searchTools(query);
|
|
11275
12160
|
}
|
|
11276
|
-
results = results.filter(
|
|
12161
|
+
results = results.filter(isPubliclyDiscoverable);
|
|
11277
12162
|
if (module) {
|
|
11278
12163
|
const moduleTools = getToolsByModule(module);
|
|
11279
|
-
results = results.filter(
|
|
12164
|
+
results = results.filter(
|
|
12165
|
+
(t) => moduleTools.some((mt) => mt.name === t.name)
|
|
12166
|
+
);
|
|
11280
12167
|
}
|
|
11281
12168
|
if (scope) {
|
|
11282
12169
|
const scopeTools = getToolsByScope(scope);
|
|
11283
|
-
results = results.filter(
|
|
12170
|
+
results = results.filter(
|
|
12171
|
+
(t) => scopeTools.some((st) => st.name === t.name)
|
|
12172
|
+
);
|
|
11284
12173
|
}
|
|
11285
12174
|
if (available_only) {
|
|
11286
12175
|
results = results.filter(isAvailable);
|
|
@@ -11318,7 +12207,12 @@ function registerDiscoveryTools(server) {
|
|
|
11318
12207
|
text: JSON.stringify(
|
|
11319
12208
|
{
|
|
11320
12209
|
toolCount: results.length,
|
|
11321
|
-
...hasKnownScopes ? {
|
|
12210
|
+
...hasKnownScopes ? {
|
|
12211
|
+
scopes: {
|
|
12212
|
+
available: currentScopes,
|
|
12213
|
+
unavailable_matches: unavailableCount
|
|
12214
|
+
}
|
|
12215
|
+
} : {},
|
|
11322
12216
|
tools: output
|
|
11323
12217
|
},
|
|
11324
12218
|
null,
|
|
@@ -11332,11 +12226,15 @@ function registerDiscoveryTools(server) {
|
|
|
11332
12226
|
}
|
|
11333
12227
|
|
|
11334
12228
|
// src/tools/pipeline.ts
|
|
12229
|
+
init_edge_function();
|
|
11335
12230
|
import { z as z24 } from "zod";
|
|
11336
12231
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
11337
12232
|
init_supabase();
|
|
11338
12233
|
function asEnvelope20(data) {
|
|
11339
|
-
return {
|
|
12234
|
+
return {
|
|
12235
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
12236
|
+
data
|
|
12237
|
+
};
|
|
11340
12238
|
}
|
|
11341
12239
|
var PLATFORM_ENUM2 = z24.enum([
|
|
11342
12240
|
"youtube",
|
|
@@ -11372,7 +12270,10 @@ function registerPipelineTools(server) {
|
|
|
11372
12270
|
action: "pipeline-readiness",
|
|
11373
12271
|
platforms,
|
|
11374
12272
|
estimated_posts,
|
|
11375
|
-
...resolvedProjectId ? {
|
|
12273
|
+
...resolvedProjectId ? {
|
|
12274
|
+
projectId: resolvedProjectId,
|
|
12275
|
+
project_id: resolvedProjectId
|
|
12276
|
+
} : {}
|
|
11376
12277
|
},
|
|
11377
12278
|
{ timeoutMs: 15e3 }
|
|
11378
12279
|
);
|
|
@@ -11390,16 +12291,24 @@ function registerPipelineTools(server) {
|
|
|
11390
12291
|
const blockers = [];
|
|
11391
12292
|
const warnings = [];
|
|
11392
12293
|
if (!isUnlimited && credits < estimatedCost) {
|
|
11393
|
-
blockers.push(
|
|
12294
|
+
blockers.push(
|
|
12295
|
+
`Insufficient credits: ${credits} available, ~${estimatedCost} needed`
|
|
12296
|
+
);
|
|
11394
12297
|
}
|
|
11395
12298
|
if (missingPlatforms.length > 0) {
|
|
11396
|
-
blockers.push(
|
|
12299
|
+
blockers.push(
|
|
12300
|
+
`Missing connected accounts: ${missingPlatforms.join(", ")}`
|
|
12301
|
+
);
|
|
11397
12302
|
}
|
|
11398
12303
|
if (!hasBrand) {
|
|
11399
|
-
warnings.push(
|
|
12304
|
+
warnings.push(
|
|
12305
|
+
"No brand profile found. Content will use generic voice."
|
|
12306
|
+
);
|
|
11400
12307
|
}
|
|
11401
12308
|
if (pendingApprovals > 0) {
|
|
11402
|
-
warnings.push(
|
|
12309
|
+
warnings.push(
|
|
12310
|
+
`${pendingApprovals} pending approval(s) from previous runs.`
|
|
12311
|
+
);
|
|
11403
12312
|
}
|
|
11404
12313
|
if (!insightsFresh) {
|
|
11405
12314
|
warnings.push(
|
|
@@ -11414,7 +12323,10 @@ function registerPipelineTools(server) {
|
|
|
11414
12323
|
estimated_cost: estimatedCost,
|
|
11415
12324
|
sufficient: credits >= estimatedCost
|
|
11416
12325
|
},
|
|
11417
|
-
connected_accounts: {
|
|
12326
|
+
connected_accounts: {
|
|
12327
|
+
platforms: connectedPlatforms,
|
|
12328
|
+
missing: missingPlatforms
|
|
12329
|
+
},
|
|
11418
12330
|
brand_profile: { exists: hasBrand },
|
|
11419
12331
|
pending_approvals: { count: pendingApprovals },
|
|
11420
12332
|
insights_available: {
|
|
@@ -11428,11 +12340,18 @@ function registerPipelineTools(server) {
|
|
|
11428
12340
|
};
|
|
11429
12341
|
if (format === "json") {
|
|
11430
12342
|
return {
|
|
11431
|
-
content: [
|
|
12343
|
+
content: [
|
|
12344
|
+
{
|
|
12345
|
+
type: "text",
|
|
12346
|
+
text: JSON.stringify(asEnvelope20(result), null, 2)
|
|
12347
|
+
}
|
|
12348
|
+
]
|
|
11432
12349
|
};
|
|
11433
12350
|
}
|
|
11434
12351
|
const lines = [];
|
|
11435
|
-
lines.push(
|
|
12352
|
+
lines.push(
|
|
12353
|
+
`Pipeline Readiness: ${result.ready ? "READY" : "NOT READY"}`
|
|
12354
|
+
);
|
|
11436
12355
|
lines.push("=".repeat(40));
|
|
11437
12356
|
lines.push(
|
|
11438
12357
|
`Credits: ${credits} available, ~${estimatedCost} needed \u2014 ${credits >= estimatedCost ? "OK" : "BLOCKED"}`
|
|
@@ -11440,7 +12359,9 @@ function registerPipelineTools(server) {
|
|
|
11440
12359
|
lines.push(
|
|
11441
12360
|
`Accounts: ${connectedPlatforms.length} connected${missingPlatforms.length > 0 ? ` (missing: ${missingPlatforms.join(", ")})` : " \u2014 OK"}`
|
|
11442
12361
|
);
|
|
11443
|
-
lines.push(
|
|
12362
|
+
lines.push(
|
|
12363
|
+
`Brand: ${hasBrand ? "OK" : "Missing (will use generic voice)"}`
|
|
12364
|
+
);
|
|
11444
12365
|
lines.push(`Pending Approvals: ${pendingApprovals}`);
|
|
11445
12366
|
lines.push(
|
|
11446
12367
|
`Insights: ${insightsFresh ? "Fresh" : insightAge === null ? "None available" : `${insightAge} days old`}`
|
|
@@ -11459,7 +12380,12 @@ function registerPipelineTools(server) {
|
|
|
11459
12380
|
} catch (err) {
|
|
11460
12381
|
const message = sanitizeError(err);
|
|
11461
12382
|
return {
|
|
11462
|
-
content: [
|
|
12383
|
+
content: [
|
|
12384
|
+
{
|
|
12385
|
+
type: "text",
|
|
12386
|
+
text: `Readiness check failed: ${message}`
|
|
12387
|
+
}
|
|
12388
|
+
],
|
|
11463
12389
|
isError: true
|
|
11464
12390
|
};
|
|
11465
12391
|
}
|
|
@@ -11473,6 +12399,9 @@ function registerPipelineTools(server) {
|
|
|
11473
12399
|
topic: z24.string().optional().describe("Content topic (required if no source_url)"),
|
|
11474
12400
|
source_url: z24.string().optional().describe("URL to extract content from"),
|
|
11475
12401
|
platforms: z24.array(PLATFORM_ENUM2).min(1).describe("Target platforms"),
|
|
12402
|
+
account_ids: z24.record(z24.string(), z24.string().uuid()).optional().describe(
|
|
12403
|
+
"Exact connected-account ID per target platform. Required when a project has multiple accounts on one platform."
|
|
12404
|
+
),
|
|
11476
12405
|
days: z24.number().min(1).max(7).default(5).describe("Days to plan"),
|
|
11477
12406
|
posts_per_day: z24.number().min(1).max(3).default(1).describe("Posts per platform per day"),
|
|
11478
12407
|
approval_mode: z24.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe(
|
|
@@ -11494,6 +12423,7 @@ function registerPipelineTools(server) {
|
|
|
11494
12423
|
topic,
|
|
11495
12424
|
source_url,
|
|
11496
12425
|
platforms,
|
|
12426
|
+
account_ids,
|
|
11497
12427
|
days,
|
|
11498
12428
|
posts_per_day,
|
|
11499
12429
|
approval_mode,
|
|
@@ -11511,7 +12441,12 @@ function registerPipelineTools(server) {
|
|
|
11511
12441
|
let creditsUsed = 0;
|
|
11512
12442
|
if (!topic && !source_url) {
|
|
11513
12443
|
return {
|
|
11514
|
-
content: [
|
|
12444
|
+
content: [
|
|
12445
|
+
{
|
|
12446
|
+
type: "text",
|
|
12447
|
+
text: "Either topic or source_url is required."
|
|
12448
|
+
}
|
|
12449
|
+
],
|
|
11515
12450
|
isError: true
|
|
11516
12451
|
};
|
|
11517
12452
|
}
|
|
@@ -11541,6 +12476,35 @@ function registerPipelineTools(server) {
|
|
|
11541
12476
|
}
|
|
11542
12477
|
try {
|
|
11543
12478
|
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
12479
|
+
if (schedulingRequested && !resolvedProjectId) {
|
|
12480
|
+
return {
|
|
12481
|
+
content: [
|
|
12482
|
+
{
|
|
12483
|
+
type: "text",
|
|
12484
|
+
text: "A project_id is required to schedule pipeline output. Configure an explicit project or use an API key scoped to exactly one project."
|
|
12485
|
+
}
|
|
12486
|
+
],
|
|
12487
|
+
isError: true
|
|
12488
|
+
};
|
|
12489
|
+
}
|
|
12490
|
+
if (schedulingRequested) {
|
|
12491
|
+
const preflightRouting = await resolveConnectedAccountRouting({
|
|
12492
|
+
projectId: resolvedProjectId,
|
|
12493
|
+
platforms,
|
|
12494
|
+
requestedAccountIds: account_ids
|
|
12495
|
+
});
|
|
12496
|
+
if (preflightRouting.error || !preflightRouting.connectedAccountIds) {
|
|
12497
|
+
return {
|
|
12498
|
+
content: [
|
|
12499
|
+
{
|
|
12500
|
+
type: "text",
|
|
12501
|
+
text: `Cannot run publishing pipeline \u2014 ${preflightRouting.error ?? "exact connected-account routing could not be established."} (checked before any credits were spent.)`
|
|
12502
|
+
}
|
|
12503
|
+
],
|
|
12504
|
+
isError: true
|
|
12505
|
+
};
|
|
12506
|
+
}
|
|
12507
|
+
}
|
|
11544
12508
|
const estimatedCost = BASE_PLAN_CREDITS + (source_url ? SOURCE_EXTRACTION_CREDITS : 0);
|
|
11545
12509
|
const { data: budgetData } = await callEdgeFunction(
|
|
11546
12510
|
"mcp-data",
|
|
@@ -11594,7 +12558,13 @@ function registerPipelineTools(server) {
|
|
|
11594
12558
|
"social-neuron-ai",
|
|
11595
12559
|
{
|
|
11596
12560
|
type: "generation",
|
|
11597
|
-
prompt: buildPlanPrompt(
|
|
12561
|
+
prompt: buildPlanPrompt(
|
|
12562
|
+
resolvedTopic,
|
|
12563
|
+
platforms,
|
|
12564
|
+
days,
|
|
12565
|
+
posts_per_day,
|
|
12566
|
+
source_url
|
|
12567
|
+
),
|
|
11598
12568
|
model: "gemini-2.5-flash",
|
|
11599
12569
|
responseFormat: "json",
|
|
11600
12570
|
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
@@ -11602,7 +12572,10 @@ function registerPipelineTools(server) {
|
|
|
11602
12572
|
{ timeoutMs: 6e4 }
|
|
11603
12573
|
);
|
|
11604
12574
|
if (planError || !planData) {
|
|
11605
|
-
errors.push({
|
|
12575
|
+
errors.push({
|
|
12576
|
+
stage: "planning",
|
|
12577
|
+
message: planError ?? "No AI response"
|
|
12578
|
+
});
|
|
11606
12579
|
await callEdgeFunction(
|
|
11607
12580
|
"mcp-data",
|
|
11608
12581
|
{
|
|
@@ -11619,7 +12592,10 @@ function registerPipelineTools(server) {
|
|
|
11619
12592
|
);
|
|
11620
12593
|
return {
|
|
11621
12594
|
content: [
|
|
11622
|
-
{
|
|
12595
|
+
{
|
|
12596
|
+
type: "text",
|
|
12597
|
+
text: `Planning failed: ${planError ?? "No AI response"}`
|
|
12598
|
+
}
|
|
11623
12599
|
],
|
|
11624
12600
|
isError: true
|
|
11625
12601
|
};
|
|
@@ -11649,20 +12625,22 @@ function registerPipelineTools(server) {
|
|
|
11649
12625
|
const postsArray = extractJsonArray(rawText);
|
|
11650
12626
|
const requestedPlatformSet = new Set(platforms);
|
|
11651
12627
|
const maxPosts = platforms.length * days * posts_per_day;
|
|
11652
|
-
const parsedPosts = (postsArray ?? []).map(
|
|
11653
|
-
|
|
11654
|
-
|
|
11655
|
-
|
|
11656
|
-
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
11665
|
-
|
|
12628
|
+
const parsedPosts = (postsArray ?? []).map(
|
|
12629
|
+
(p) => ({
|
|
12630
|
+
id: String(p.id ?? randomUUID3().slice(0, 8)),
|
|
12631
|
+
day: Number(p.day ?? 1),
|
|
12632
|
+
date: String(p.date ?? ""),
|
|
12633
|
+
platform: String(p.platform ?? ""),
|
|
12634
|
+
content_type: p.content_type ?? "caption",
|
|
12635
|
+
caption: String(p.caption ?? ""),
|
|
12636
|
+
title: p.title ? String(p.title) : void 0,
|
|
12637
|
+
hashtags: Array.isArray(p.hashtags) ? p.hashtags.map(String) : void 0,
|
|
12638
|
+
hook: String(p.hook ?? ""),
|
|
12639
|
+
angle: String(p.angle ?? ""),
|
|
12640
|
+
visual_direction: p.visual_direction ? String(p.visual_direction) : void 0,
|
|
12641
|
+
media_type: p.media_type ? String(p.media_type) : void 0
|
|
12642
|
+
})
|
|
12643
|
+
);
|
|
11666
12644
|
const platformFilteredPosts = parsedPosts.filter(
|
|
11667
12645
|
(post) => requestedPlatformSet.has(post.platform)
|
|
11668
12646
|
);
|
|
@@ -11745,7 +12723,10 @@ function registerPipelineTools(server) {
|
|
|
11745
12723
|
estimated_credits: estimatedCost,
|
|
11746
12724
|
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
11747
12725
|
},
|
|
11748
|
-
...resolvedProjectId ? {
|
|
12726
|
+
...resolvedProjectId ? {
|
|
12727
|
+
projectId: resolvedProjectId,
|
|
12728
|
+
project_id: resolvedProjectId
|
|
12729
|
+
} : {}
|
|
11749
12730
|
},
|
|
11750
12731
|
{ timeoutMs: 1e4 }
|
|
11751
12732
|
);
|
|
@@ -11798,6 +12779,22 @@ function registerPipelineTools(server) {
|
|
|
11798
12779
|
}
|
|
11799
12780
|
}
|
|
11800
12781
|
let postsScheduled = 0;
|
|
12782
|
+
const pipelineRouting = schedulingRequested && postsApproved > 0 ? await resolveConnectedAccountRouting({
|
|
12783
|
+
projectId: resolvedProjectId,
|
|
12784
|
+
platforms,
|
|
12785
|
+
requestedAccountIds: account_ids
|
|
12786
|
+
}) : void 0;
|
|
12787
|
+
if (pipelineRouting?.error || schedulingRequested && postsApproved > 0 && !pipelineRouting?.connectedAccountIds) {
|
|
12788
|
+
return {
|
|
12789
|
+
content: [
|
|
12790
|
+
{
|
|
12791
|
+
type: "text",
|
|
12792
|
+
text: `Cannot run publishing pipeline \u2014 ${pipelineRouting?.error ?? "exact connected-account routing could not be established."}`
|
|
12793
|
+
}
|
|
12794
|
+
],
|
|
12795
|
+
isError: true
|
|
12796
|
+
};
|
|
12797
|
+
}
|
|
11801
12798
|
if (!dry_run && !skipSet.has("schedule") && postsApproved > 0) {
|
|
11802
12799
|
const approvedPosts = posts.filter((p) => p.status === "approved");
|
|
11803
12800
|
const scheduleBase = /* @__PURE__ */ new Date();
|
|
@@ -11805,10 +12802,27 @@ function registerPipelineTools(server) {
|
|
|
11805
12802
|
const scheduleBaseMs = scheduleBase.getTime();
|
|
11806
12803
|
for (const post of approvedPosts) {
|
|
11807
12804
|
if (creditsUsed >= creditLimit) {
|
|
11808
|
-
errors.push({
|
|
12805
|
+
errors.push({
|
|
12806
|
+
stage: "schedule",
|
|
12807
|
+
message: "Credit limit reached"
|
|
12808
|
+
});
|
|
11809
12809
|
break;
|
|
11810
12810
|
}
|
|
11811
|
-
const scheduledAt = post.schedule_at ?? new Date(
|
|
12811
|
+
const scheduledAt = post.schedule_at ?? new Date(
|
|
12812
|
+
scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5
|
|
12813
|
+
).toISOString();
|
|
12814
|
+
const route = Object.entries(
|
|
12815
|
+
pipelineRouting.connectedAccountIds
|
|
12816
|
+
).find(
|
|
12817
|
+
([platform2]) => platform2.toLowerCase() === post.platform.toLowerCase()
|
|
12818
|
+
);
|
|
12819
|
+
if (!route) {
|
|
12820
|
+
errors.push({
|
|
12821
|
+
stage: "schedule",
|
|
12822
|
+
message: `No verified connected account route for ${post.platform}`
|
|
12823
|
+
});
|
|
12824
|
+
continue;
|
|
12825
|
+
}
|
|
11812
12826
|
try {
|
|
11813
12827
|
const { error: schedError } = await callEdgeFunction(
|
|
11814
12828
|
"schedule-post",
|
|
@@ -11821,7 +12835,11 @@ function registerPipelineTools(server) {
|
|
|
11821
12835
|
scheduledAt,
|
|
11822
12836
|
planId,
|
|
11823
12837
|
idempotencyKey: `pipeline-${planId}-${post.id}`,
|
|
11824
|
-
...resolvedProjectId ? {
|
|
12838
|
+
...resolvedProjectId ? {
|
|
12839
|
+
projectId: resolvedProjectId,
|
|
12840
|
+
project_id: resolvedProjectId
|
|
12841
|
+
} : {},
|
|
12842
|
+
connectedAccountIds: { [route[0]]: route[1] }
|
|
11825
12843
|
},
|
|
11826
12844
|
{ timeoutMs: 15e3 }
|
|
11827
12845
|
);
|
|
@@ -11887,12 +12905,17 @@ function registerPipelineTools(server) {
|
|
|
11887
12905
|
if (response_format === "json") {
|
|
11888
12906
|
return {
|
|
11889
12907
|
content: [
|
|
11890
|
-
{
|
|
12908
|
+
{
|
|
12909
|
+
type: "text",
|
|
12910
|
+
text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
|
|
12911
|
+
}
|
|
11891
12912
|
]
|
|
11892
12913
|
};
|
|
11893
12914
|
}
|
|
11894
12915
|
const lines = [];
|
|
11895
|
-
lines.push(
|
|
12916
|
+
lines.push(
|
|
12917
|
+
`Pipeline ${pipelineId.slice(0, 8)}... ${finalStatus.toUpperCase()}`
|
|
12918
|
+
);
|
|
11896
12919
|
lines.push("=".repeat(40));
|
|
11897
12920
|
lines.push(`Posts generated: ${posts.length}`);
|
|
11898
12921
|
lines.push(`Posts approved: ${postsApproved}`);
|
|
@@ -11931,7 +12954,9 @@ function registerPipelineTools(server) {
|
|
|
11931
12954
|
} catch {
|
|
11932
12955
|
}
|
|
11933
12956
|
return {
|
|
11934
|
-
content: [
|
|
12957
|
+
content: [
|
|
12958
|
+
{ type: "text", text: `Pipeline failed: ${message}` }
|
|
12959
|
+
],
|
|
11935
12960
|
isError: true
|
|
11936
12961
|
};
|
|
11937
12962
|
}
|
|
@@ -11968,7 +12993,12 @@ function registerPipelineTools(server) {
|
|
|
11968
12993
|
}
|
|
11969
12994
|
if (format === "json") {
|
|
11970
12995
|
return {
|
|
11971
|
-
content: [
|
|
12996
|
+
content: [
|
|
12997
|
+
{
|
|
12998
|
+
type: "text",
|
|
12999
|
+
text: JSON.stringify(asEnvelope20(data), null, 2)
|
|
13000
|
+
}
|
|
13001
|
+
]
|
|
11972
13002
|
};
|
|
11973
13003
|
}
|
|
11974
13004
|
const lines = [];
|
|
@@ -12008,10 +13038,19 @@ function registerPipelineTools(server) {
|
|
|
12008
13038
|
},
|
|
12009
13039
|
async ({ plan_id, quality_threshold, response_format }) => {
|
|
12010
13040
|
try {
|
|
12011
|
-
const { data: loadResult, error: loadError } = await callEdgeFunction(
|
|
13041
|
+
const { data: loadResult, error: loadError } = await callEdgeFunction(
|
|
13042
|
+
"mcp-data",
|
|
13043
|
+
{ action: "auto-approve-plan", plan_id },
|
|
13044
|
+
{ timeoutMs: 1e4 }
|
|
13045
|
+
);
|
|
12012
13046
|
if (loadError) {
|
|
12013
13047
|
return {
|
|
12014
|
-
content: [
|
|
13048
|
+
content: [
|
|
13049
|
+
{
|
|
13050
|
+
type: "text",
|
|
13051
|
+
text: `Failed to load plan: ${loadError}`
|
|
13052
|
+
}
|
|
13053
|
+
],
|
|
12015
13054
|
isError: true
|
|
12016
13055
|
};
|
|
12017
13056
|
}
|
|
@@ -12019,7 +13058,10 @@ function registerPipelineTools(server) {
|
|
|
12019
13058
|
if (!stored?.plan_payload) {
|
|
12020
13059
|
return {
|
|
12021
13060
|
content: [
|
|
12022
|
-
{
|
|
13061
|
+
{
|
|
13062
|
+
type: "text",
|
|
13063
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
13064
|
+
}
|
|
12023
13065
|
],
|
|
12024
13066
|
isError: true
|
|
12025
13067
|
};
|
|
@@ -12046,7 +13088,11 @@ function registerPipelineTools(server) {
|
|
|
12046
13088
|
blockers: []
|
|
12047
13089
|
};
|
|
12048
13090
|
autoApproved++;
|
|
12049
|
-
details.push({
|
|
13091
|
+
details.push({
|
|
13092
|
+
post_id: post.id,
|
|
13093
|
+
action: "approved",
|
|
13094
|
+
score: quality.total
|
|
13095
|
+
});
|
|
12050
13096
|
} else if (quality.total >= quality_threshold - 5) {
|
|
12051
13097
|
post.status = "needs_edit";
|
|
12052
13098
|
post.quality = {
|
|
@@ -12056,7 +13102,11 @@ function registerPipelineTools(server) {
|
|
|
12056
13102
|
blockers: quality.blockers
|
|
12057
13103
|
};
|
|
12058
13104
|
flagged++;
|
|
12059
|
-
details.push({
|
|
13105
|
+
details.push({
|
|
13106
|
+
post_id: post.id,
|
|
13107
|
+
action: "flagged",
|
|
13108
|
+
score: quality.total
|
|
13109
|
+
});
|
|
12060
13110
|
} else {
|
|
12061
13111
|
post.status = "rejected";
|
|
12062
13112
|
post.quality = {
|
|
@@ -12066,7 +13116,11 @@ function registerPipelineTools(server) {
|
|
|
12066
13116
|
blockers: quality.blockers
|
|
12067
13117
|
};
|
|
12068
13118
|
rejected++;
|
|
12069
|
-
details.push({
|
|
13119
|
+
details.push({
|
|
13120
|
+
post_id: post.id,
|
|
13121
|
+
action: "rejected",
|
|
13122
|
+
score: quality.total
|
|
13123
|
+
});
|
|
12070
13124
|
}
|
|
12071
13125
|
}
|
|
12072
13126
|
const newStatus = flagged === 0 && rejected === 0 ? "approved" : "in_review";
|
|
@@ -12102,7 +13156,10 @@ function registerPipelineTools(server) {
|
|
|
12102
13156
|
if (response_format === "json") {
|
|
12103
13157
|
return {
|
|
12104
13158
|
content: [
|
|
12105
|
-
{
|
|
13159
|
+
{
|
|
13160
|
+
type: "text",
|
|
13161
|
+
text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
|
|
13162
|
+
}
|
|
12106
13163
|
]
|
|
12107
13164
|
};
|
|
12108
13165
|
}
|
|
@@ -12123,7 +13180,9 @@ function registerPipelineTools(server) {
|
|
|
12123
13180
|
} catch (err) {
|
|
12124
13181
|
const message = sanitizeError(err);
|
|
12125
13182
|
return {
|
|
12126
|
-
content: [
|
|
13183
|
+
content: [
|
|
13184
|
+
{ type: "text", text: `Auto-approve failed: ${message}` }
|
|
13185
|
+
],
|
|
12127
13186
|
isError: true
|
|
12128
13187
|
};
|
|
12129
13188
|
}
|
|
@@ -12149,6 +13208,7 @@ function buildPlanPrompt(topic, platforms, days, postsPerDay, sourceUrl) {
|
|
|
12149
13208
|
}
|
|
12150
13209
|
|
|
12151
13210
|
// src/tools/suggest.ts
|
|
13211
|
+
init_edge_function();
|
|
12152
13212
|
import { z as z25 } from "zod";
|
|
12153
13213
|
function asEnvelope21(data) {
|
|
12154
13214
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
@@ -12290,6 +13350,7 @@ ${i + 1}. ${s.topic}`);
|
|
|
12290
13350
|
}
|
|
12291
13351
|
|
|
12292
13352
|
// src/tools/digest.ts
|
|
13353
|
+
init_edge_function();
|
|
12293
13354
|
import { z as z26 } from "zod";
|
|
12294
13355
|
|
|
12295
13356
|
// src/lib/anomaly-detector.ts
|
|
@@ -12695,8 +13756,9 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
|
|
|
12695
13756
|
}
|
|
12696
13757
|
|
|
12697
13758
|
// src/tools/brandRuntime.ts
|
|
12698
|
-
|
|
13759
|
+
init_edge_function();
|
|
12699
13760
|
init_supabase();
|
|
13761
|
+
import { z as z27 } from "zod";
|
|
12700
13762
|
|
|
12701
13763
|
// src/lib/brandScoring.ts
|
|
12702
13764
|
var WEIGHTS = {
|
|
@@ -13553,6 +14615,7 @@ function registerBrandRuntimeTools(server) {
|
|
|
13553
14615
|
}
|
|
13554
14616
|
|
|
13555
14617
|
// src/tools/carousel.ts
|
|
14618
|
+
init_edge_function();
|
|
13556
14619
|
import { z as z28 } from "zod";
|
|
13557
14620
|
init_supabase();
|
|
13558
14621
|
var IMAGE_CREDIT_ESTIMATES2 = {
|
|
@@ -13630,25 +14693,39 @@ function registerCarouselTools(server) {
|
|
|
13630
14693
|
]).optional().describe("Carousel template. Default: hormozi-authority."),
|
|
13631
14694
|
slide_count: z28.number().min(3).max(10).optional().describe("Number of slides (3-10). Default: 7."),
|
|
13632
14695
|
aspect_ratio: z28.enum(["1:1", "4:5", "9:16"]).optional().describe("Aspect ratio for both carousel and images. Default: 1:1."),
|
|
13633
|
-
style: z28.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
|
|
14696
|
+
style: z28.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
|
|
14697
|
+
"Visual style. Default: hormozi for hormozi-authority template."
|
|
14698
|
+
),
|
|
13634
14699
|
image_style_suffix: z28.string().max(500).optional().describe(
|
|
13635
14700
|
'Style suffix appended to every image prompt for visual consistency across slides. Example: "dark moody lighting, cinematic, 35mm film grain".'
|
|
13636
14701
|
),
|
|
13637
14702
|
hook: z28.string().max(300).optional().describe(
|
|
13638
14703
|
"Explicit hook/opener for slide 1. Overrides any hook derived from topic. Keep under 15 words for strongest scroll-stop."
|
|
13639
14704
|
),
|
|
13640
|
-
hook_family: z28.enum([
|
|
14705
|
+
hook_family: z28.enum([
|
|
14706
|
+
"curiosity",
|
|
14707
|
+
"authority",
|
|
14708
|
+
"pain_point",
|
|
14709
|
+
"contrarian",
|
|
14710
|
+
"data_driven"
|
|
14711
|
+
]).optional().describe(
|
|
13641
14712
|
"Hook family tag. Recorded on the carousel so downstream analytics can attribute engagement to hook pattern."
|
|
13642
14713
|
),
|
|
13643
|
-
cta_text: z28.string().max(200).optional().describe(
|
|
13644
|
-
|
|
14714
|
+
cta_text: z28.string().max(200).optional().describe(
|
|
14715
|
+
"Explicit CTA copy for the final slide. If omitted, derived from topic."
|
|
14716
|
+
),
|
|
14717
|
+
cta_url: z28.string().url().optional().describe(
|
|
14718
|
+
"URL promoted on the CTA slide. Defaults to the project landing page."
|
|
14719
|
+
),
|
|
13645
14720
|
tone: z28.string().max(200).optional().describe(
|
|
13646
14721
|
'Voice/tone override. Composes with the brand profile voice when present. Example: "educational, confident, not arrogant".'
|
|
13647
14722
|
),
|
|
13648
14723
|
constraints: z28.string().max(500).optional().describe(
|
|
13649
14724
|
'Content constraints applied at generation time. Example: "No fabricated statistics. Sentence case only. No ALL CAPS."'
|
|
13650
14725
|
),
|
|
13651
|
-
platform: z28.enum(["linkedin", "instagram", "tiktok", "x"]).optional().describe(
|
|
14726
|
+
platform: z28.enum(["linkedin", "instagram", "tiktok", "x"]).optional().describe(
|
|
14727
|
+
"Target platform. Affects tone conventions and slide-count guardrails."
|
|
14728
|
+
),
|
|
13652
14729
|
brand_id: z28.string().optional().describe(
|
|
13653
14730
|
"Brand/project ID to pull visual context from (colors, logo, mood). Falls back to project_id, then default project."
|
|
13654
14731
|
),
|
|
@@ -13680,7 +14757,9 @@ function registerCarouselTools(server) {
|
|
|
13680
14757
|
const slideCount = slide_count ?? 7;
|
|
13681
14758
|
const ratio = aspect_ratio ?? "1:1";
|
|
13682
14759
|
let brandContext = null;
|
|
13683
|
-
const
|
|
14760
|
+
const defaultProjectId = await getDefaultProjectId();
|
|
14761
|
+
const brandProjectId = brand_id || project_id || defaultProjectId;
|
|
14762
|
+
const resolvedProjectId = project_id || defaultProjectId;
|
|
13684
14763
|
if (brandProjectId) {
|
|
13685
14764
|
brandContext = await fetchBrandVisualContext(brandProjectId);
|
|
13686
14765
|
}
|
|
@@ -13689,10 +14768,7 @@ function registerCarouselTools(server) {
|
|
|
13689
14768
|
const totalEstimatedCost = carouselTextCost + slideCount * perImageCost;
|
|
13690
14769
|
const budgetCheck = checkCreditBudget(totalEstimatedCost);
|
|
13691
14770
|
if (!budgetCheck.ok) {
|
|
13692
|
-
return
|
|
13693
|
-
content: [{ type: "text", text: budgetCheck.message }],
|
|
13694
|
-
isError: true
|
|
13695
|
-
};
|
|
14771
|
+
return budgetCheck.error;
|
|
13696
14772
|
}
|
|
13697
14773
|
const assetBudget = checkAssetBudget(slideCount);
|
|
13698
14774
|
if (!assetBudget.ok) {
|
|
@@ -13702,7 +14778,10 @@ function registerCarouselTools(server) {
|
|
|
13702
14778
|
};
|
|
13703
14779
|
}
|
|
13704
14780
|
const userId = await getDefaultUserId();
|
|
13705
|
-
const rateLimit = checkRateLimit(
|
|
14781
|
+
const rateLimit = checkRateLimit(
|
|
14782
|
+
"generation",
|
|
14783
|
+
`create_carousel:${userId}`
|
|
14784
|
+
);
|
|
13706
14785
|
if (!rateLimit.allowed) {
|
|
13707
14786
|
return {
|
|
13708
14787
|
content: [
|
|
@@ -13722,7 +14801,7 @@ function registerCarouselTools(server) {
|
|
|
13722
14801
|
slideCount,
|
|
13723
14802
|
aspectRatio: ratio,
|
|
13724
14803
|
style: resolvedStyle,
|
|
13725
|
-
projectId:
|
|
14804
|
+
projectId: resolvedProjectId,
|
|
13726
14805
|
hook,
|
|
13727
14806
|
hookFamily: hook_family,
|
|
13728
14807
|
ctaText: cta_text,
|
|
@@ -13736,7 +14815,12 @@ function registerCarouselTools(server) {
|
|
|
13736
14815
|
if (carouselError || !carouselData?.carousel) {
|
|
13737
14816
|
const errMsg = carouselError ?? "No carousel data returned";
|
|
13738
14817
|
return {
|
|
13739
|
-
content: [
|
|
14818
|
+
content: [
|
|
14819
|
+
{
|
|
14820
|
+
type: "text",
|
|
14821
|
+
text: `Carousel text generation failed: ${errMsg}`
|
|
14822
|
+
}
|
|
14823
|
+
],
|
|
13740
14824
|
isError: true
|
|
13741
14825
|
};
|
|
13742
14826
|
}
|
|
@@ -13765,7 +14849,8 @@ function registerCarouselTools(server) {
|
|
|
13765
14849
|
{
|
|
13766
14850
|
prompt: imagePrompt,
|
|
13767
14851
|
model: image_model,
|
|
13768
|
-
aspectRatio: ratio
|
|
14852
|
+
aspectRatio: ratio,
|
|
14853
|
+
...resolvedProjectId && { projectId: resolvedProjectId }
|
|
13769
14854
|
},
|
|
13770
14855
|
{ timeoutMs: 3e4 }
|
|
13771
14856
|
);
|
|
@@ -13822,14 +14907,19 @@ function registerCarouselTools(server) {
|
|
|
13822
14907
|
type: "text",
|
|
13823
14908
|
text: JSON.stringify(
|
|
13824
14909
|
{
|
|
13825
|
-
_meta: {
|
|
14910
|
+
_meta: {
|
|
14911
|
+
version: MCP_VERSION,
|
|
14912
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
14913
|
+
},
|
|
13826
14914
|
data: {
|
|
13827
14915
|
carouselId: carousel.id,
|
|
13828
14916
|
templateId,
|
|
13829
14917
|
style: resolvedStyle,
|
|
13830
14918
|
slideCount: carousel.slides.length,
|
|
13831
14919
|
slides: carousel.slides.map((s) => {
|
|
13832
|
-
const job = imageJobs.find(
|
|
14920
|
+
const job = imageJobs.find(
|
|
14921
|
+
(j) => j.slideNumber === s.slideNumber
|
|
14922
|
+
);
|
|
13833
14923
|
return {
|
|
13834
14924
|
...s,
|
|
13835
14925
|
imageJobId: job?.jobId ?? null,
|
|
@@ -13856,9 +14946,17 @@ function registerCarouselTools(server) {
|
|
|
13856
14946
|
credits: {
|
|
13857
14947
|
textGeneration: textCredits,
|
|
13858
14948
|
imagesEstimated: successfulJobs.length * perImageCost,
|
|
13859
|
-
imagesCharged: imageJobs.reduce(
|
|
13860
|
-
|
|
13861
|
-
|
|
14949
|
+
imagesCharged: imageJobs.reduce(
|
|
14950
|
+
(sum, job) => sum + (job.creditsCharged ?? 0),
|
|
14951
|
+
0
|
|
14952
|
+
),
|
|
14953
|
+
imagesRefunded: imageJobs.reduce(
|
|
14954
|
+
(sum, job) => sum + (job.creditsRefunded ?? 0),
|
|
14955
|
+
0
|
|
14956
|
+
),
|
|
14957
|
+
billingUnknownSlides: imageJobs.filter(
|
|
14958
|
+
(job) => job.billingStatus === "unknown"
|
|
14959
|
+
).length,
|
|
13862
14960
|
totalEstimated: textCredits + successfulJobs.length * perImageCost
|
|
13863
14961
|
}
|
|
13864
14962
|
}
|
|
@@ -13886,7 +14984,9 @@ function registerCarouselTools(server) {
|
|
|
13886
14984
|
for (const slide of carousel.slides) {
|
|
13887
14985
|
const job = imageJobs.find((j) => j.slideNumber === slide.slideNumber);
|
|
13888
14986
|
const status = job?.jobId ? `image: ${job.jobId}` : `image FAILED: ${job?.error}`;
|
|
13889
|
-
lines.push(
|
|
14987
|
+
lines.push(
|
|
14988
|
+
` ${slide.slideNumber}. ${slide.headline || "(no headline)"} [${status}]`
|
|
14989
|
+
);
|
|
13890
14990
|
}
|
|
13891
14991
|
if (failedJobs.length > 0) {
|
|
13892
14992
|
lines.push("");
|
|
@@ -13897,7 +14997,9 @@ function registerCarouselTools(server) {
|
|
|
13897
14997
|
const jobIdList = successfulJobs.map((j) => j.jobId).join(", ");
|
|
13898
14998
|
lines.push("");
|
|
13899
14999
|
lines.push("Next steps:");
|
|
13900
|
-
lines.push(
|
|
15000
|
+
lines.push(
|
|
15001
|
+
` 1. Poll each job: check_status with job_id for each of: ${jobIdList}`
|
|
15002
|
+
);
|
|
13901
15003
|
lines.push(
|
|
13902
15004
|
" 2. When all complete: schedule_post with job_ids=[...] and media_type=CAROUSEL_ALBUM"
|
|
13903
15005
|
);
|
|
@@ -13909,6 +15011,7 @@ function registerCarouselTools(server) {
|
|
|
13909
15011
|
}
|
|
13910
15012
|
|
|
13911
15013
|
// src/tools/niche-research.ts
|
|
15014
|
+
init_edge_function();
|
|
13912
15015
|
import { z as z29 } from "zod";
|
|
13913
15016
|
function asEnvelope24(data) {
|
|
13914
15017
|
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
@@ -13995,6 +15098,7 @@ function registerNicheResearchTools(server) {
|
|
|
13995
15098
|
// src/tools/hyperframes.ts
|
|
13996
15099
|
import { z as z30 } from "zod";
|
|
13997
15100
|
init_supabase();
|
|
15101
|
+
init_edge_function();
|
|
13998
15102
|
var HYPERFRAMES_BLOCKS = [
|
|
13999
15103
|
// Transitions
|
|
14000
15104
|
{
|
|
@@ -14249,6 +15353,9 @@ function registerHyperframesTools(server) {
|
|
|
14249
15353
|
}
|
|
14250
15354
|
|
|
14251
15355
|
// src/apps/content-calendar.ts
|
|
15356
|
+
init_edge_function();
|
|
15357
|
+
init_request_context();
|
|
15358
|
+
init_supabase();
|
|
14252
15359
|
import {
|
|
14253
15360
|
registerAppTool,
|
|
14254
15361
|
registerAppResource,
|
|
@@ -14258,8 +15365,6 @@ import { z as z31 } from "zod";
|
|
|
14258
15365
|
import fs from "node:fs/promises";
|
|
14259
15366
|
import path from "node:path";
|
|
14260
15367
|
import { fileURLToPath } from "node:url";
|
|
14261
|
-
init_request_context();
|
|
14262
|
-
init_supabase();
|
|
14263
15368
|
var CALENDAR_URI = "ui://content-calendar/v1/mcp-app.html";
|
|
14264
15369
|
var CALENDAR_CSP = {
|
|
14265
15370
|
// The HTML is fully self-contained. Tool calls travel over the host bridge,
|
|
@@ -14470,6 +15575,8 @@ function registerContentCalendarApp(server) {
|
|
|
14470
15575
|
}
|
|
14471
15576
|
|
|
14472
15577
|
// src/apps/analytics-pulse.ts
|
|
15578
|
+
init_edge_function();
|
|
15579
|
+
init_supabase();
|
|
14473
15580
|
import {
|
|
14474
15581
|
registerAppResource as registerAppResource2,
|
|
14475
15582
|
registerAppTool as registerAppTool2,
|
|
@@ -14479,7 +15586,6 @@ import { z as z32 } from "zod";
|
|
|
14479
15586
|
import fs2 from "node:fs/promises";
|
|
14480
15587
|
import path2 from "node:path";
|
|
14481
15588
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
14482
|
-
init_supabase();
|
|
14483
15589
|
var ANALYTICS_URI = "ui://analytics-pulse/v1/mcp-app.html";
|
|
14484
15590
|
var ANALYTICS_CSP = {
|
|
14485
15591
|
connectDomains: [],
|
|
@@ -14712,6 +15818,7 @@ function registerAnalyticsPulseApp(server) {
|
|
|
14712
15818
|
}
|
|
14713
15819
|
|
|
14714
15820
|
// src/tools/connections.ts
|
|
15821
|
+
init_edge_function();
|
|
14715
15822
|
import { z as z33 } from "zod";
|
|
14716
15823
|
init_supabase();
|
|
14717
15824
|
var PLATFORM_ENUM4 = [
|
|
@@ -14726,12 +15833,12 @@ var PLATFORM_ENUM4 = [
|
|
|
14726
15833
|
"shopify",
|
|
14727
15834
|
"etsy"
|
|
14728
15835
|
];
|
|
14729
|
-
function
|
|
15836
|
+
function findActiveAccounts(accounts, platform2, projectId) {
|
|
14730
15837
|
const target = platform2.toLowerCase();
|
|
14731
|
-
return accounts.
|
|
15838
|
+
return accounts.filter((a) => {
|
|
14732
15839
|
const effectiveStatus = a.effective_status || a.status;
|
|
14733
|
-
return a.platform.toLowerCase() === target && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
|
|
14734
|
-
})
|
|
15840
|
+
return a.platform.toLowerCase() === target && a.project_id === projectId && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
|
|
15841
|
+
});
|
|
14735
15842
|
}
|
|
14736
15843
|
var MAX_CONCURRENT_WAITS_PER_USER = 3;
|
|
14737
15844
|
var inFlightWaitsByUser = /* @__PURE__ */ new Map();
|
|
@@ -14740,16 +15847,21 @@ function registerConnectionTools(server) {
|
|
|
14740
15847
|
"start_platform_connection",
|
|
14741
15848
|
"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.",
|
|
14742
15849
|
{
|
|
14743
|
-
platform: z33.enum(PLATFORM_ENUM4).describe(
|
|
14744
|
-
|
|
14745
|
-
|
|
15850
|
+
platform: z33.enum(PLATFORM_ENUM4).describe(
|
|
15851
|
+
"Platform to connect. Lower-case: instagram, tiktok, youtube, etc."
|
|
15852
|
+
),
|
|
15853
|
+
project_id: z33.string().uuid().optional().describe(
|
|
15854
|
+
"Brand/project ID to bind the new social account to. Required when the account has multiple brands."
|
|
14746
15855
|
),
|
|
14747
15856
|
response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
14748
15857
|
},
|
|
14749
15858
|
async ({ platform: platform2, project_id, response_format }) => {
|
|
14750
15859
|
const format = response_format ?? "text";
|
|
14751
15860
|
const userId = await getDefaultUserId();
|
|
14752
|
-
const rl = checkRateLimit(
|
|
15861
|
+
const rl = checkRateLimit(
|
|
15862
|
+
"posting",
|
|
15863
|
+
`start_platform_connection:${userId}`
|
|
15864
|
+
);
|
|
14753
15865
|
if (!rl.allowed) {
|
|
14754
15866
|
return {
|
|
14755
15867
|
content: [
|
|
@@ -14761,12 +15873,27 @@ function registerConnectionTools(server) {
|
|
|
14761
15873
|
isError: true
|
|
14762
15874
|
};
|
|
14763
15875
|
}
|
|
15876
|
+
const projectResolution = await resolveProjectStrict(project_id);
|
|
15877
|
+
if (!projectResolution.projectId) {
|
|
15878
|
+
return {
|
|
15879
|
+
content: [
|
|
15880
|
+
{
|
|
15881
|
+
type: "text",
|
|
15882
|
+
text: projectResolution.error ?? "A project_id is required to connect a platform. Configure an explicit project or use an API key scoped to exactly one project."
|
|
15883
|
+
}
|
|
15884
|
+
],
|
|
15885
|
+
isError: true
|
|
15886
|
+
};
|
|
15887
|
+
}
|
|
15888
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
15889
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
14764
15890
|
const { data, error } = await callEdgeFunction(
|
|
14765
15891
|
"mcp-data",
|
|
14766
15892
|
{
|
|
14767
15893
|
action: "mint-connection-nonce",
|
|
14768
15894
|
platform: platform2,
|
|
14769
|
-
|
|
15895
|
+
projectId: resolvedProjectId,
|
|
15896
|
+
project_id: resolvedProjectId
|
|
14770
15897
|
},
|
|
14771
15898
|
{ timeoutMs: 1e4 }
|
|
14772
15899
|
);
|
|
@@ -14782,6 +15909,17 @@ function registerConnectionTools(server) {
|
|
|
14782
15909
|
isError: true
|
|
14783
15910
|
};
|
|
14784
15911
|
}
|
|
15912
|
+
if (data.project_id !== resolvedProjectId) {
|
|
15913
|
+
return {
|
|
15914
|
+
content: [
|
|
15915
|
+
{
|
|
15916
|
+
type: "text",
|
|
15917
|
+
text: "Connection link project attestation failed. No OAuth link was returned."
|
|
15918
|
+
}
|
|
15919
|
+
],
|
|
15920
|
+
isError: true
|
|
15921
|
+
};
|
|
15922
|
+
}
|
|
14785
15923
|
if (format === "json") {
|
|
14786
15924
|
return {
|
|
14787
15925
|
content: [
|
|
@@ -14790,10 +15928,11 @@ function registerConnectionTools(server) {
|
|
|
14790
15928
|
text: JSON.stringify(
|
|
14791
15929
|
{
|
|
14792
15930
|
platform: data.platform,
|
|
14793
|
-
project_id:
|
|
15931
|
+
project_id: resolvedProjectId,
|
|
14794
15932
|
deep_link: data.deep_link,
|
|
14795
15933
|
expires_at: data.expires_at,
|
|
14796
|
-
next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection."
|
|
15934
|
+
next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection.",
|
|
15935
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
14797
15936
|
},
|
|
14798
15937
|
null,
|
|
14799
15938
|
2
|
|
@@ -14809,7 +15948,7 @@ function registerConnectionTools(server) {
|
|
|
14809
15948
|
type: "text",
|
|
14810
15949
|
text: [
|
|
14811
15950
|
`${data.platform} connection ready.`,
|
|
14812
|
-
|
|
15951
|
+
`Brand/project: ${resolvedProjectId}`,
|
|
14813
15952
|
"",
|
|
14814
15953
|
'Ask the user to open this link in a browser and click "Connect" on the platform:',
|
|
14815
15954
|
` ${data.deep_link}`,
|
|
@@ -14817,7 +15956,8 @@ function registerConnectionTools(server) {
|
|
|
14817
15956
|
`Link expires at: ${data.expires_at} (~2 minutes).`,
|
|
14818
15957
|
"",
|
|
14819
15958
|
"After they approve, call `wait_for_connection` with the same platform to confirm.",
|
|
14820
|
-
"This is a one-time browser setup \u2014 not another OAuth flow inside Claude."
|
|
15959
|
+
"This is a one-time browser setup \u2014 not another OAuth flow inside Claude.",
|
|
15960
|
+
...projectAutoResolvedNote ? ["", `Note: ${projectAutoResolvedNote}`] : []
|
|
14821
15961
|
].join("\n")
|
|
14822
15962
|
}
|
|
14823
15963
|
],
|
|
@@ -14830,14 +15970,20 @@ function registerConnectionTools(server) {
|
|
|
14830
15970
|
"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.",
|
|
14831
15971
|
{
|
|
14832
15972
|
platform: z33.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
|
|
14833
|
-
project_id: z33.string().optional().describe(
|
|
15973
|
+
project_id: z33.string().uuid().optional().describe(
|
|
14834
15974
|
"Brand/project ID to scope the connection poll. Use the same project_id passed to start_platform_connection."
|
|
14835
15975
|
),
|
|
14836
15976
|
timeout_s: z33.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
|
|
14837
15977
|
poll_interval_s: z33.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
|
|
14838
15978
|
response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
14839
15979
|
},
|
|
14840
|
-
async ({
|
|
15980
|
+
async ({
|
|
15981
|
+
platform: platform2,
|
|
15982
|
+
project_id,
|
|
15983
|
+
timeout_s,
|
|
15984
|
+
poll_interval_s,
|
|
15985
|
+
response_format
|
|
15986
|
+
}) => {
|
|
14841
15987
|
const format = response_format ?? "text";
|
|
14842
15988
|
const startedAt = Date.now();
|
|
14843
15989
|
const timeoutMs = (timeout_s ?? 120) * 1e3;
|
|
@@ -14856,6 +16002,18 @@ function registerConnectionTools(server) {
|
|
|
14856
16002
|
isError: true
|
|
14857
16003
|
};
|
|
14858
16004
|
}
|
|
16005
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
16006
|
+
if (!resolvedProjectId) {
|
|
16007
|
+
return {
|
|
16008
|
+
content: [
|
|
16009
|
+
{
|
|
16010
|
+
type: "text",
|
|
16011
|
+
text: "A project_id is required to wait for a connection. Use the same project_id used to start the OAuth flow."
|
|
16012
|
+
}
|
|
16013
|
+
],
|
|
16014
|
+
isError: true
|
|
16015
|
+
};
|
|
16016
|
+
}
|
|
14859
16017
|
const activeWaits = inFlightWaitsByUser.get(userId) ?? 0;
|
|
14860
16018
|
if (activeWaits >= MAX_CONCURRENT_WAITS_PER_USER) {
|
|
14861
16019
|
return {
|
|
@@ -14877,12 +16035,62 @@ function registerConnectionTools(server) {
|
|
|
14877
16035
|
"mcp-data",
|
|
14878
16036
|
{
|
|
14879
16037
|
action: "connected-accounts",
|
|
14880
|
-
|
|
16038
|
+
projectId: resolvedProjectId,
|
|
16039
|
+
project_id: resolvedProjectId
|
|
14881
16040
|
},
|
|
14882
16041
|
{ timeoutMs: 1e4 }
|
|
14883
16042
|
);
|
|
14884
16043
|
if (!error && data?.success) {
|
|
14885
|
-
const
|
|
16044
|
+
const foundAccounts = findActiveAccounts(
|
|
16045
|
+
data.accounts ?? [],
|
|
16046
|
+
platform2,
|
|
16047
|
+
resolvedProjectId
|
|
16048
|
+
);
|
|
16049
|
+
if (foundAccounts.length > 1) {
|
|
16050
|
+
if (format === "json") {
|
|
16051
|
+
return {
|
|
16052
|
+
content: [
|
|
16053
|
+
{
|
|
16054
|
+
type: "text",
|
|
16055
|
+
text: JSON.stringify(
|
|
16056
|
+
{
|
|
16057
|
+
connected: true,
|
|
16058
|
+
platform: platform2,
|
|
16059
|
+
project_id: resolvedProjectId,
|
|
16060
|
+
accounts: foundAccounts.map((a) => ({
|
|
16061
|
+
id: a.id,
|
|
16062
|
+
username: a.username,
|
|
16063
|
+
connected_at: a.created_at
|
|
16064
|
+
})),
|
|
16065
|
+
attempts,
|
|
16066
|
+
message: `${platform2} has ${foundAccounts.length} active accounts for this project. Pass the exact account_id to schedule_post.`
|
|
16067
|
+
},
|
|
16068
|
+
null,
|
|
16069
|
+
2
|
|
16070
|
+
)
|
|
16071
|
+
}
|
|
16072
|
+
],
|
|
16073
|
+
isError: false
|
|
16074
|
+
};
|
|
16075
|
+
}
|
|
16076
|
+
return {
|
|
16077
|
+
content: [
|
|
16078
|
+
{
|
|
16079
|
+
type: "text",
|
|
16080
|
+
text: [
|
|
16081
|
+
`${platform2} is connected \u2014 ${foundAccounts.length} active accounts found for project ${resolvedProjectId}:`,
|
|
16082
|
+
...foundAccounts.map(
|
|
16083
|
+
(a) => ` ${a.username || "(unnamed)"} (id=${a.id})`
|
|
16084
|
+
),
|
|
16085
|
+
"",
|
|
16086
|
+
"Call schedule_post with the exact account_id (or account_ids) for the one you mean."
|
|
16087
|
+
].join("\n")
|
|
16088
|
+
}
|
|
16089
|
+
],
|
|
16090
|
+
isError: false
|
|
16091
|
+
};
|
|
16092
|
+
}
|
|
16093
|
+
const found = foundAccounts[0];
|
|
14886
16094
|
if (found) {
|
|
14887
16095
|
if (format === "json") {
|
|
14888
16096
|
return {
|
|
@@ -14893,7 +16101,7 @@ function registerConnectionTools(server) {
|
|
|
14893
16101
|
{
|
|
14894
16102
|
connected: true,
|
|
14895
16103
|
platform: found.platform,
|
|
14896
|
-
project_id:
|
|
16104
|
+
project_id: resolvedProjectId,
|
|
14897
16105
|
account_id: found.id,
|
|
14898
16106
|
username: found.username,
|
|
14899
16107
|
connected_at: found.created_at,
|
|
@@ -14913,7 +16121,7 @@ function registerConnectionTools(server) {
|
|
|
14913
16121
|
type: "text",
|
|
14914
16122
|
text: [
|
|
14915
16123
|
`${found.platform} is connected.`,
|
|
14916
|
-
|
|
16124
|
+
`Brand/project: ${resolvedProjectId}`,
|
|
14917
16125
|
`Account: ${found.username || "(unnamed)"} (id=${found.id})`,
|
|
14918
16126
|
`Detected after ${attempts} poll(s) in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s.`,
|
|
14919
16127
|
"Ready to call `schedule_post`."
|
|
@@ -14926,7 +16134,9 @@ function registerConnectionTools(server) {
|
|
|
14926
16134
|
}
|
|
14927
16135
|
const remaining = deadline - Date.now();
|
|
14928
16136
|
if (remaining <= 0) break;
|
|
14929
|
-
await new Promise(
|
|
16137
|
+
await new Promise(
|
|
16138
|
+
(resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining))
|
|
16139
|
+
);
|
|
14930
16140
|
}
|
|
14931
16141
|
const message = `${platform2} 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.`;
|
|
14932
16142
|
if (format === "json") {
|
|
@@ -14938,7 +16148,7 @@ function registerConnectionTools(server) {
|
|
|
14938
16148
|
{
|
|
14939
16149
|
connected: false,
|
|
14940
16150
|
platform: platform2,
|
|
14941
|
-
project_id:
|
|
16151
|
+
project_id: resolvedProjectId,
|
|
14942
16152
|
attempts,
|
|
14943
16153
|
timed_out: true,
|
|
14944
16154
|
message
|
|
@@ -14968,6 +16178,7 @@ function registerConnectionTools(server) {
|
|
|
14968
16178
|
}
|
|
14969
16179
|
|
|
14970
16180
|
// src/tools/harness.ts
|
|
16181
|
+
init_edge_function();
|
|
14971
16182
|
import { z as z34 } from "zod";
|
|
14972
16183
|
var ALLOWED_AGENTS = [
|
|
14973
16184
|
"conductor",
|
|
@@ -15105,6 +16316,7 @@ function registerHarnessTools(server, _ctx) {
|
|
|
15105
16316
|
}
|
|
15106
16317
|
|
|
15107
16318
|
// src/tools/hermes.ts
|
|
16319
|
+
init_edge_function();
|
|
15108
16320
|
import { z as z35 } from "zod";
|
|
15109
16321
|
function asEnvelope25(data) {
|
|
15110
16322
|
return {
|
|
@@ -15382,6 +16594,7 @@ ${"=".repeat(40)}
|
|
|
15382
16594
|
|
|
15383
16595
|
// src/tools/skills.ts
|
|
15384
16596
|
import { z as z36 } from "zod";
|
|
16597
|
+
init_edge_function();
|
|
15385
16598
|
|
|
15386
16599
|
// src/lib/skills-manifest.ts
|
|
15387
16600
|
var SKILLS_MANIFEST = [
|
|
@@ -15724,6 +16937,7 @@ ${skill.compiled_section}` : "";
|
|
|
15724
16937
|
}
|
|
15725
16938
|
|
|
15726
16939
|
// src/tools/loopPulse.ts
|
|
16940
|
+
init_edge_function();
|
|
15727
16941
|
import { z as z37 } from "zod";
|
|
15728
16942
|
function asEnvelope27(data) {
|
|
15729
16943
|
return {
|
|
@@ -15779,8 +16993,9 @@ function registerLoopPulseTools(server) {
|
|
|
15779
16993
|
}
|
|
15780
16994
|
|
|
15781
16995
|
// src/tools/banditState.ts
|
|
15782
|
-
|
|
16996
|
+
init_edge_function();
|
|
15783
16997
|
init_supabase();
|
|
16998
|
+
import { z as z38 } from "zod";
|
|
15784
16999
|
function asEnvelope28(data) {
|
|
15785
17000
|
return {
|
|
15786
17001
|
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
@@ -15876,8 +17091,9 @@ function registerBanditStateTools(server) {
|
|
|
15876
17091
|
}
|
|
15877
17092
|
|
|
15878
17093
|
// src/tools/lifecycle.ts
|
|
15879
|
-
|
|
17094
|
+
init_edge_function();
|
|
15880
17095
|
init_supabase();
|
|
17096
|
+
import { z as z39 } from "zod";
|
|
15881
17097
|
function envelope(data) {
|
|
15882
17098
|
return {
|
|
15883
17099
|
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
@@ -16440,6 +17656,7 @@ Deliver a report covering:
|
|
|
16440
17656
|
}
|
|
16441
17657
|
|
|
16442
17658
|
// src/resources.ts
|
|
17659
|
+
init_edge_function();
|
|
16443
17660
|
function registerResources(server) {
|
|
16444
17661
|
server.resource(
|
|
16445
17662
|
"brand-profile",
|
|
@@ -17341,6 +18558,7 @@ var MAX_METADATA_BYTES = 8192;
|
|
|
17341
18558
|
var MAX_CACHED_CLIENTS = 1e3;
|
|
17342
18559
|
var MAX_PERSISTED_CLIENTS = 5e3;
|
|
17343
18560
|
var OAUTH_CLIENT_RETENTION_DAYS = 90;
|
|
18561
|
+
var OAUTH_CLIENT_TOUCH_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
17344
18562
|
function byteLength(value) {
|
|
17345
18563
|
return Buffer.byteLength(value, "utf8");
|
|
17346
18564
|
}
|
|
@@ -17363,6 +18581,7 @@ function assertRegistrationWithinBounds(client3) {
|
|
|
17363
18581
|
}
|
|
17364
18582
|
}
|
|
17365
18583
|
function cacheClient(cache, clientId, client3) {
|
|
18584
|
+
const evictedClientIds = [];
|
|
17366
18585
|
if (cache.has(clientId)) {
|
|
17367
18586
|
cache.delete(clientId);
|
|
17368
18587
|
}
|
|
@@ -17371,7 +18590,9 @@ function cacheClient(cache, clientId, client3) {
|
|
|
17371
18590
|
const oldestClientId = cache.keys().next().value;
|
|
17372
18591
|
if (!oldestClientId) break;
|
|
17373
18592
|
cache.delete(oldestClientId);
|
|
18593
|
+
evictedClientIds.push(oldestClientId);
|
|
17374
18594
|
}
|
|
18595
|
+
return evictedClientIds;
|
|
17375
18596
|
}
|
|
17376
18597
|
function rowToClient(row) {
|
|
17377
18598
|
return {
|
|
@@ -17406,6 +18627,7 @@ function clientToRow(c) {
|
|
|
17406
18627
|
}
|
|
17407
18628
|
function createClientsStore() {
|
|
17408
18629
|
const cache = /* @__PURE__ */ new Map();
|
|
18630
|
+
const lastClientTouch = /* @__PURE__ */ new Map();
|
|
17409
18631
|
let supabaseAvailable = true;
|
|
17410
18632
|
function markUnavailable(reason) {
|
|
17411
18633
|
if (supabaseAvailable) {
|
|
@@ -17415,14 +18637,50 @@ function createClientsStore() {
|
|
|
17415
18637
|
supabaseAvailable = false;
|
|
17416
18638
|
}
|
|
17417
18639
|
}
|
|
18640
|
+
function clearEvictedClientTouches(evictedClientIds) {
|
|
18641
|
+
for (const evictedClientId of evictedClientIds) {
|
|
18642
|
+
lastClientTouch.delete(evictedClientId);
|
|
18643
|
+
}
|
|
18644
|
+
}
|
|
18645
|
+
function touchClientActivity(clientId) {
|
|
18646
|
+
if (!supabaseAvailable) return;
|
|
18647
|
+
const now = Date.now();
|
|
18648
|
+
const lastTouchedAt = lastClientTouch.get(clientId);
|
|
18649
|
+
if (lastTouchedAt !== void 0 && now - lastTouchedAt < OAUTH_CLIENT_TOUCH_INTERVAL_MS) {
|
|
18650
|
+
return;
|
|
18651
|
+
}
|
|
18652
|
+
lastClientTouch.set(clientId, now);
|
|
18653
|
+
const releaseTouchReservation = () => {
|
|
18654
|
+
if (lastClientTouch.get(clientId) === now) {
|
|
18655
|
+
lastClientTouch.delete(clientId);
|
|
18656
|
+
}
|
|
18657
|
+
};
|
|
18658
|
+
void (async () => {
|
|
18659
|
+
try {
|
|
18660
|
+
const supabase = getSupabaseClient();
|
|
18661
|
+
const { error: touchError } = await supabase.from("mcp_oauth_clients").update({ last_used_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("client_id", clientId);
|
|
18662
|
+
if (touchError) {
|
|
18663
|
+
releaseTouchReservation();
|
|
18664
|
+
console.error(
|
|
18665
|
+
`[oauth] failed to update client activity: ${sanitizeError(touchError)}`
|
|
18666
|
+
);
|
|
18667
|
+
}
|
|
18668
|
+
} catch (touchError) {
|
|
18669
|
+
releaseTouchReservation();
|
|
18670
|
+
console.error(`[oauth] failed to update client activity: ${sanitizeError(touchError)}`);
|
|
18671
|
+
}
|
|
18672
|
+
})();
|
|
18673
|
+
}
|
|
17418
18674
|
return {
|
|
17419
18675
|
async getClient(clientId) {
|
|
17420
18676
|
const cached3 = cache.get(clientId);
|
|
17421
18677
|
if (cached3) {
|
|
17422
18678
|
if (!hasAllowedRedirectUris(cached3)) {
|
|
17423
18679
|
cache.delete(clientId);
|
|
18680
|
+
lastClientTouch.delete(clientId);
|
|
17424
18681
|
return void 0;
|
|
17425
18682
|
}
|
|
18683
|
+
touchClientActivity(clientId);
|
|
17426
18684
|
return cached3;
|
|
17427
18685
|
}
|
|
17428
18686
|
if (!supabaseAvailable) return void 0;
|
|
@@ -17444,8 +18702,8 @@ function createClientsStore() {
|
|
|
17444
18702
|
}
|
|
17445
18703
|
return void 0;
|
|
17446
18704
|
}
|
|
17447
|
-
cacheClient(cache, clientId, client3);
|
|
17448
|
-
|
|
18705
|
+
clearEvictedClientTouches(cacheClient(cache, clientId, client3));
|
|
18706
|
+
touchClientActivity(clientId);
|
|
17449
18707
|
return client3;
|
|
17450
18708
|
} catch (err) {
|
|
17451
18709
|
markUnavailable(err instanceof Error ? err.message : "unknown error");
|
|
@@ -17491,7 +18749,8 @@ function createClientsStore() {
|
|
|
17491
18749
|
markUnavailable(err instanceof Error ? err.message : "unknown error");
|
|
17492
18750
|
}
|
|
17493
18751
|
}
|
|
17494
|
-
|
|
18752
|
+
lastClientTouch.delete(client3.client_id);
|
|
18753
|
+
clearEvictedClientTouches(cacheClient(cache, client3.client_id, client3));
|
|
17495
18754
|
return client3;
|
|
17496
18755
|
}
|
|
17497
18756
|
};
|