@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/sn.js
CHANGED
|
@@ -19,7 +19,7 @@ var MCP_VERSION;
|
|
|
19
19
|
var init_version = __esm({
|
|
20
20
|
"src/lib/version.ts"() {
|
|
21
21
|
"use strict";
|
|
22
|
-
MCP_VERSION = "1.
|
|
22
|
+
MCP_VERSION = "1.9.1";
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -56,6 +56,209 @@ var init_request_context = __esm({
|
|
|
56
56
|
}
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
+
// src/lib/edge-function.ts
|
|
60
|
+
var edge_function_exports = {};
|
|
61
|
+
__export(edge_function_exports, {
|
|
62
|
+
callEdgeFunction: () => callEdgeFunction
|
|
63
|
+
});
|
|
64
|
+
function safeGatewayError(responseText, status) {
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(responseText);
|
|
67
|
+
const nested = parsed.error && typeof parsed.error === "object" ? parsed.error : null;
|
|
68
|
+
const candidates = [
|
|
69
|
+
nested?.error_type,
|
|
70
|
+
nested?.code,
|
|
71
|
+
parsed.error_type,
|
|
72
|
+
parsed.error_code,
|
|
73
|
+
parsed.code,
|
|
74
|
+
typeof parsed.error === "string" ? parsed.error : null
|
|
75
|
+
];
|
|
76
|
+
for (const value of candidates) {
|
|
77
|
+
if (typeof value !== "string") continue;
|
|
78
|
+
const normalized = value.trim().toLowerCase();
|
|
79
|
+
if (SAFE_GATEWAY_ERROR_CODES.has(normalized)) return normalized;
|
|
80
|
+
const embedded = [...SAFE_GATEWAY_ERROR_CODES].find((code) => normalized.includes(code));
|
|
81
|
+
if (embedded) return embedded;
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
return `Backend request failed (HTTP ${status}).`;
|
|
86
|
+
}
|
|
87
|
+
function safeFailureData(responseText) {
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(responseText);
|
|
90
|
+
const metric = (name) => {
|
|
91
|
+
const value = parsed[name];
|
|
92
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
93
|
+
};
|
|
94
|
+
const billingStatus = typeof parsed.billing_status === "string" && SAFE_BILLING_STATUSES.has(parsed.billing_status) ? parsed.billing_status : void 0;
|
|
95
|
+
const failureReason = parsed.failure_reason === "generation_failed" || parsed.failure_reason === "authentication_failed" || parsed.failure_reason === "cancelled_by_user" ? parsed.failure_reason : void 0;
|
|
96
|
+
const jobStatus = typeof parsed.status === "string" && SAFE_JOB_STATUSES.has(parsed.status) ? parsed.status : void 0;
|
|
97
|
+
const safe = {
|
|
98
|
+
...jobStatus ? { status: jobStatus } : {},
|
|
99
|
+
...metric("credits_reserved") !== void 0 ? { credits_reserved: metric("credits_reserved") } : {},
|
|
100
|
+
...metric("credits_charged") !== void 0 ? { credits_charged: metric("credits_charged") } : {},
|
|
101
|
+
...metric("credits_refunded") !== void 0 ? { credits_refunded: metric("credits_refunded") } : {},
|
|
102
|
+
...billingStatus ? { billing_status: billingStatus } : {},
|
|
103
|
+
...failureReason ? { failure_reason: failureReason } : {}
|
|
104
|
+
};
|
|
105
|
+
return Object.keys(safe).length > 0 ? safe : null;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function getApiKeyOrNull() {
|
|
111
|
+
const envKey = process.env.SOCIALNEURON_API_KEY;
|
|
112
|
+
if (envKey && envKey.trim().length) return envKey.trim();
|
|
113
|
+
const requestToken = getRequestToken();
|
|
114
|
+
if (requestToken) return requestToken;
|
|
115
|
+
return getAuthenticatedApiKey();
|
|
116
|
+
}
|
|
117
|
+
async function callEdgeFunction(functionName, body, options) {
|
|
118
|
+
const supabaseUrl = getSupabaseUrl();
|
|
119
|
+
const apiKey = getApiKeyOrNull();
|
|
120
|
+
const controller = new AbortController();
|
|
121
|
+
const timeoutMs = options?.timeoutMs ?? 6e4;
|
|
122
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
123
|
+
const enrichedBody = { ...body };
|
|
124
|
+
if (!enrichedBody.userId && !enrichedBody.user_id) {
|
|
125
|
+
try {
|
|
126
|
+
const defaultId = await getDefaultUserId();
|
|
127
|
+
enrichedBody.userId = defaultId;
|
|
128
|
+
enrichedBody.user_id = defaultId;
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
if (enrichedBody.userId && !enrichedBody.user_id) enrichedBody.user_id = enrichedBody.userId;
|
|
133
|
+
if (enrichedBody.user_id && !enrichedBody.userId) enrichedBody.userId = enrichedBody.user_id;
|
|
134
|
+
}
|
|
135
|
+
if (!enrichedBody.projectId && !enrichedBody.project_id) {
|
|
136
|
+
try {
|
|
137
|
+
const { getDefaultProjectId: getDefaultProjectId2 } = await Promise.resolve().then(() => (init_supabase(), supabase_exports));
|
|
138
|
+
const defaultProjectId = await getDefaultProjectId2();
|
|
139
|
+
if (defaultProjectId) {
|
|
140
|
+
enrichedBody.projectId = defaultProjectId;
|
|
141
|
+
enrichedBody.project_id = defaultProjectId;
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
let url;
|
|
147
|
+
let method = options?.method ?? "POST";
|
|
148
|
+
let headers;
|
|
149
|
+
let requestBody;
|
|
150
|
+
if (!apiKey) {
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
return {
|
|
153
|
+
data: null,
|
|
154
|
+
error: "Not authenticated. Run: npx @socialneuron/mcp-server login \u2014 Requires a paid plan (Starter+). See https://socialneuron.com/pricing"
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
url = new URL(`${supabaseUrl}/functions/v1/mcp-gateway`);
|
|
158
|
+
headers = {
|
|
159
|
+
Authorization: `Bearer ${apiKey}`,
|
|
160
|
+
"Content-Type": "application/json"
|
|
161
|
+
};
|
|
162
|
+
requestBody = {
|
|
163
|
+
functionName,
|
|
164
|
+
body: enrichedBody,
|
|
165
|
+
query: options?.query,
|
|
166
|
+
method: method.toUpperCase(),
|
|
167
|
+
timeoutMs
|
|
168
|
+
};
|
|
169
|
+
method = "POST";
|
|
170
|
+
try {
|
|
171
|
+
const response = await fetch(url.toString(), {
|
|
172
|
+
method,
|
|
173
|
+
headers,
|
|
174
|
+
body: method === "GET" ? void 0 : JSON.stringify(requestBody),
|
|
175
|
+
signal: controller.signal
|
|
176
|
+
});
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
const responseText = await response.text();
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
const errorCode = safeGatewayError(responseText, response.status);
|
|
181
|
+
const failureData = safeFailureData(responseText);
|
|
182
|
+
if (response.status === 401) {
|
|
183
|
+
return {
|
|
184
|
+
data: failureData,
|
|
185
|
+
error: `Authentication failed (HTTP 401). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (response.status === 403) {
|
|
189
|
+
return {
|
|
190
|
+
data: failureData,
|
|
191
|
+
error: `Forbidden (HTTP 403): ${errorCode}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (response.status === 429) {
|
|
195
|
+
const retryAfter = response.headers.get("retry-after") || "60";
|
|
196
|
+
return {
|
|
197
|
+
data: failureData,
|
|
198
|
+
error: `Rate limit exceeded (HTTP 429). Wait ${retryAfter}s before retrying. Reduce request frequency or upgrade your plan.`
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return { data: failureData, error: errorCode };
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const data = JSON.parse(responseText);
|
|
205
|
+
return { data, error: null };
|
|
206
|
+
} catch {
|
|
207
|
+
return { data: { text: responseText }, error: null };
|
|
208
|
+
}
|
|
209
|
+
} catch (err) {
|
|
210
|
+
clearTimeout(timer);
|
|
211
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
212
|
+
return {
|
|
213
|
+
data: null,
|
|
214
|
+
error: `Edge Function '${functionName}' timed out after ${timeoutMs}ms`
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return { data: null, error: "Network request failed. Please retry." };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
var SAFE_GATEWAY_ERROR_CODES, SAFE_BILLING_STATUSES, SAFE_JOB_STATUSES;
|
|
221
|
+
var init_edge_function = __esm({
|
|
222
|
+
"src/lib/edge-function.ts"() {
|
|
223
|
+
"use strict";
|
|
224
|
+
init_supabase();
|
|
225
|
+
init_request_context();
|
|
226
|
+
SAFE_GATEWAY_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
227
|
+
"daily_limit_reached",
|
|
228
|
+
"insufficient_credits",
|
|
229
|
+
"project_scope_mismatch",
|
|
230
|
+
"schedule_conflict",
|
|
231
|
+
"post_not_found",
|
|
232
|
+
"post_not_reschedulable",
|
|
233
|
+
"post_in_progress",
|
|
234
|
+
"not_cancellable",
|
|
235
|
+
"publishing_in_progress",
|
|
236
|
+
"plan_upgrade_required",
|
|
237
|
+
"rate_limited",
|
|
238
|
+
"validation_error",
|
|
239
|
+
"permission_denied",
|
|
240
|
+
"not_found"
|
|
241
|
+
]);
|
|
242
|
+
SAFE_BILLING_STATUSES = /* @__PURE__ */ new Set([
|
|
243
|
+
"reserved",
|
|
244
|
+
"charged",
|
|
245
|
+
"refunded",
|
|
246
|
+
"failed_no_charge",
|
|
247
|
+
"refund_pending",
|
|
248
|
+
"not_charged"
|
|
249
|
+
]);
|
|
250
|
+
SAFE_JOB_STATUSES = /* @__PURE__ */ new Set([
|
|
251
|
+
"queued",
|
|
252
|
+
"pending",
|
|
253
|
+
"processing",
|
|
254
|
+
"completed",
|
|
255
|
+
"failed",
|
|
256
|
+
"cancelled",
|
|
257
|
+
"canceled"
|
|
258
|
+
]);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
59
262
|
// src/cli/credentials.ts
|
|
60
263
|
var credentials_exports = {};
|
|
61
264
|
__export(credentials_exports, {
|
|
@@ -82,6 +285,10 @@ import {
|
|
|
82
285
|
} from "node:fs";
|
|
83
286
|
import { homedir, platform } from "node:os";
|
|
84
287
|
import { join } from "node:path";
|
|
288
|
+
function loadNativeKeyring() {
|
|
289
|
+
nativeKeyringModule ??= import("@napi-rs/keyring").catch(() => null);
|
|
290
|
+
return nativeKeyringModule;
|
|
291
|
+
}
|
|
85
292
|
function assertSafeCredentialPaths() {
|
|
86
293
|
if (platform() === "win32") return;
|
|
87
294
|
const uid = process.getuid?.();
|
|
@@ -201,50 +408,91 @@ function writeCredentialsFile(data) {
|
|
|
201
408
|
}
|
|
202
409
|
hardenCredentialPermissions();
|
|
203
410
|
}
|
|
204
|
-
function
|
|
411
|
+
function isMacKeychainItemMissing(error) {
|
|
412
|
+
const commandError = error;
|
|
413
|
+
if (commandError?.status === 44) return true;
|
|
414
|
+
const detail = `${commandError?.stderr ?? ""}
|
|
415
|
+
${commandError?.message ?? ""}`;
|
|
416
|
+
return /\berrsecitemnotfound\b/i.test(detail) || /(?:^|:\s*)the specified item could not be found in the keychain\.\s*$/i.test(detail.trim());
|
|
417
|
+
}
|
|
418
|
+
async function inspectMacKeychain(service) {
|
|
419
|
+
const native = await loadNativeKeyring();
|
|
420
|
+
if (native) {
|
|
421
|
+
try {
|
|
422
|
+
const value = new native.Entry(service, KEYCHAIN_ACCOUNT).getPassword();
|
|
423
|
+
if (value) return { status: "found", value };
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
426
|
+
}
|
|
205
427
|
try {
|
|
206
428
|
const result = execFileSync(
|
|
207
429
|
"security",
|
|
208
430
|
["find-generic-password", "-a", KEYCHAIN_ACCOUNT, "-s", service, "-w"],
|
|
209
431
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
210
432
|
);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
433
|
+
const value = result.trim();
|
|
434
|
+
return value ? { status: "found", value } : { status: "missing" };
|
|
435
|
+
} catch (error) {
|
|
436
|
+
return isMacKeychainItemMissing(error) ? { status: "missing" } : { status: "unavailable" };
|
|
214
437
|
}
|
|
215
438
|
}
|
|
216
|
-
function
|
|
439
|
+
async function macKeychainRead(service) {
|
|
440
|
+
const result = await inspectMacKeychain(service);
|
|
441
|
+
return result.status === "found" ? result.value : null;
|
|
442
|
+
}
|
|
443
|
+
async function macKeychainWrite(service, value) {
|
|
444
|
+
const native = await loadNativeKeyring();
|
|
445
|
+
if (!native) return false;
|
|
217
446
|
try {
|
|
218
|
-
|
|
219
|
-
"security",
|
|
220
|
-
[
|
|
221
|
-
"add-generic-password",
|
|
222
|
-
"-a",
|
|
223
|
-
KEYCHAIN_ACCOUNT,
|
|
224
|
-
"-s",
|
|
225
|
-
service,
|
|
226
|
-
"-w",
|
|
227
|
-
value,
|
|
228
|
-
"-U"
|
|
229
|
-
// update if exists
|
|
230
|
-
],
|
|
231
|
-
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
232
|
-
);
|
|
447
|
+
new native.Entry(service, KEYCHAIN_ACCOUNT).setPassword(value);
|
|
233
448
|
return true;
|
|
234
449
|
} catch {
|
|
235
450
|
return false;
|
|
236
451
|
}
|
|
237
452
|
}
|
|
238
|
-
function macKeychainDelete(service) {
|
|
453
|
+
async function macKeychainDelete(service) {
|
|
454
|
+
const native = await loadNativeKeyring();
|
|
455
|
+
let nativeDeleted = false;
|
|
456
|
+
if (native) {
|
|
457
|
+
try {
|
|
458
|
+
nativeDeleted = new native.Entry(service, KEYCHAIN_ACCOUNT).deletePassword();
|
|
459
|
+
} catch {
|
|
460
|
+
}
|
|
461
|
+
}
|
|
239
462
|
try {
|
|
240
463
|
execFileSync("security", ["delete-generic-password", "-a", KEYCHAIN_ACCOUNT, "-s", service], {
|
|
241
464
|
stdio: ["pipe", "pipe", "pipe"]
|
|
242
465
|
});
|
|
243
|
-
return
|
|
244
|
-
} catch {
|
|
245
|
-
return
|
|
466
|
+
return "deleted";
|
|
467
|
+
} catch (error) {
|
|
468
|
+
if (isMacKeychainItemMissing(error)) return nativeDeleted ? "deleted" : "missing";
|
|
469
|
+
return "unavailable";
|
|
246
470
|
}
|
|
247
471
|
}
|
|
472
|
+
async function clearMacKeychain(service) {
|
|
473
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
474
|
+
const deleted = await macKeychainDelete(service);
|
|
475
|
+
if (deleted === "unavailable") break;
|
|
476
|
+
const remaining = await inspectMacKeychain(service);
|
|
477
|
+
if (remaining.status === "missing") return;
|
|
478
|
+
if (remaining.status === "unavailable") break;
|
|
479
|
+
}
|
|
480
|
+
throw new Error(
|
|
481
|
+
"Unable to verify removal of the existing Social Neuron Keychain credential. Unlock Keychain Access, remove the item, and retry."
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
async function verifyMacFileFallback(service) {
|
|
485
|
+
const existing = await inspectMacKeychain(service);
|
|
486
|
+
if (existing.status === "missing") return;
|
|
487
|
+
if (existing.status === "found") {
|
|
488
|
+
throw new Error(
|
|
489
|
+
"An existing Social Neuron Keychain credential could not be replaced. The existing value was retained; unlock or remove it in Keychain Access and retry."
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
throw new Error(
|
|
493
|
+
"Unable to verify absence of an existing Social Neuron Keychain credential. Unlock Keychain Access and retry."
|
|
494
|
+
);
|
|
495
|
+
}
|
|
248
496
|
function linuxSecretRead(key) {
|
|
249
497
|
try {
|
|
250
498
|
const result = execFileSync(
|
|
@@ -284,7 +532,7 @@ async function loadApiKey() {
|
|
|
284
532
|
if (envKey) return envKey;
|
|
285
533
|
const os = platform();
|
|
286
534
|
if (os === "darwin") {
|
|
287
|
-
const key = macKeychainRead(KEYCHAIN_SERVICE_API);
|
|
535
|
+
const key = await macKeychainRead(KEYCHAIN_SERVICE_API);
|
|
288
536
|
if (key) return key;
|
|
289
537
|
} else if (os === "linux") {
|
|
290
538
|
const key = linuxSecretRead("api-key");
|
|
@@ -301,13 +549,18 @@ async function loadApiKey() {
|
|
|
301
549
|
async function saveApiKey(key) {
|
|
302
550
|
const os = platform();
|
|
303
551
|
let saved = false;
|
|
552
|
+
let fallbackCredentials;
|
|
304
553
|
if (os === "darwin") {
|
|
305
|
-
saved = macKeychainWrite(KEYCHAIN_SERVICE_API, key);
|
|
554
|
+
saved = await macKeychainWrite(KEYCHAIN_SERVICE_API, key);
|
|
555
|
+
if (!saved) {
|
|
556
|
+
fallbackCredentials = readCredentialsFile();
|
|
557
|
+
await verifyMacFileFallback(KEYCHAIN_SERVICE_API);
|
|
558
|
+
}
|
|
306
559
|
} else if (os === "linux") {
|
|
307
560
|
saved = linuxSecretWrite("api-key", key);
|
|
308
561
|
}
|
|
309
562
|
if (!saved) {
|
|
310
|
-
const creds = readCredentialsFile();
|
|
563
|
+
const creds = fallbackCredentials ?? readCredentialsFile();
|
|
311
564
|
creds.apiKey = key;
|
|
312
565
|
writeCredentialsFile(creds);
|
|
313
566
|
if (os === "win32") {
|
|
@@ -330,8 +583,13 @@ async function saveApiKey(key) {
|
|
|
330
583
|
}
|
|
331
584
|
async function deleteApiKey() {
|
|
332
585
|
const os = platform();
|
|
586
|
+
let keychainError;
|
|
333
587
|
if (os === "darwin") {
|
|
334
|
-
|
|
588
|
+
try {
|
|
589
|
+
await clearMacKeychain(KEYCHAIN_SERVICE_API);
|
|
590
|
+
} catch (error) {
|
|
591
|
+
keychainError = error;
|
|
592
|
+
}
|
|
335
593
|
} else if (os === "linux") {
|
|
336
594
|
linuxSecretDelete("api-key");
|
|
337
595
|
}
|
|
@@ -347,13 +605,14 @@ async function deleteApiKey() {
|
|
|
347
605
|
writeCredentialsFile(creds);
|
|
348
606
|
}
|
|
349
607
|
}
|
|
608
|
+
if (keychainError) throw keychainError;
|
|
350
609
|
}
|
|
351
610
|
async function loadSupabaseUrl() {
|
|
352
611
|
const envUrl = process.env.SOCIALNEURON_SUPABASE_URL || process.env.SUPABASE_URL;
|
|
353
612
|
if (envUrl) return envUrl;
|
|
354
613
|
const os = platform();
|
|
355
614
|
if (os === "darwin") {
|
|
356
|
-
const url = macKeychainRead(KEYCHAIN_SERVICE_URL);
|
|
615
|
+
const url = await macKeychainRead(KEYCHAIN_SERVICE_URL);
|
|
357
616
|
if (url) return url;
|
|
358
617
|
} else if (os === "linux") {
|
|
359
618
|
const url = linuxSecretRead("supabase-url");
|
|
@@ -365,18 +624,23 @@ async function loadSupabaseUrl() {
|
|
|
365
624
|
async function saveSupabaseUrl(url) {
|
|
366
625
|
const os = platform();
|
|
367
626
|
let saved = false;
|
|
627
|
+
let fallbackCredentials;
|
|
368
628
|
if (os === "darwin") {
|
|
369
|
-
saved = macKeychainWrite(KEYCHAIN_SERVICE_URL, url);
|
|
629
|
+
saved = await macKeychainWrite(KEYCHAIN_SERVICE_URL, url);
|
|
630
|
+
if (!saved) {
|
|
631
|
+
fallbackCredentials = readCredentialsFile();
|
|
632
|
+
await verifyMacFileFallback(KEYCHAIN_SERVICE_URL);
|
|
633
|
+
}
|
|
370
634
|
} else if (os === "linux") {
|
|
371
635
|
saved = linuxSecretWrite("supabase-url", url);
|
|
372
636
|
}
|
|
373
637
|
if (!saved) {
|
|
374
|
-
const creds = readCredentialsFile();
|
|
638
|
+
const creds = fallbackCredentials ?? readCredentialsFile();
|
|
375
639
|
creds.supabaseUrl = url;
|
|
376
640
|
writeCredentialsFile(creds);
|
|
377
641
|
}
|
|
378
642
|
}
|
|
379
|
-
var KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE_API, KEYCHAIN_SERVICE_URL, CONFIG_DIR, CREDENTIALS_FILE;
|
|
643
|
+
var KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE_API, KEYCHAIN_SERVICE_URL, CONFIG_DIR, CREDENTIALS_FILE, nativeKeyringModule;
|
|
380
644
|
var init_credentials = __esm({
|
|
381
645
|
"src/cli/credentials.ts"() {
|
|
382
646
|
"use strict";
|
|
@@ -529,7 +793,10 @@ __export(supabase_exports, {
|
|
|
529
793
|
getSupabaseUrl: () => getSupabaseUrl,
|
|
530
794
|
initializeAuth: () => initializeAuth,
|
|
531
795
|
isTelemetryDisabled: () => isTelemetryDisabled,
|
|
532
|
-
|
|
796
|
+
listAccessibleProjectsWithAccountStatus: () => listAccessibleProjectsWithAccountStatus,
|
|
797
|
+
logMcpToolInvocation: () => logMcpToolInvocation,
|
|
798
|
+
resolveProjectForConnectedAccountTool: () => resolveProjectForConnectedAccountTool,
|
|
799
|
+
resolveProjectStrict: () => resolveProjectStrict
|
|
533
800
|
});
|
|
534
801
|
import { createClient } from "@supabase/supabase-js";
|
|
535
802
|
import { randomUUID } from "node:crypto";
|
|
@@ -568,20 +835,17 @@ async function getDefaultUserId() {
|
|
|
568
835
|
if (authenticatedUserId) return authenticatedUserId;
|
|
569
836
|
const envUserId = process.env.SOCIALNEURON_USER_ID;
|
|
570
837
|
if (envUserId) return envUserId;
|
|
571
|
-
throw new Error(
|
|
838
|
+
throw new Error(
|
|
839
|
+
"No user ID available. Set SOCIALNEURON_USER_ID or authenticate via API key."
|
|
840
|
+
);
|
|
572
841
|
}
|
|
573
842
|
async function getDefaultProjectId() {
|
|
574
843
|
const requestProjectId = getRequestProjectId();
|
|
575
844
|
if (requestProjectId) return requestProjectId;
|
|
576
845
|
if (authenticatedProjectId) return authenticatedProjectId;
|
|
577
846
|
const userId = await getDefaultUserId().catch(() => null);
|
|
578
|
-
if (userId) {
|
|
579
|
-
const cached = projectIdCache.get(userId);
|
|
580
|
-
if (cached) return cached;
|
|
581
|
-
}
|
|
582
847
|
const envProjectId = process.env.SOCIALNEURON_PROJECT_ID;
|
|
583
848
|
if (envProjectId) {
|
|
584
|
-
if (userId) projectIdCache.set(userId, envProjectId);
|
|
585
849
|
return envProjectId;
|
|
586
850
|
}
|
|
587
851
|
if (!userId) return null;
|
|
@@ -590,15 +854,126 @@ async function getDefaultProjectId() {
|
|
|
590
854
|
const { data: memberships } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId);
|
|
591
855
|
const orgIds = (memberships ?? []).map((m) => m.organization_id);
|
|
592
856
|
if (orgIds.length === 0) return null;
|
|
593
|
-
const { data } = await supabase.from("projects").select("id").in("organization_id", orgIds).order("created_at", { ascending: false }).limit(
|
|
594
|
-
if (data?.
|
|
595
|
-
projectIdCache.set(userId, data.id);
|
|
596
|
-
return data.id;
|
|
597
|
-
}
|
|
857
|
+
const { data } = await supabase.from("projects").select("id").in("organization_id", orgIds).order("created_at", { ascending: false }).limit(2);
|
|
858
|
+
if (data?.length === 1) return data[0].id;
|
|
598
859
|
} catch {
|
|
599
860
|
}
|
|
600
861
|
return null;
|
|
601
862
|
}
|
|
863
|
+
function normalizePlatformFilter(platform3) {
|
|
864
|
+
if (!platform3) return null;
|
|
865
|
+
const values = Array.isArray(platform3) ? platform3 : [platform3];
|
|
866
|
+
const set = new Set(values.filter(Boolean).map((p) => p.toLowerCase()));
|
|
867
|
+
return set.size > 0 ? set : null;
|
|
868
|
+
}
|
|
869
|
+
async function listAccessibleProjectsWithAccountStatusDirect(userId, platform3) {
|
|
870
|
+
try {
|
|
871
|
+
const supabase = getSupabaseClient();
|
|
872
|
+
const { data: memberships } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId);
|
|
873
|
+
const orgIds = (memberships ?? []).map((m) => m.organization_id);
|
|
874
|
+
if (orgIds.length === 0) return [];
|
|
875
|
+
const { data: projects } = await supabase.from("projects").select("id, name").in("organization_id", orgIds).order("created_at", { ascending: false });
|
|
876
|
+
const projectRows = projects ?? [];
|
|
877
|
+
if (projectRows.length === 0) return [];
|
|
878
|
+
const projectIds = projectRows.map((p) => p.id);
|
|
879
|
+
const { data: accounts } = await supabase.from("connected_accounts").select("project_id, status, platform").eq("user_id", userId).in("project_id", projectIds);
|
|
880
|
+
const anyAccountByProject = /* @__PURE__ */ new Set();
|
|
881
|
+
const platformsByProject = /* @__PURE__ */ new Map();
|
|
882
|
+
for (const row of accounts ?? []) {
|
|
883
|
+
if (row.status !== "active" && row.status !== "expires_soon") continue;
|
|
884
|
+
if (!row.project_id) continue;
|
|
885
|
+
anyAccountByProject.add(row.project_id);
|
|
886
|
+
if (row.platform) {
|
|
887
|
+
const set = platformsByProject.get(row.project_id) ?? /* @__PURE__ */ new Set();
|
|
888
|
+
set.add(row.platform.toLowerCase());
|
|
889
|
+
platformsByProject.set(row.project_id, set);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
const requestedPlatforms = normalizePlatformFilter(platform3);
|
|
893
|
+
return projectRows.map((p) => {
|
|
894
|
+
const platforms = Array.from(platformsByProject.get(p.id) ?? []);
|
|
895
|
+
const hasConnectedAccounts = requestedPlatforms ? platforms.some((pl) => requestedPlatforms.has(pl)) : anyAccountByProject.has(p.id);
|
|
896
|
+
return { id: p.id, name: p.name, hasConnectedAccounts, platforms };
|
|
897
|
+
});
|
|
898
|
+
} catch {
|
|
899
|
+
return [];
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
async function listAccessibleProjectsWithAccountStatusViaEdgeFunction(userId, platform3) {
|
|
903
|
+
try {
|
|
904
|
+
const { callEdgeFunction: callEdgeFunction2 } = await Promise.resolve().then(() => (init_edge_function(), edge_function_exports));
|
|
905
|
+
const { data, error } = await callEdgeFunction2(
|
|
906
|
+
"mcp-data",
|
|
907
|
+
{ action: "projects", userId, user_id: userId },
|
|
908
|
+
{ timeoutMs: 1e4 }
|
|
909
|
+
);
|
|
910
|
+
if (error || !data?.success || !Array.isArray(data.projects)) return [];
|
|
911
|
+
const requestedPlatforms = normalizePlatformFilter(platform3);
|
|
912
|
+
return data.projects.map((p) => {
|
|
913
|
+
const platforms = (p.platforms ?? []).map((pl) => pl.toLowerCase());
|
|
914
|
+
const hasConnectedAccounts = requestedPlatforms ? platforms.some((pl) => requestedPlatforms.has(pl)) : Boolean(p.hasConnectedAccounts);
|
|
915
|
+
return { id: p.id, name: p.name, hasConnectedAccounts, platforms };
|
|
916
|
+
});
|
|
917
|
+
} catch {
|
|
918
|
+
return [];
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
async function listAccessibleProjectsWithAccountStatus(userId, platform3) {
|
|
922
|
+
if (getServiceKeyOrNull()) {
|
|
923
|
+
return listAccessibleProjectsWithAccountStatusDirect(userId, platform3);
|
|
924
|
+
}
|
|
925
|
+
return listAccessibleProjectsWithAccountStatusViaEdgeFunction(
|
|
926
|
+
userId,
|
|
927
|
+
platform3
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
async function resolveProjectForConnectedAccountTool(explicitProjectId, platform3) {
|
|
931
|
+
if (explicitProjectId) return { projectId: explicitProjectId };
|
|
932
|
+
const defaultProjectId = await getDefaultProjectId();
|
|
933
|
+
if (defaultProjectId) return { projectId: defaultProjectId };
|
|
934
|
+
const genericError = "project_id is required. Configure an explicit project or use an API key scoped to exactly one project.";
|
|
935
|
+
const userId = await getDefaultUserId().catch(() => null);
|
|
936
|
+
if (!userId) return { error: genericError };
|
|
937
|
+
const projects = await listAccessibleProjectsWithAccountStatus(
|
|
938
|
+
userId,
|
|
939
|
+
platform3
|
|
940
|
+
);
|
|
941
|
+
if (projects.length === 0) return { error: genericError };
|
|
942
|
+
const withAccounts = projects.filter((p) => p.hasConnectedAccounts);
|
|
943
|
+
if (withAccounts.length === 1) {
|
|
944
|
+
const chosen = withAccounts[0];
|
|
945
|
+
const platformNote = platform3 ? ` for ${Array.isArray(platform3) ? platform3.join("/") : platform3}` : "";
|
|
946
|
+
return {
|
|
947
|
+
projectId: chosen.id,
|
|
948
|
+
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}.`,
|
|
949
|
+
projects
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
const projectList = projects.map(
|
|
953
|
+
(p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
|
|
954
|
+
).join("; ");
|
|
955
|
+
return {
|
|
956
|
+
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}.`,
|
|
957
|
+
projects
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
async function resolveProjectStrict(explicitProjectId) {
|
|
961
|
+
if (explicitProjectId) return { projectId: explicitProjectId };
|
|
962
|
+
const defaultProjectId = await getDefaultProjectId();
|
|
963
|
+
if (defaultProjectId) return { projectId: defaultProjectId };
|
|
964
|
+
const genericError = "project_id is required. Configure an explicit project or use an API key scoped to exactly one project.";
|
|
965
|
+
const userId = await getDefaultUserId().catch(() => null);
|
|
966
|
+
if (!userId) return { error: genericError };
|
|
967
|
+
const projects = await listAccessibleProjectsWithAccountStatus(userId);
|
|
968
|
+
if (projects.length === 0) return { error: genericError };
|
|
969
|
+
const projectList = projects.map(
|
|
970
|
+
(p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
|
|
971
|
+
).join("; ");
|
|
972
|
+
return {
|
|
973
|
+
error: `project_id is required \u2014 your account has ${projects.length} projects. Pass the exact project_id from this list: ${projectList}.`,
|
|
974
|
+
projects
|
|
975
|
+
};
|
|
976
|
+
}
|
|
602
977
|
async function initializeAuth() {
|
|
603
978
|
const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
604
979
|
const apiKey = await loadApiKey2();
|
|
@@ -624,8 +999,11 @@ async function initializeAuth() {
|
|
|
624
999
|
}
|
|
625
1000
|
if (authenticatedExpiresAt) {
|
|
626
1001
|
const expiresMs = new Date(authenticatedExpiresAt).getTime();
|
|
627
|
-
const daysLeft = Math.ceil(
|
|
628
|
-
|
|
1002
|
+
const daysLeft = Math.ceil(
|
|
1003
|
+
(expiresMs - Date.now()) / (1e3 * 60 * 60 * 24)
|
|
1004
|
+
);
|
|
1005
|
+
if (!_quietAuth)
|
|
1006
|
+
console.error("[MCP] Key expires: " + authenticatedExpiresAt);
|
|
629
1007
|
if (daysLeft <= 7) {
|
|
630
1008
|
console.error(
|
|
631
1009
|
`[MCP] Warning: API key expires in ${daysLeft} day(s). Run: npx @socialneuron/mcp-server login`
|
|
@@ -704,7 +1082,7 @@ async function logMcpToolInvocation(args) {
|
|
|
704
1082
|
Promise.resolve().then(() => (init_posthog(), posthog_exports)).then(({ captureToolEvent: captureToolEvent2 }) => captureToolEvent2(args)).catch(() => {
|
|
705
1083
|
});
|
|
706
1084
|
}
|
|
707
|
-
var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, authenticatedProjectId, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY
|
|
1085
|
+
var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, authenticatedProjectId, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY;
|
|
708
1086
|
var init_supabase = __esm({
|
|
709
1087
|
"src/lib/supabase.ts"() {
|
|
710
1088
|
"use strict";
|
|
@@ -722,7 +1100,6 @@ var init_supabase = __esm({
|
|
|
722
1100
|
MCP_RUN_ID = randomUUID();
|
|
723
1101
|
CLOUD_SUPABASE_URL = "https://rhukkjscgzauutioyeei.supabase.co";
|
|
724
1102
|
CLOUD_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJodWtranNjZ3phdXV0aW95ZWVpIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ4NjM4ODYsImV4cCI6MjA4MDQzOTg4Nn0.JVtrviGvN0HaSh0JFS5KNl5FAB5ffG5Y1IMZsQFUrNQ";
|
|
725
|
-
projectIdCache = /* @__PURE__ */ new Map();
|
|
726
1103
|
}
|
|
727
1104
|
});
|
|
728
1105
|
|
|
@@ -858,269 +1235,66 @@ function classifyError(err) {
|
|
|
858
1235
|
const lower = message.toLowerCase();
|
|
859
1236
|
if (lower.includes("no authentication") || lower.includes("unauthorized") || lower.includes("api key invalid") || lower.includes("expired") || lower.includes("not logged in") || lower.includes("invalid api key") || lower.includes("invalid signature")) {
|
|
860
1237
|
return {
|
|
861
|
-
message,
|
|
862
|
-
errorType: "AUTH",
|
|
863
|
-
retryable: false,
|
|
864
|
-
hint: "Run: npx @socialneuron/mcp-server login \u2014 Requires a paid plan (Starter+). See https://socialneuron.com/pricing"
|
|
865
|
-
};
|
|
866
|
-
}
|
|
867
|
-
if (lower.includes("econnrefused") || lower.includes("etimedout") || lower.includes("fetch failed") || lower.includes("aborterror") || lower.includes("network") || lower.includes("dns") || lower.includes("temporary issue")) {
|
|
868
|
-
return {
|
|
869
|
-
message,
|
|
870
|
-
errorType: "NETWORK",
|
|
871
|
-
retryable: true,
|
|
872
|
-
hint: "Check your network connection and try again."
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
if (lower.includes("rate limit") || lower.includes("429") || lower.includes("too many requests")) {
|
|
876
|
-
return {
|
|
877
|
-
message,
|
|
878
|
-
errorType: "RATE_LIMIT",
|
|
879
|
-
retryable: true,
|
|
880
|
-
hint: "Wait a moment and retry."
|
|
881
|
-
};
|
|
882
|
-
}
|
|
883
|
-
if (lower.includes("not found") || lower.includes("404") || lower.includes("no job found")) {
|
|
884
|
-
return { message, errorType: "NOT_FOUND", retryable: false };
|
|
885
|
-
}
|
|
886
|
-
if (lower.includes("missing required") || lower.includes("invalid") || lower.includes("cannot delete builtin")) {
|
|
887
|
-
return { message, errorType: "VALIDATION", retryable: false };
|
|
888
|
-
}
|
|
889
|
-
return { message, errorType: "INTERNAL", retryable: true };
|
|
890
|
-
}
|
|
891
|
-
async function withSnErrorHandling(command, asJson, fn, replMode = false) {
|
|
892
|
-
try {
|
|
893
|
-
await fn();
|
|
894
|
-
} catch (err) {
|
|
895
|
-
const classified = classifyError(err);
|
|
896
|
-
if (asJson) {
|
|
897
|
-
const response = {
|
|
898
|
-
ok: false,
|
|
899
|
-
command,
|
|
900
|
-
error: classified.message,
|
|
901
|
-
errorType: classified.errorType,
|
|
902
|
-
retryable: classified.retryable,
|
|
903
|
-
...classified.hint ? { hint: classified.hint } : {},
|
|
904
|
-
schema_version: "1"
|
|
905
|
-
};
|
|
906
|
-
process.stdout.write(JSON.stringify(response, null, 2) + "\n");
|
|
907
|
-
} else {
|
|
908
|
-
process.stderr.write(`Error [${command}]: ${classified.message}
|
|
909
|
-
`);
|
|
910
|
-
if (classified.hint) process.stderr.write(`Hint: ${classified.hint}
|
|
911
|
-
`);
|
|
912
|
-
}
|
|
913
|
-
if (!replMode) {
|
|
914
|
-
process.exit(1);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
var init_error_handling = __esm({
|
|
919
|
-
"src/cli/error-handling.ts"() {
|
|
920
|
-
"use strict";
|
|
921
|
-
}
|
|
922
|
-
});
|
|
923
|
-
|
|
924
|
-
// src/lib/edge-function.ts
|
|
925
|
-
var edge_function_exports = {};
|
|
926
|
-
__export(edge_function_exports, {
|
|
927
|
-
callEdgeFunction: () => callEdgeFunction
|
|
928
|
-
});
|
|
929
|
-
function safeGatewayError(responseText, status) {
|
|
930
|
-
try {
|
|
931
|
-
const parsed = JSON.parse(responseText);
|
|
932
|
-
const nested = parsed.error && typeof parsed.error === "object" ? parsed.error : null;
|
|
933
|
-
const candidates = [
|
|
934
|
-
nested?.error_type,
|
|
935
|
-
nested?.code,
|
|
936
|
-
parsed.error_type,
|
|
937
|
-
parsed.error_code,
|
|
938
|
-
parsed.code,
|
|
939
|
-
typeof parsed.error === "string" ? parsed.error : null
|
|
940
|
-
];
|
|
941
|
-
for (const value of candidates) {
|
|
942
|
-
if (typeof value !== "string") continue;
|
|
943
|
-
const normalized = value.trim().toLowerCase();
|
|
944
|
-
if (SAFE_GATEWAY_ERROR_CODES.has(normalized)) return normalized;
|
|
945
|
-
const embedded = [...SAFE_GATEWAY_ERROR_CODES].find((code) => normalized.includes(code));
|
|
946
|
-
if (embedded) return embedded;
|
|
947
|
-
}
|
|
948
|
-
} catch {
|
|
949
|
-
}
|
|
950
|
-
return `Backend request failed (HTTP ${status}).`;
|
|
951
|
-
}
|
|
952
|
-
function safeFailureData(responseText) {
|
|
953
|
-
try {
|
|
954
|
-
const parsed = JSON.parse(responseText);
|
|
955
|
-
const metric = (name) => {
|
|
956
|
-
const value = parsed[name];
|
|
957
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
958
|
-
};
|
|
959
|
-
const billingStatus = typeof parsed.billing_status === "string" && SAFE_BILLING_STATUSES.has(parsed.billing_status) ? parsed.billing_status : void 0;
|
|
960
|
-
const failureReason = parsed.failure_reason === "generation_failed" || parsed.failure_reason === "authentication_failed" || parsed.failure_reason === "cancelled_by_user" ? parsed.failure_reason : void 0;
|
|
961
|
-
const jobStatus = typeof parsed.status === "string" && SAFE_JOB_STATUSES.has(parsed.status) ? parsed.status : void 0;
|
|
962
|
-
const safe = {
|
|
963
|
-
...jobStatus ? { status: jobStatus } : {},
|
|
964
|
-
...metric("credits_reserved") !== void 0 ? { credits_reserved: metric("credits_reserved") } : {},
|
|
965
|
-
...metric("credits_charged") !== void 0 ? { credits_charged: metric("credits_charged") } : {},
|
|
966
|
-
...metric("credits_refunded") !== void 0 ? { credits_refunded: metric("credits_refunded") } : {},
|
|
967
|
-
...billingStatus ? { billing_status: billingStatus } : {},
|
|
968
|
-
...failureReason ? { failure_reason: failureReason } : {}
|
|
969
|
-
};
|
|
970
|
-
return Object.keys(safe).length > 0 ? safe : null;
|
|
971
|
-
} catch {
|
|
972
|
-
return null;
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
function getApiKeyOrNull() {
|
|
976
|
-
const envKey = process.env.SOCIALNEURON_API_KEY;
|
|
977
|
-
if (envKey && envKey.trim().length) return envKey.trim();
|
|
978
|
-
const requestToken = getRequestToken();
|
|
979
|
-
if (requestToken) return requestToken;
|
|
980
|
-
return getAuthenticatedApiKey();
|
|
981
|
-
}
|
|
982
|
-
async function callEdgeFunction(functionName, body, options) {
|
|
983
|
-
const supabaseUrl = getSupabaseUrl();
|
|
984
|
-
const apiKey = getApiKeyOrNull();
|
|
985
|
-
const controller = new AbortController();
|
|
986
|
-
const timeoutMs = options?.timeoutMs ?? 6e4;
|
|
987
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
988
|
-
const enrichedBody = { ...body };
|
|
989
|
-
if (!enrichedBody.userId && !enrichedBody.user_id) {
|
|
990
|
-
try {
|
|
991
|
-
const defaultId = await getDefaultUserId();
|
|
992
|
-
enrichedBody.userId = defaultId;
|
|
993
|
-
enrichedBody.user_id = defaultId;
|
|
994
|
-
} catch {
|
|
995
|
-
}
|
|
996
|
-
} else {
|
|
997
|
-
if (enrichedBody.userId && !enrichedBody.user_id) enrichedBody.user_id = enrichedBody.userId;
|
|
998
|
-
if (enrichedBody.user_id && !enrichedBody.userId) enrichedBody.userId = enrichedBody.user_id;
|
|
1238
|
+
message,
|
|
1239
|
+
errorType: "AUTH",
|
|
1240
|
+
retryable: false,
|
|
1241
|
+
hint: "Run: npx @socialneuron/mcp-server login \u2014 Requires a paid plan (Starter+). See https://socialneuron.com/pricing"
|
|
1242
|
+
};
|
|
999
1243
|
}
|
|
1000
|
-
if (
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
}
|
|
1008
|
-
} catch {
|
|
1009
|
-
}
|
|
1244
|
+
if (lower.includes("econnrefused") || lower.includes("etimedout") || lower.includes("fetch failed") || lower.includes("aborterror") || lower.includes("network") || lower.includes("dns") || lower.includes("temporary issue")) {
|
|
1245
|
+
return {
|
|
1246
|
+
message,
|
|
1247
|
+
errorType: "NETWORK",
|
|
1248
|
+
retryable: true,
|
|
1249
|
+
hint: "Check your network connection and try again."
|
|
1250
|
+
};
|
|
1010
1251
|
}
|
|
1011
|
-
|
|
1012
|
-
let method = options?.method ?? "POST";
|
|
1013
|
-
let headers;
|
|
1014
|
-
let requestBody;
|
|
1015
|
-
if (!apiKey) {
|
|
1016
|
-
clearTimeout(timer);
|
|
1252
|
+
if (lower.includes("rate limit") || lower.includes("429") || lower.includes("too many requests")) {
|
|
1017
1253
|
return {
|
|
1018
|
-
|
|
1019
|
-
|
|
1254
|
+
message,
|
|
1255
|
+
errorType: "RATE_LIMIT",
|
|
1256
|
+
retryable: true,
|
|
1257
|
+
hint: "Wait a moment and retry."
|
|
1020
1258
|
};
|
|
1021
1259
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
method: method.toUpperCase(),
|
|
1032
|
-
timeoutMs
|
|
1033
|
-
};
|
|
1034
|
-
method = "POST";
|
|
1260
|
+
if (lower.includes("not found") || lower.includes("404") || lower.includes("no job found")) {
|
|
1261
|
+
return { message, errorType: "NOT_FOUND", retryable: false };
|
|
1262
|
+
}
|
|
1263
|
+
if (lower.includes("missing required") || lower.includes("invalid") || lower.includes("cannot delete builtin")) {
|
|
1264
|
+
return { message, errorType: "VALIDATION", retryable: false };
|
|
1265
|
+
}
|
|
1266
|
+
return { message, errorType: "INTERNAL", retryable: true };
|
|
1267
|
+
}
|
|
1268
|
+
async function withSnErrorHandling(command, asJson, fn, replMode = false) {
|
|
1035
1269
|
try {
|
|
1036
|
-
|
|
1037
|
-
method,
|
|
1038
|
-
headers,
|
|
1039
|
-
body: method === "GET" ? void 0 : JSON.stringify(requestBody),
|
|
1040
|
-
signal: controller.signal
|
|
1041
|
-
});
|
|
1042
|
-
clearTimeout(timer);
|
|
1043
|
-
const responseText = await response.text();
|
|
1044
|
-
if (!response.ok) {
|
|
1045
|
-
const errorCode = safeGatewayError(responseText, response.status);
|
|
1046
|
-
const failureData = safeFailureData(responseText);
|
|
1047
|
-
if (response.status === 401) {
|
|
1048
|
-
return {
|
|
1049
|
-
data: failureData,
|
|
1050
|
-
error: `Authentication failed (HTTP 401). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
|
|
1051
|
-
};
|
|
1052
|
-
}
|
|
1053
|
-
if (response.status === 403) {
|
|
1054
|
-
return {
|
|
1055
|
-
data: failureData,
|
|
1056
|
-
error: `Forbidden (HTTP 403): ${errorCode}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
|
|
1057
|
-
};
|
|
1058
|
-
}
|
|
1059
|
-
if (response.status === 429) {
|
|
1060
|
-
const retryAfter = response.headers.get("retry-after") || "60";
|
|
1061
|
-
return {
|
|
1062
|
-
data: failureData,
|
|
1063
|
-
error: `Rate limit exceeded (HTTP 429). Wait ${retryAfter}s before retrying. Reduce request frequency or upgrade your plan.`
|
|
1064
|
-
};
|
|
1065
|
-
}
|
|
1066
|
-
return { data: failureData, error: errorCode };
|
|
1067
|
-
}
|
|
1068
|
-
try {
|
|
1069
|
-
const data = JSON.parse(responseText);
|
|
1070
|
-
return { data, error: null };
|
|
1071
|
-
} catch {
|
|
1072
|
-
return { data: { text: responseText }, error: null };
|
|
1073
|
-
}
|
|
1270
|
+
await fn();
|
|
1074
1271
|
} catch (err) {
|
|
1075
|
-
|
|
1076
|
-
if (
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1272
|
+
const classified = classifyError(err);
|
|
1273
|
+
if (asJson) {
|
|
1274
|
+
const response = {
|
|
1275
|
+
ok: false,
|
|
1276
|
+
command,
|
|
1277
|
+
error: classified.message,
|
|
1278
|
+
errorType: classified.errorType,
|
|
1279
|
+
retryable: classified.retryable,
|
|
1280
|
+
...classified.hint ? { hint: classified.hint } : {},
|
|
1281
|
+
schema_version: "1"
|
|
1080
1282
|
};
|
|
1283
|
+
process.stdout.write(JSON.stringify(response, null, 2) + "\n");
|
|
1284
|
+
} else {
|
|
1285
|
+
process.stderr.write(`Error [${command}]: ${classified.message}
|
|
1286
|
+
`);
|
|
1287
|
+
if (classified.hint) process.stderr.write(`Hint: ${classified.hint}
|
|
1288
|
+
`);
|
|
1289
|
+
}
|
|
1290
|
+
if (!replMode) {
|
|
1291
|
+
process.exit(1);
|
|
1081
1292
|
}
|
|
1082
|
-
return { data: null, error: "Network request failed. Please retry." };
|
|
1083
1293
|
}
|
|
1084
1294
|
}
|
|
1085
|
-
var
|
|
1086
|
-
|
|
1087
|
-
"src/lib/edge-function.ts"() {
|
|
1295
|
+
var init_error_handling = __esm({
|
|
1296
|
+
"src/cli/error-handling.ts"() {
|
|
1088
1297
|
"use strict";
|
|
1089
|
-
init_supabase();
|
|
1090
|
-
init_request_context();
|
|
1091
|
-
SAFE_GATEWAY_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1092
|
-
"daily_limit_reached",
|
|
1093
|
-
"insufficient_credits",
|
|
1094
|
-
"project_scope_mismatch",
|
|
1095
|
-
"schedule_conflict",
|
|
1096
|
-
"post_not_found",
|
|
1097
|
-
"post_not_reschedulable",
|
|
1098
|
-
"post_in_progress",
|
|
1099
|
-
"not_cancellable",
|
|
1100
|
-
"publishing_in_progress",
|
|
1101
|
-
"plan_upgrade_required",
|
|
1102
|
-
"rate_limited",
|
|
1103
|
-
"validation_error",
|
|
1104
|
-
"permission_denied",
|
|
1105
|
-
"not_found"
|
|
1106
|
-
]);
|
|
1107
|
-
SAFE_BILLING_STATUSES = /* @__PURE__ */ new Set([
|
|
1108
|
-
"reserved",
|
|
1109
|
-
"charged",
|
|
1110
|
-
"refunded",
|
|
1111
|
-
"failed_no_charge",
|
|
1112
|
-
"refund_pending",
|
|
1113
|
-
"not_charged"
|
|
1114
|
-
]);
|
|
1115
|
-
SAFE_JOB_STATUSES = /* @__PURE__ */ new Set([
|
|
1116
|
-
"queued",
|
|
1117
|
-
"pending",
|
|
1118
|
-
"processing",
|
|
1119
|
-
"completed",
|
|
1120
|
-
"failed",
|
|
1121
|
-
"cancelled",
|
|
1122
|
-
"canceled"
|
|
1123
|
-
]);
|
|
1124
1298
|
}
|
|
1125
1299
|
});
|
|
1126
1300
|
|
|
@@ -4173,6 +4347,92 @@ var init_ideation = __esm({
|
|
|
4173
4347
|
}
|
|
4174
4348
|
});
|
|
4175
4349
|
|
|
4350
|
+
// src/lib/sanitize-error.ts
|
|
4351
|
+
function redactSensitiveIdentifiers(message) {
|
|
4352
|
+
return message.replace(EMAIL_PATTERN, "[redacted-email]").replace(UUID_PATTERN, "[redacted-id]");
|
|
4353
|
+
}
|
|
4354
|
+
function sanitizeError(error) {
|
|
4355
|
+
const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
4356
|
+
for (const [pattern, userMessage] of ERROR_PATTERNS) {
|
|
4357
|
+
if (pattern.test(msg)) {
|
|
4358
|
+
return userMessage;
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
return "An unexpected error occurred. Please try again.";
|
|
4362
|
+
}
|
|
4363
|
+
var ERROR_PATTERNS, UUID_PATTERN, EMAIL_PATTERN;
|
|
4364
|
+
var init_sanitize_error = __esm({
|
|
4365
|
+
"src/lib/sanitize-error.ts"() {
|
|
4366
|
+
"use strict";
|
|
4367
|
+
ERROR_PATTERNS = [
|
|
4368
|
+
// Postgres / PostgREST
|
|
4369
|
+
[
|
|
4370
|
+
/PGRST301|permission denied/i,
|
|
4371
|
+
"Access denied. Check your account permissions."
|
|
4372
|
+
],
|
|
4373
|
+
[
|
|
4374
|
+
/42P01|does not exist/i,
|
|
4375
|
+
"Service temporarily unavailable. Please try again."
|
|
4376
|
+
],
|
|
4377
|
+
[
|
|
4378
|
+
/23505|unique.*constraint|duplicate key/i,
|
|
4379
|
+
"A duplicate record already exists."
|
|
4380
|
+
],
|
|
4381
|
+
[/23503|foreign key/i, "Referenced record not found."],
|
|
4382
|
+
// Gemini / Google AI
|
|
4383
|
+
[
|
|
4384
|
+
/google.*api.*key|googleapis\.com.*40[13]/i,
|
|
4385
|
+
"Content generation failed. Please try again."
|
|
4386
|
+
],
|
|
4387
|
+
[
|
|
4388
|
+
/RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
|
|
4389
|
+
"AI service rate limit reached. Please wait and retry."
|
|
4390
|
+
],
|
|
4391
|
+
[
|
|
4392
|
+
/SAFETY|prompt.*blocked|content.*filter/i,
|
|
4393
|
+
"Content was blocked by the AI safety filter. Try rephrasing."
|
|
4394
|
+
],
|
|
4395
|
+
[
|
|
4396
|
+
/gemini.*error|generativelanguage/i,
|
|
4397
|
+
"Content generation failed. Please try again."
|
|
4398
|
+
],
|
|
4399
|
+
// Kie.ai
|
|
4400
|
+
[/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
|
|
4401
|
+
// Stripe
|
|
4402
|
+
[
|
|
4403
|
+
/stripe.*api|sk_live_|sk_test_/i,
|
|
4404
|
+
"Payment processing error. Please try again."
|
|
4405
|
+
],
|
|
4406
|
+
// Network / fetch
|
|
4407
|
+
[
|
|
4408
|
+
/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
|
|
4409
|
+
"External service unavailable. Please try again."
|
|
4410
|
+
],
|
|
4411
|
+
[
|
|
4412
|
+
/fetch failed|network error|abort.*timeout/i,
|
|
4413
|
+
"Network request failed. Please try again."
|
|
4414
|
+
],
|
|
4415
|
+
[/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
|
|
4416
|
+
// Supabase Edge Function internals
|
|
4417
|
+
[
|
|
4418
|
+
/FunctionsHttpError|non-2xx status/i,
|
|
4419
|
+
"Backend service error. Please try again."
|
|
4420
|
+
],
|
|
4421
|
+
[
|
|
4422
|
+
/JWT|token.*expired|token.*invalid/i,
|
|
4423
|
+
"Authentication expired. Please re-authenticate."
|
|
4424
|
+
],
|
|
4425
|
+
// Generic sensitive patterns (API keys, URLs with secrets)
|
|
4426
|
+
[
|
|
4427
|
+
/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i,
|
|
4428
|
+
"An internal error occurred. Please try again."
|
|
4429
|
+
]
|
|
4430
|
+
];
|
|
4431
|
+
UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
|
|
4432
|
+
EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
4433
|
+
}
|
|
4434
|
+
});
|
|
4435
|
+
|
|
4176
4436
|
// src/lib/checkStatusShape.ts
|
|
4177
4437
|
function buildCheckStatusPayload(job, liveStatus) {
|
|
4178
4438
|
const status = liveStatus?.status ?? job.status;
|
|
@@ -4268,9 +4528,22 @@ function checkCreditBudget(estimatedCost) {
|
|
|
4268
4528
|
}
|
|
4269
4529
|
const used = getCreditsUsed();
|
|
4270
4530
|
if (used + estimatedCost > MAX_CREDITS_PER_RUN) {
|
|
4531
|
+
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.`;
|
|
4271
4532
|
return {
|
|
4272
4533
|
ok: false,
|
|
4273
|
-
message
|
|
4534
|
+
message,
|
|
4535
|
+
error: toolError("billing_error", message, {
|
|
4536
|
+
recover_with: [
|
|
4537
|
+
"Raise SOCIALNEURON_MAX_CREDITS_PER_RUN to a higher credit ceiling.",
|
|
4538
|
+
"Unset SOCIALNEURON_MAX_CREDITS_PER_RUN to remove the per-run cap."
|
|
4539
|
+
],
|
|
4540
|
+
details: {
|
|
4541
|
+
env_var: "SOCIALNEURON_MAX_CREDITS_PER_RUN",
|
|
4542
|
+
credits_used_this_run: used,
|
|
4543
|
+
estimated_call_cost: estimatedCost,
|
|
4544
|
+
max_credits_per_run: MAX_CREDITS_PER_RUN
|
|
4545
|
+
}
|
|
4546
|
+
})
|
|
4274
4547
|
};
|
|
4275
4548
|
}
|
|
4276
4549
|
return { ok: true };
|
|
@@ -4293,8 +4566,15 @@ var init_budget = __esm({
|
|
|
4293
4566
|
"src/lib/budget.ts"() {
|
|
4294
4567
|
"use strict";
|
|
4295
4568
|
init_request_context();
|
|
4296
|
-
|
|
4297
|
-
|
|
4569
|
+
init_tool_error();
|
|
4570
|
+
MAX_CREDITS_PER_RUN = Math.max(
|
|
4571
|
+
0,
|
|
4572
|
+
Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0)
|
|
4573
|
+
);
|
|
4574
|
+
MAX_ASSETS_PER_RUN = Math.max(
|
|
4575
|
+
0,
|
|
4576
|
+
Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0)
|
|
4577
|
+
);
|
|
4298
4578
|
_globalCreditsUsed = 0;
|
|
4299
4579
|
_globalAssetsGenerated = 0;
|
|
4300
4580
|
}
|
|
@@ -4321,7 +4601,7 @@ function asEnvelope2(data) {
|
|
|
4321
4601
|
function registerContentTools(server) {
|
|
4322
4602
|
server.tool(
|
|
4323
4603
|
"generate_video",
|
|
4324
|
-
"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
|
|
4604
|
+
"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.",
|
|
4325
4605
|
{
|
|
4326
4606
|
prompt: z2.string().max(2500).describe(
|
|
4327
4607
|
'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.'
|
|
@@ -4336,7 +4616,7 @@ function registerContentTools(server) {
|
|
|
4336
4616
|
"Video aspect ratio. 16:9 for YouTube/landscape, 9:16 for TikTok/Reels/Shorts, 1:1 for Instagram feed/square. Defaults to 16:9."
|
|
4337
4617
|
),
|
|
4338
4618
|
enable_audio: z2.boolean().optional().describe(
|
|
4339
|
-
"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).
|
|
4619
|
+
"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."
|
|
4340
4620
|
),
|
|
4341
4621
|
image_url: z2.string().optional().describe(
|
|
4342
4622
|
"Start frame image URL for image-to-video (Kling 3.0 frame control)."
|
|
@@ -4372,12 +4652,12 @@ function registerContentTools(server) {
|
|
|
4372
4652
|
const estimatedCost = VIDEO_CREDIT_ESTIMATES[model] ?? 120;
|
|
4373
4653
|
const budgetCheck = checkCreditBudget(estimatedCost);
|
|
4374
4654
|
if (!budgetCheck.ok) {
|
|
4375
|
-
return
|
|
4376
|
-
content: [{ type: "text", text: budgetCheck.message }],
|
|
4377
|
-
isError: true
|
|
4378
|
-
};
|
|
4655
|
+
return budgetCheck.error;
|
|
4379
4656
|
}
|
|
4380
|
-
const rateLimit = checkRateLimit(
|
|
4657
|
+
const rateLimit = checkRateLimit(
|
|
4658
|
+
"generation",
|
|
4659
|
+
`generate_video:${userId}`
|
|
4660
|
+
);
|
|
4381
4661
|
if (!rateLimit.allowed) {
|
|
4382
4662
|
return {
|
|
4383
4663
|
content: [
|
|
@@ -4396,9 +4676,12 @@ function registerContentTools(server) {
|
|
|
4396
4676
|
model,
|
|
4397
4677
|
duration: duration ?? 5,
|
|
4398
4678
|
aspectRatio: aspect_ratio ?? "16:9",
|
|
4399
|
-
// Default FALSE (2026-07-13): the old `?? true` default silently
|
|
4400
|
-
// kling-family costs (2.65x on kling 2.6) for callers that never asked
|
|
4401
|
-
|
|
4679
|
+
// Default FALSE for most models (2026-07-13): the old `?? true` default silently
|
|
4680
|
+
// multiplied kling-family costs (2.65x on kling 2.6) for callers that never asked
|
|
4681
|
+
// for audio. Exception (2026-07-17): the seedance-2 family bills audio into its base
|
|
4682
|
+
// cost and generates it natively, so a false default there shipped silent no-audio
|
|
4683
|
+
// mp4s — those models default TRUE when the caller omits enable_audio.
|
|
4684
|
+
enableAudio: enable_audio ?? AUDIO_NATIVE_DEFAULT_MODELS.has(model),
|
|
4402
4685
|
...image_url && { imageUrl: image_url },
|
|
4403
4686
|
...end_frame_url && { endFrameUrl: end_frame_url },
|
|
4404
4687
|
// The server reads projectId and enforces
|
|
@@ -4500,7 +4783,14 @@ function registerContentTools(server) {
|
|
|
4500
4783
|
project_id: z2.string().optional().describe("Project ID to associate the generated image with."),
|
|
4501
4784
|
response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
4502
4785
|
},
|
|
4503
|
-
async ({
|
|
4786
|
+
async ({
|
|
4787
|
+
prompt: prompt2,
|
|
4788
|
+
model,
|
|
4789
|
+
aspect_ratio,
|
|
4790
|
+
image_url,
|
|
4791
|
+
project_id,
|
|
4792
|
+
response_format
|
|
4793
|
+
}) => {
|
|
4504
4794
|
const format = response_format ?? "text";
|
|
4505
4795
|
const userId = await getDefaultUserId();
|
|
4506
4796
|
const assetBudget = checkAssetBudget();
|
|
@@ -4513,12 +4803,12 @@ function registerContentTools(server) {
|
|
|
4513
4803
|
const estimatedCost = IMAGE_CREDIT_ESTIMATES[model] ?? 30;
|
|
4514
4804
|
const budgetCheck = checkCreditBudget(estimatedCost);
|
|
4515
4805
|
if (!budgetCheck.ok) {
|
|
4516
|
-
return
|
|
4517
|
-
content: [{ type: "text", text: budgetCheck.message }],
|
|
4518
|
-
isError: true
|
|
4519
|
-
};
|
|
4806
|
+
return budgetCheck.error;
|
|
4520
4807
|
}
|
|
4521
|
-
const rateLimit = checkRateLimit(
|
|
4808
|
+
const rateLimit = checkRateLimit(
|
|
4809
|
+
"generation",
|
|
4810
|
+
`generate_image:${userId}`
|
|
4811
|
+
);
|
|
4522
4812
|
if (!rateLimit.allowed) {
|
|
4523
4813
|
return {
|
|
4524
4814
|
content: [
|
|
@@ -4649,6 +4939,15 @@ function registerContentTools(server) {
|
|
|
4649
4939
|
isError: true
|
|
4650
4940
|
};
|
|
4651
4941
|
}
|
|
4942
|
+
let projectsDisclosure;
|
|
4943
|
+
const ownScopedProjectId = await getDefaultProjectId();
|
|
4944
|
+
if (!ownScopedProjectId) {
|
|
4945
|
+
const ownUserId = await getDefaultUserId().catch(() => null);
|
|
4946
|
+
if (ownUserId) {
|
|
4947
|
+
const list = await listAccessibleProjectsWithAccountStatus(ownUserId);
|
|
4948
|
+
if (list.length > 0) projectsDisclosure = list;
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4652
4951
|
if (job.external_id && (job.status === "pending" || job.status === "processing")) {
|
|
4653
4952
|
const { data: liveStatus } = await callEdgeFunction(
|
|
4654
4953
|
"kie-task-status",
|
|
@@ -4672,7 +4971,9 @@ function registerContentTools(server) {
|
|
|
4672
4971
|
if (livePayload.error) {
|
|
4673
4972
|
lines2.push(`Error: ${livePayload.error}`);
|
|
4674
4973
|
}
|
|
4675
|
-
const fallbackDisclosure2 = buildFallbackDisclosureLine(
|
|
4974
|
+
const fallbackDisclosure2 = buildFallbackDisclosureLine(
|
|
4975
|
+
job.result_metadata
|
|
4976
|
+
);
|
|
4676
4977
|
if (fallbackDisclosure2) lines2.push(fallbackDisclosure2);
|
|
4677
4978
|
lines2.push(`Credits: ${job.credits_cost}`);
|
|
4678
4979
|
if (job.billing_status) {
|
|
@@ -4680,7 +4981,15 @@ function registerContentTools(server) {
|
|
|
4680
4981
|
`Billing: ${job.billing_status} (charged ${job.credits_charged ?? 0}, refunded ${job.credits_refunded ?? 0})`
|
|
4681
4982
|
);
|
|
4682
4983
|
}
|
|
4683
|
-
lines2.push(`Created: ${job.created_at}`);
|
|
4984
|
+
lines2.push(`Created: ${job.created_at}`);
|
|
4985
|
+
if (projectsDisclosure) {
|
|
4986
|
+
lines2.push(
|
|
4987
|
+
"",
|
|
4988
|
+
`Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
|
|
4989
|
+
(p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
|
|
4990
|
+
).join("; ")}`
|
|
4991
|
+
);
|
|
4992
|
+
}
|
|
4684
4993
|
if (format === "json") {
|
|
4685
4994
|
return {
|
|
4686
4995
|
content: [
|
|
@@ -4697,7 +5006,8 @@ function registerContentTools(server) {
|
|
|
4697
5006
|
// `job` + `liveStatus`; do not duplicate them here.
|
|
4698
5007
|
asEnvelope2({
|
|
4699
5008
|
...liveStatus,
|
|
4700
|
-
...livePayload
|
|
5009
|
+
...livePayload,
|
|
5010
|
+
...projectsDisclosure ? { projects: projectsDisclosure } : {}
|
|
4701
5011
|
}),
|
|
4702
5012
|
null,
|
|
4703
5013
|
2
|
|
@@ -4738,9 +5048,11 @@ function registerContentTools(server) {
|
|
|
4738
5048
|
);
|
|
4739
5049
|
}
|
|
4740
5050
|
if (job.error_message) {
|
|
4741
|
-
lines.push(`Error: ${job.error_message}`);
|
|
5051
|
+
lines.push(`Error: ${redactSensitiveIdentifiers(job.error_message)}`);
|
|
4742
5052
|
}
|
|
4743
|
-
const fallbackDisclosure = buildFallbackDisclosureLine(
|
|
5053
|
+
const fallbackDisclosure = buildFallbackDisclosureLine(
|
|
5054
|
+
job.result_metadata
|
|
5055
|
+
);
|
|
4744
5056
|
if (fallbackDisclosure) lines.push(fallbackDisclosure);
|
|
4745
5057
|
lines.push(`Credits: ${job.credits_cost}`);
|
|
4746
5058
|
if (job.billing_status) {
|
|
@@ -4752,10 +5064,19 @@ function registerContentTools(server) {
|
|
|
4752
5064
|
if (job.completed_at) {
|
|
4753
5065
|
lines.push(`Completed: ${job.completed_at}`);
|
|
4754
5066
|
}
|
|
5067
|
+
if (projectsDisclosure) {
|
|
5068
|
+
lines.push(
|
|
5069
|
+
"",
|
|
5070
|
+
`Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
|
|
5071
|
+
(p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
|
|
5072
|
+
).join("; ")}`
|
|
5073
|
+
);
|
|
5074
|
+
}
|
|
4755
5075
|
if (format === "json") {
|
|
4756
5076
|
const enriched = {
|
|
4757
5077
|
...job,
|
|
4758
|
-
...buildCheckStatusPayload(job)
|
|
5078
|
+
...buildCheckStatusPayload(job),
|
|
5079
|
+
...projectsDisclosure ? { projects: projectsDisclosure } : {}
|
|
4759
5080
|
};
|
|
4760
5081
|
return {
|
|
4761
5082
|
content: [
|
|
@@ -4881,10 +5202,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
4881
5202
|
const estimatedCost = 10;
|
|
4882
5203
|
const budgetCheck = checkCreditBudget(estimatedCost);
|
|
4883
5204
|
if (!budgetCheck.ok) {
|
|
4884
|
-
return
|
|
4885
|
-
content: [{ type: "text", text: budgetCheck.message }],
|
|
4886
|
-
isError: true
|
|
4887
|
-
};
|
|
5205
|
+
return budgetCheck.error;
|
|
4888
5206
|
}
|
|
4889
5207
|
const { data, error } = await callEdgeFunction(
|
|
4890
5208
|
"social-neuron-ai",
|
|
@@ -4971,10 +5289,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
4971
5289
|
const estimatedCost = 15;
|
|
4972
5290
|
const budgetCheck = checkCreditBudget(estimatedCost);
|
|
4973
5291
|
if (!budgetCheck.ok) {
|
|
4974
|
-
return
|
|
4975
|
-
content: [{ type: "text", text: budgetCheck.message }],
|
|
4976
|
-
isError: true
|
|
4977
|
-
};
|
|
5292
|
+
return budgetCheck.error;
|
|
4978
5293
|
}
|
|
4979
5294
|
const rateLimit = checkRateLimit(
|
|
4980
5295
|
"generation",
|
|
@@ -5137,10 +5452,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
5137
5452
|
const estimatedCost = 10 + slideCount * 2;
|
|
5138
5453
|
const budgetCheck = checkCreditBudget(estimatedCost);
|
|
5139
5454
|
if (!budgetCheck.ok) {
|
|
5140
|
-
return
|
|
5141
|
-
content: [{ type: "text", text: budgetCheck.message }],
|
|
5142
|
-
isError: true
|
|
5143
|
-
};
|
|
5455
|
+
return budgetCheck.error;
|
|
5144
5456
|
}
|
|
5145
5457
|
const userId = await getDefaultUserId();
|
|
5146
5458
|
const rateLimit = checkRateLimit(
|
|
@@ -5243,13 +5555,14 @@ Return ONLY valid JSON in this exact format:
|
|
|
5243
5555
|
}
|
|
5244
5556
|
);
|
|
5245
5557
|
}
|
|
5246
|
-
var VIDEO_CREDIT_ESTIMATES, VIDEO_MODEL_ENUM, IMAGE_CREDIT_ESTIMATES;
|
|
5558
|
+
var VIDEO_CREDIT_ESTIMATES, AUDIO_NATIVE_DEFAULT_MODELS, VIDEO_MODEL_ENUM, IMAGE_CREDIT_ESTIMATES;
|
|
5247
5559
|
var init_content2 = __esm({
|
|
5248
5560
|
"src/tools/content.ts"() {
|
|
5249
5561
|
"use strict";
|
|
5250
5562
|
init_edge_function();
|
|
5251
5563
|
init_rate_limit();
|
|
5252
5564
|
init_supabase();
|
|
5565
|
+
init_sanitize_error();
|
|
5253
5566
|
init_version();
|
|
5254
5567
|
init_checkStatusShape();
|
|
5255
5568
|
init_budget();
|
|
@@ -5267,6 +5580,10 @@ var init_content2 = __esm({
|
|
|
5267
5580
|
"seedance-1.5-pro": 150,
|
|
5268
5581
|
kling: 170
|
|
5269
5582
|
};
|
|
5583
|
+
AUDIO_NATIVE_DEFAULT_MODELS = /* @__PURE__ */ new Set([
|
|
5584
|
+
"seedance-2-fast",
|
|
5585
|
+
"seedance-2"
|
|
5586
|
+
]);
|
|
5270
5587
|
VIDEO_MODEL_ENUM = [
|
|
5271
5588
|
"seedance-2-fast",
|
|
5272
5589
|
"kling-3",
|
|
@@ -5295,57 +5612,6 @@ var init_content2 = __esm({
|
|
|
5295
5612
|
}
|
|
5296
5613
|
});
|
|
5297
5614
|
|
|
5298
|
-
// src/lib/sanitize-error.ts
|
|
5299
|
-
function sanitizeError(error) {
|
|
5300
|
-
const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
5301
|
-
for (const [pattern, userMessage] of ERROR_PATTERNS) {
|
|
5302
|
-
if (pattern.test(msg)) {
|
|
5303
|
-
return userMessage;
|
|
5304
|
-
}
|
|
5305
|
-
}
|
|
5306
|
-
return "An unexpected error occurred. Please try again.";
|
|
5307
|
-
}
|
|
5308
|
-
var ERROR_PATTERNS;
|
|
5309
|
-
var init_sanitize_error = __esm({
|
|
5310
|
-
"src/lib/sanitize-error.ts"() {
|
|
5311
|
-
"use strict";
|
|
5312
|
-
ERROR_PATTERNS = [
|
|
5313
|
-
// Postgres / PostgREST
|
|
5314
|
-
[/PGRST301|permission denied/i, "Access denied. Check your account permissions."],
|
|
5315
|
-
[/42P01|does not exist/i, "Service temporarily unavailable. Please try again."],
|
|
5316
|
-
[/23505|unique.*constraint|duplicate key/i, "A duplicate record already exists."],
|
|
5317
|
-
[/23503|foreign key/i, "Referenced record not found."],
|
|
5318
|
-
// Gemini / Google AI
|
|
5319
|
-
[/google.*api.*key|googleapis\.com.*40[13]/i, "Content generation failed. Please try again."],
|
|
5320
|
-
[
|
|
5321
|
-
/RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
|
|
5322
|
-
"AI service rate limit reached. Please wait and retry."
|
|
5323
|
-
],
|
|
5324
|
-
[
|
|
5325
|
-
/SAFETY|prompt.*blocked|content.*filter/i,
|
|
5326
|
-
"Content was blocked by the AI safety filter. Try rephrasing."
|
|
5327
|
-
],
|
|
5328
|
-
[/gemini.*error|generativelanguage/i, "Content generation failed. Please try again."],
|
|
5329
|
-
// Kie.ai
|
|
5330
|
-
[/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
|
|
5331
|
-
// Stripe
|
|
5332
|
-
[/stripe.*api|sk_live_|sk_test_/i, "Payment processing error. Please try again."],
|
|
5333
|
-
// Network / fetch
|
|
5334
|
-
[
|
|
5335
|
-
/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
|
|
5336
|
-
"External service unavailable. Please try again."
|
|
5337
|
-
],
|
|
5338
|
-
[/fetch failed|network error|abort.*timeout/i, "Network request failed. Please try again."],
|
|
5339
|
-
[/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
|
|
5340
|
-
// Supabase Edge Function internals
|
|
5341
|
-
[/FunctionsHttpError|non-2xx status/i, "Backend service error. Please try again."],
|
|
5342
|
-
[/JWT|token.*expired|token.*invalid/i, "Authentication expired. Please re-authenticate."],
|
|
5343
|
-
// Generic sensitive patterns (API keys, URLs with secrets)
|
|
5344
|
-
[/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i, "An internal error occurred. Please try again."]
|
|
5345
|
-
];
|
|
5346
|
-
}
|
|
5347
|
-
});
|
|
5348
|
-
|
|
5349
5615
|
// src/lib/ssrf.ts
|
|
5350
5616
|
import { promises as dnsPromises } from "node:dns";
|
|
5351
5617
|
function isBlockedIP(ip) {
|
|
@@ -5499,6 +5765,122 @@ var init_ssrf = __esm({
|
|
|
5499
5765
|
}
|
|
5500
5766
|
});
|
|
5501
5767
|
|
|
5768
|
+
// src/lib/connected-account-routing.ts
|
|
5769
|
+
function canonicalPlatform(value) {
|
|
5770
|
+
const normalized = value.trim().toLowerCase();
|
|
5771
|
+
return normalized === "x" ? "twitter" : normalized;
|
|
5772
|
+
}
|
|
5773
|
+
function providerPlatform(value) {
|
|
5774
|
+
return PLATFORM_CASE_MAP[canonicalPlatform(value)] ?? value;
|
|
5775
|
+
}
|
|
5776
|
+
function isUsable(account) {
|
|
5777
|
+
const status = account.effective_status ?? account.status;
|
|
5778
|
+
return status === "active" || status === "expires_soon";
|
|
5779
|
+
}
|
|
5780
|
+
function normalizeRequestedIds(requested) {
|
|
5781
|
+
const ids = /* @__PURE__ */ new Map();
|
|
5782
|
+
for (const [platform3, accountId] of Object.entries(requested ?? {})) {
|
|
5783
|
+
const canonical = canonicalPlatform(platform3);
|
|
5784
|
+
const existing = ids.get(canonical);
|
|
5785
|
+
if (existing && existing !== accountId) {
|
|
5786
|
+
return {
|
|
5787
|
+
error: `Conflicting account IDs were supplied for ${platform3} and its platform alias.`
|
|
5788
|
+
};
|
|
5789
|
+
}
|
|
5790
|
+
ids.set(canonical, accountId);
|
|
5791
|
+
}
|
|
5792
|
+
return { ids };
|
|
5793
|
+
}
|
|
5794
|
+
async function resolveConnectedAccountRouting(input) {
|
|
5795
|
+
const normalizedRequested = normalizeRequestedIds(input.requestedAccountIds);
|
|
5796
|
+
if (normalizedRequested.error) return { error: normalizedRequested.error };
|
|
5797
|
+
const targetPlatforms = new Set(input.platforms.map(canonicalPlatform));
|
|
5798
|
+
for (const requestedPlatform of normalizedRequested.ids?.keys() ?? []) {
|
|
5799
|
+
if (!targetPlatforms.has(requestedPlatform)) {
|
|
5800
|
+
return {
|
|
5801
|
+
error: `An account ID was supplied for untargeted platform ${requestedPlatform}.`
|
|
5802
|
+
};
|
|
5803
|
+
}
|
|
5804
|
+
}
|
|
5805
|
+
const { data, error } = await callEdgeFunction(
|
|
5806
|
+
"mcp-data",
|
|
5807
|
+
{
|
|
5808
|
+
action: "connected-accounts",
|
|
5809
|
+
projectId: input.projectId,
|
|
5810
|
+
project_id: input.projectId
|
|
5811
|
+
},
|
|
5812
|
+
{ timeoutMs: 1e4 }
|
|
5813
|
+
);
|
|
5814
|
+
if (error || !Array.isArray(data?.accounts)) {
|
|
5815
|
+
return {
|
|
5816
|
+
error: `Connected-account verification failed: ${error ?? data?.error ?? "no account inventory returned"}.`
|
|
5817
|
+
};
|
|
5818
|
+
}
|
|
5819
|
+
const connectedAccountIds = {};
|
|
5820
|
+
for (const platform3 of input.platforms) {
|
|
5821
|
+
const canonical = canonicalPlatform(platform3);
|
|
5822
|
+
const displayPlatform = providerPlatform(platform3);
|
|
5823
|
+
const platformAccounts = data.accounts.filter(
|
|
5824
|
+
(account) => canonicalPlatform(account.platform) === canonical && account.project_id === input.projectId && isUsable(account)
|
|
5825
|
+
);
|
|
5826
|
+
const requestedId = normalizedRequested.ids?.get(canonical);
|
|
5827
|
+
let selected;
|
|
5828
|
+
if (requestedId) {
|
|
5829
|
+
selected = data.accounts.find((account) => account.id === requestedId);
|
|
5830
|
+
if (!selected) {
|
|
5831
|
+
return {
|
|
5832
|
+
error: `${displayPlatform}: account ${requestedId} is not available for project_id ${input.projectId}.`
|
|
5833
|
+
};
|
|
5834
|
+
}
|
|
5835
|
+
if (canonicalPlatform(selected.platform) !== canonical) {
|
|
5836
|
+
return {
|
|
5837
|
+
error: `${displayPlatform}: account ${requestedId} belongs to ${selected.platform}.`
|
|
5838
|
+
};
|
|
5839
|
+
}
|
|
5840
|
+
if (selected.project_id !== input.projectId) {
|
|
5841
|
+
return {
|
|
5842
|
+
error: `${displayPlatform}: account ${requestedId} is not bound to project_id ${input.projectId}.`
|
|
5843
|
+
};
|
|
5844
|
+
}
|
|
5845
|
+
if (!isUsable(selected)) {
|
|
5846
|
+
return {
|
|
5847
|
+
error: `${displayPlatform}: account ${requestedId} is ${selected.effective_status ?? selected.status}.`
|
|
5848
|
+
};
|
|
5849
|
+
}
|
|
5850
|
+
} else if (platformAccounts.length === 1) {
|
|
5851
|
+
selected = platformAccounts[0];
|
|
5852
|
+
} else if (platformAccounts.length === 0) {
|
|
5853
|
+
return {
|
|
5854
|
+
error: `${displayPlatform}: no active account is bound to project_id ${input.projectId}.`
|
|
5855
|
+
};
|
|
5856
|
+
} else {
|
|
5857
|
+
return {
|
|
5858
|
+
error: `${displayPlatform}: multiple active accounts are bound to project_id ${input.projectId}; pass the exact account ID returned by list_connected_accounts.`
|
|
5859
|
+
};
|
|
5860
|
+
}
|
|
5861
|
+
connectedAccountIds[displayPlatform] = selected.id;
|
|
5862
|
+
}
|
|
5863
|
+
return { connectedAccountIds };
|
|
5864
|
+
}
|
|
5865
|
+
var PLATFORM_CASE_MAP;
|
|
5866
|
+
var init_connected_account_routing = __esm({
|
|
5867
|
+
"src/lib/connected-account-routing.ts"() {
|
|
5868
|
+
"use strict";
|
|
5869
|
+
init_edge_function();
|
|
5870
|
+
PLATFORM_CASE_MAP = {
|
|
5871
|
+
youtube: "YouTube",
|
|
5872
|
+
tiktok: "TikTok",
|
|
5873
|
+
instagram: "Instagram",
|
|
5874
|
+
twitter: "Twitter",
|
|
5875
|
+
x: "Twitter",
|
|
5876
|
+
linkedin: "LinkedIn",
|
|
5877
|
+
facebook: "Facebook",
|
|
5878
|
+
threads: "Threads",
|
|
5879
|
+
bluesky: "Bluesky"
|
|
5880
|
+
};
|
|
5881
|
+
}
|
|
5882
|
+
});
|
|
5883
|
+
|
|
5502
5884
|
// src/tools/distribution.ts
|
|
5503
5885
|
import { z as z3 } from "zod";
|
|
5504
5886
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -5611,21 +5993,6 @@ async function validatePublishMediaUrl(url) {
|
|
|
5611
5993
|
function accountEffectiveStatus(account) {
|
|
5612
5994
|
return account.effective_status || account.status;
|
|
5613
5995
|
}
|
|
5614
|
-
function isUsableAccount(account) {
|
|
5615
|
-
const status = accountEffectiveStatus(account);
|
|
5616
|
-
return status === "active" || status === "expires_soon";
|
|
5617
|
-
}
|
|
5618
|
-
function formatAccountChoice(account) {
|
|
5619
|
-
const name = account.username ? `@${account.username}` : "unnamed";
|
|
5620
|
-
const project = account.project_id ? `project_id=${account.project_id}` : "project_id=unassigned";
|
|
5621
|
-
const refresh = account.has_refresh_token ? "OAuth 2.0 refresh" : "no refresh token";
|
|
5622
|
-
return `${account.id} (${name}, ${project}, ${accountEffectiveStatus(account)}, ${refresh})`;
|
|
5623
|
-
}
|
|
5624
|
-
function requestedAccountIdForPlatform(accountId, accountIds, platform3) {
|
|
5625
|
-
if (accountId) return accountId;
|
|
5626
|
-
if (!accountIds) return void 0;
|
|
5627
|
-
return accountIds[platform3] || accountIds[platform3.toLowerCase()];
|
|
5628
|
-
}
|
|
5629
5996
|
async function rehostExternalUrl(mediaUrl, projectId) {
|
|
5630
5997
|
const ssrf = await validateUrlForSSRF(mediaUrl);
|
|
5631
5998
|
if (!ssrf.isValid) {
|
|
@@ -5761,17 +6128,17 @@ function registerDistributionTools(server) {
|
|
|
5761
6128
|
schedule_at: z3.string().optional().describe(
|
|
5762
6129
|
'ISO 8601 UTC datetime for scheduled posting (e.g. "2026-03-20T14:00:00Z"). Omit to post immediately. Must be in the future.'
|
|
5763
6130
|
),
|
|
5764
|
-
project_id: z3.string().optional().describe(
|
|
6131
|
+
project_id: z3.string().uuid().optional().describe(
|
|
5765
6132
|
"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."
|
|
5766
6133
|
),
|
|
5767
6134
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
|
|
5768
6135
|
attribution: z3.boolean().optional().describe(
|
|
5769
6136
|
'If true, appends "Created with Social Neuron" to the caption. Default: false.'
|
|
5770
6137
|
),
|
|
5771
|
-
account_id: z3.string().optional().describe(
|
|
5772
|
-
"Connected account ID to post from.
|
|
6138
|
+
account_id: z3.string().uuid().optional().describe(
|
|
6139
|
+
"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."
|
|
5773
6140
|
),
|
|
5774
|
-
account_ids: z3.record(z3.string(), z3.string()).optional().describe(
|
|
6141
|
+
account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
|
|
5775
6142
|
'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.'
|
|
5776
6143
|
),
|
|
5777
6144
|
auto_rehost: z3.boolean().optional().describe(
|
|
@@ -5815,6 +6182,45 @@ function registerDistributionTools(server) {
|
|
|
5815
6182
|
isError: true
|
|
5816
6183
|
};
|
|
5817
6184
|
}
|
|
6185
|
+
const projectResolution = await resolveProjectForConnectedAccountTool(
|
|
6186
|
+
project_id,
|
|
6187
|
+
platforms
|
|
6188
|
+
);
|
|
6189
|
+
if (!projectResolution.projectId) {
|
|
6190
|
+
return {
|
|
6191
|
+
content: [
|
|
6192
|
+
{
|
|
6193
|
+
type: "text",
|
|
6194
|
+
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."
|
|
6195
|
+
}
|
|
6196
|
+
],
|
|
6197
|
+
isError: true
|
|
6198
|
+
};
|
|
6199
|
+
}
|
|
6200
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
6201
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
6202
|
+
if (account_id && account_ids) {
|
|
6203
|
+
return {
|
|
6204
|
+
content: [
|
|
6205
|
+
{
|
|
6206
|
+
type: "text",
|
|
6207
|
+
text: "Pass either account_id or account_ids, not both."
|
|
6208
|
+
}
|
|
6209
|
+
],
|
|
6210
|
+
isError: true
|
|
6211
|
+
};
|
|
6212
|
+
}
|
|
6213
|
+
if (account_id && platforms.length !== 1) {
|
|
6214
|
+
return {
|
|
6215
|
+
content: [
|
|
6216
|
+
{
|
|
6217
|
+
type: "text",
|
|
6218
|
+
text: "account_id is valid only for a single target platform. Use account_ids for multi-platform publishing."
|
|
6219
|
+
}
|
|
6220
|
+
],
|
|
6221
|
+
isError: true
|
|
6222
|
+
};
|
|
6223
|
+
}
|
|
5818
6224
|
const userId = await getDefaultUserId();
|
|
5819
6225
|
const rateLimit = checkRateLimit("posting", `schedule_post:${userId}`);
|
|
5820
6226
|
if (!rateLimit.allowed) {
|
|
@@ -5919,11 +6325,16 @@ function registerDistributionTools(server) {
|
|
|
5919
6325
|
}
|
|
5920
6326
|
const resolvedJobs = resolved;
|
|
5921
6327
|
resolvedMediaUrls = resolvedJobs.map((item) => item.url);
|
|
5922
|
-
resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map(
|
|
6328
|
+
resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map(
|
|
6329
|
+
(item) => item.trustedR2
|
|
6330
|
+
);
|
|
5923
6331
|
}
|
|
5924
6332
|
const shouldRehost = auto_rehost !== false;
|
|
5925
6333
|
if (shouldRehost && resolvedMediaUrl && !resolvedMediaUrlIsTrustedR2) {
|
|
5926
|
-
const rehost = await rehostExternalUrl(
|
|
6334
|
+
const rehost = await rehostExternalUrl(
|
|
6335
|
+
resolvedMediaUrl,
|
|
6336
|
+
resolvedProjectId
|
|
6337
|
+
);
|
|
5927
6338
|
if ("error" in rehost) {
|
|
5928
6339
|
return {
|
|
5929
6340
|
content: [
|
|
@@ -5941,7 +6352,7 @@ function registerDistributionTools(server) {
|
|
|
5941
6352
|
if (shouldRehost && resolvedMediaUrls && resolvedMediaUrls.length > 0) {
|
|
5942
6353
|
const rehosted = await Promise.all(
|
|
5943
6354
|
resolvedMediaUrls.map(
|
|
5944
|
-
(u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u,
|
|
6355
|
+
(u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, resolvedProjectId)
|
|
5945
6356
|
)
|
|
5946
6357
|
);
|
|
5947
6358
|
const failIdx = rehosted.findIndex((r) => "error" in r);
|
|
@@ -6008,7 +6419,7 @@ function registerDistributionTools(server) {
|
|
|
6008
6419
|
}
|
|
6009
6420
|
}
|
|
6010
6421
|
const normalizedPlatforms = platforms.map(
|
|
6011
|
-
(p) =>
|
|
6422
|
+
(p) => PLATFORM_CASE_MAP2[p.toLowerCase()] || p
|
|
6012
6423
|
);
|
|
6013
6424
|
const blockedPlatforms = normalizedPlatforms.filter(
|
|
6014
6425
|
(p) => MCP_NOT_LIVE_FOR_POSTING.has(p)
|
|
@@ -6024,92 +6435,27 @@ function registerDistributionTools(server) {
|
|
|
6024
6435
|
isError: true
|
|
6025
6436
|
};
|
|
6026
6437
|
}
|
|
6027
|
-
|
|
6028
|
-
"mcp-data",
|
|
6029
|
-
{
|
|
6030
|
-
action: "connected-accounts",
|
|
6031
|
-
...project_id ? { projectId: project_id, project_id } : {}
|
|
6032
|
-
},
|
|
6033
|
-
{ timeoutMs: 1e4 }
|
|
6034
|
-
);
|
|
6035
|
-
if (accountsData?.accounts) {
|
|
6036
|
-
const accounts = accountsData.accounts;
|
|
6037
|
-
const issues = [];
|
|
6038
|
-
for (const platform3 of normalizedPlatforms) {
|
|
6039
|
-
const platformAccounts = accounts.filter(
|
|
6040
|
-
(a) => a.platform.toLowerCase() === platform3.toLowerCase() && isUsableAccount(a)
|
|
6041
|
-
);
|
|
6042
|
-
const requestedAccountId = requestedAccountIdForPlatform(
|
|
6043
|
-
account_id,
|
|
6044
|
-
account_ids,
|
|
6045
|
-
platform3
|
|
6046
|
-
);
|
|
6047
|
-
if (requestedAccountId) {
|
|
6048
|
-
const selected = accounts.find((a) => a.id === requestedAccountId);
|
|
6049
|
-
if (!selected) {
|
|
6050
|
-
issues.push(
|
|
6051
|
-
`${platform3}: Account "${requestedAccountId}" is not available${project_id ? ` for project_id ${project_id}` : ""}. Call \`list_connected_accounts\`${project_id ? " with the same project_id" : ""} and choose one of the returned IDs.`
|
|
6052
|
-
);
|
|
6053
|
-
} else if (selected.platform.toLowerCase() !== platform3.toLowerCase()) {
|
|
6054
|
-
issues.push(
|
|
6055
|
-
`${platform3}: Account "${requestedAccountId}" belongs to ${selected.platform}, not ${platform3}.`
|
|
6056
|
-
);
|
|
6057
|
-
} else if (!isUsableAccount(selected)) {
|
|
6058
|
-
issues.push(
|
|
6059
|
-
`${platform3}: Account "${selected.username || selected.id}" is ${accountEffectiveStatus(selected)}. Reconnect at socialneuron.com/settings/connections.`
|
|
6060
|
-
);
|
|
6061
|
-
} else if (project_id && selected.project_id && selected.project_id !== project_id) {
|
|
6062
|
-
issues.push(
|
|
6063
|
-
`${platform3}: Account "${selected.username || selected.id}" is bound to project_id ${selected.project_id}, not ${project_id}. Use the selected brand's account or reconnect/assign it in Settings > Integrations.`
|
|
6064
|
-
);
|
|
6065
|
-
}
|
|
6066
|
-
continue;
|
|
6067
|
-
}
|
|
6068
|
-
if (platformAccounts.length === 0) {
|
|
6069
|
-
issues.push(
|
|
6070
|
-
`${platform3}: not connected yet. This is a one-time browser setup on socialneuron.com \u2014 NOT another OAuth in Claude. Call \`start_platform_connection\` with platform="${platform3.toLowerCase()}"${project_id ? ` and project_id="${project_id}"` : ""} to get a deep link, ask the user to open it in their browser and approve on the platform, then call \`wait_for_connection\` before retrying schedule_post.`
|
|
6071
|
-
);
|
|
6072
|
-
} else if (platformAccounts.length > 1) {
|
|
6073
|
-
const accountList = platformAccounts.map((a) => ` - ${formatAccountChoice(a)}`).join("\n");
|
|
6074
|
-
issues.push(
|
|
6075
|
-
`${platform3}: Multiple accounts found. Specify account_id or account_ids to choose:
|
|
6076
|
-
${accountList}`
|
|
6077
|
-
);
|
|
6078
|
-
} else if (platformAccounts.length === 1) {
|
|
6079
|
-
const acct = platformAccounts[0];
|
|
6080
|
-
if (acct.expires_at && new Date(acct.expires_at) < /* @__PURE__ */ new Date() && !acct.has_refresh_token) {
|
|
6081
|
-
issues.push(
|
|
6082
|
-
`${platform3}: Account "${acct.username || acct.id}" has expired OAuth and no refresh token. Reconnect at socialneuron.com/settings/connections.`
|
|
6083
|
-
);
|
|
6084
|
-
}
|
|
6085
|
-
}
|
|
6086
|
-
}
|
|
6087
|
-
if (issues.length > 0) {
|
|
6088
|
-
return {
|
|
6089
|
-
content: [
|
|
6090
|
-
{
|
|
6091
|
-
type: "text",
|
|
6092
|
-
text: `Cannot post \u2014 account issues found:
|
|
6093
|
-
|
|
6094
|
-
${issues.join("\n\n")}`
|
|
6095
|
-
}
|
|
6096
|
-
],
|
|
6097
|
-
isError: true
|
|
6098
|
-
};
|
|
6099
|
-
}
|
|
6100
|
-
}
|
|
6101
|
-
let connectedAccountIds;
|
|
6438
|
+
let requestedAccountIds;
|
|
6102
6439
|
if (account_id) {
|
|
6103
|
-
|
|
6104
|
-
for (const p of normalizedPlatforms) {
|
|
6105
|
-
connectedAccountIds[p] = account_id;
|
|
6106
|
-
}
|
|
6440
|
+
requestedAccountIds = { [normalizedPlatforms[0]]: account_id };
|
|
6107
6441
|
} else if (account_ids) {
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6442
|
+
requestedAccountIds = account_ids;
|
|
6443
|
+
}
|
|
6444
|
+
const routing = await resolveConnectedAccountRouting({
|
|
6445
|
+
projectId: resolvedProjectId,
|
|
6446
|
+
platforms: normalizedPlatforms,
|
|
6447
|
+
requestedAccountIds
|
|
6448
|
+
});
|
|
6449
|
+
if (routing.error || !routing.connectedAccountIds) {
|
|
6450
|
+
return {
|
|
6451
|
+
content: [
|
|
6452
|
+
{
|
|
6453
|
+
type: "text",
|
|
6454
|
+
text: `Cannot post \u2014 ${routing.error ?? "exact connected-account routing could not be established."}`
|
|
6455
|
+
}
|
|
6456
|
+
],
|
|
6457
|
+
isError: true
|
|
6458
|
+
};
|
|
6113
6459
|
}
|
|
6114
6460
|
let finalCaption = caption;
|
|
6115
6461
|
if (attribution && finalCaption) {
|
|
@@ -6163,8 +6509,9 @@ Created with Social Neuron`;
|
|
|
6163
6509
|
title,
|
|
6164
6510
|
hashtags,
|
|
6165
6511
|
scheduledAt: schedule_at,
|
|
6166
|
-
projectId:
|
|
6167
|
-
|
|
6512
|
+
projectId: resolvedProjectId,
|
|
6513
|
+
project_id: resolvedProjectId,
|
|
6514
|
+
connectedAccountIds: routing.connectedAccountIds,
|
|
6168
6515
|
...normalizedPlatformMetadata ? {
|
|
6169
6516
|
platformMetadata: convertPlatformMetadata(
|
|
6170
6517
|
normalizedPlatformMetadata
|
|
@@ -6199,10 +6546,14 @@ Created with Social Neuron`;
|
|
|
6199
6546
|
isError: true
|
|
6200
6547
|
};
|
|
6201
6548
|
}
|
|
6549
|
+
const responseData = projectAutoResolvedNote ? { ...data, project_auto_resolved: projectAutoResolvedNote } : data;
|
|
6202
6550
|
const lines = [
|
|
6203
6551
|
data.success ? "Post scheduled successfully." : "Post scheduling had errors.",
|
|
6204
6552
|
`Scheduled for: ${data.scheduledAt}`
|
|
6205
6553
|
];
|
|
6554
|
+
if (projectAutoResolvedNote) {
|
|
6555
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
6556
|
+
}
|
|
6206
6557
|
if (tiktokAutoInboxApplied) {
|
|
6207
6558
|
lines.push(
|
|
6208
6559
|
"",
|
|
@@ -6220,7 +6571,7 @@ Created with Social Neuron`;
|
|
|
6220
6571
|
}
|
|
6221
6572
|
}
|
|
6222
6573
|
if (format === "json") {
|
|
6223
|
-
const structuredContent = asEnvelope3(
|
|
6574
|
+
const structuredContent = asEnvelope3(responseData);
|
|
6224
6575
|
return {
|
|
6225
6576
|
structuredContent,
|
|
6226
6577
|
content: [
|
|
@@ -6233,7 +6584,7 @@ Created with Social Neuron`;
|
|
|
6233
6584
|
};
|
|
6234
6585
|
}
|
|
6235
6586
|
return {
|
|
6236
|
-
structuredContent: asEnvelope3(
|
|
6587
|
+
structuredContent: asEnvelope3(responseData),
|
|
6237
6588
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
6238
6589
|
isError: !data.success
|
|
6239
6590
|
};
|
|
@@ -6247,7 +6598,9 @@ Created with Social Neuron`;
|
|
|
6247
6598
|
project_id: z3.string().uuid().optional().describe(
|
|
6248
6599
|
"Brand/project ID that owns the post. Defaults to the authenticated key's project or the account default."
|
|
6249
6600
|
),
|
|
6250
|
-
scheduled_at: z3.string().datetime({ offset: true }).describe(
|
|
6601
|
+
scheduled_at: z3.string().datetime({ offset: true }).describe(
|
|
6602
|
+
"New future publish time as an ISO 8601 datetime with timezone."
|
|
6603
|
+
),
|
|
6251
6604
|
expected_scheduled_at: z3.string().datetime({ offset: true }).optional().describe(
|
|
6252
6605
|
"Optional current schedule timestamp. If it changed since you read it, the update is rejected instead of silently overwriting it."
|
|
6253
6606
|
),
|
|
@@ -6303,7 +6656,11 @@ Created with Social Neuron`;
|
|
|
6303
6656
|
projectId: resolvedProjectId,
|
|
6304
6657
|
project_id: resolvedProjectId,
|
|
6305
6658
|
scheduled_at: next.toISOString(),
|
|
6306
|
-
...expected_scheduled_at ? {
|
|
6659
|
+
...expected_scheduled_at ? {
|
|
6660
|
+
expected_scheduled_at: new Date(
|
|
6661
|
+
expected_scheduled_at
|
|
6662
|
+
).toISOString()
|
|
6663
|
+
} : {}
|
|
6307
6664
|
});
|
|
6308
6665
|
if (error || !result?.success) {
|
|
6309
6666
|
const code = result?.error ?? error ?? "reschedule_failed";
|
|
@@ -6336,17 +6693,34 @@ Created with Social Neuron`;
|
|
|
6336
6693
|
"list_connected_accounts",
|
|
6337
6694
|
"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.",
|
|
6338
6695
|
{
|
|
6339
|
-
project_id: z3.string().optional().describe(
|
|
6696
|
+
project_id: z3.string().uuid().optional().describe(
|
|
6340
6697
|
"Brand/project ID to scope connected accounts. Use the same project_id when calling schedule_post."
|
|
6341
6698
|
),
|
|
6342
|
-
include_all: z3.boolean().optional().describe(
|
|
6699
|
+
include_all: z3.boolean().optional().describe(
|
|
6700
|
+
"If true, include expired or inactive accounts as well as usable accounts."
|
|
6701
|
+
),
|
|
6343
6702
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6344
6703
|
},
|
|
6345
6704
|
async ({ project_id, include_all, response_format }) => {
|
|
6346
6705
|
const format = response_format ?? "text";
|
|
6706
|
+
const projectResolution = await resolveProjectForConnectedAccountTool(project_id);
|
|
6707
|
+
if (!projectResolution.projectId) {
|
|
6708
|
+
return {
|
|
6709
|
+
content: [
|
|
6710
|
+
{
|
|
6711
|
+
type: "text",
|
|
6712
|
+
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."
|
|
6713
|
+
}
|
|
6714
|
+
],
|
|
6715
|
+
isError: true
|
|
6716
|
+
};
|
|
6717
|
+
}
|
|
6718
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
6719
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
6347
6720
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
6348
6721
|
action: "connected-accounts",
|
|
6349
|
-
|
|
6722
|
+
projectId: resolvedProjectId,
|
|
6723
|
+
project_id: resolvedProjectId,
|
|
6350
6724
|
...include_all ? { includeAll: true } : {}
|
|
6351
6725
|
});
|
|
6352
6726
|
if (efError || !result?.success) {
|
|
@@ -6360,10 +6734,27 @@ Created with Social Neuron`;
|
|
|
6360
6734
|
isError: true
|
|
6361
6735
|
};
|
|
6362
6736
|
}
|
|
6363
|
-
const
|
|
6737
|
+
const parsedAccounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
|
|
6738
|
+
if (parsedAccounts.some(
|
|
6739
|
+
(account) => account.project_id !== resolvedProjectId
|
|
6740
|
+
)) {
|
|
6741
|
+
return {
|
|
6742
|
+
content: [
|
|
6743
|
+
{
|
|
6744
|
+
type: "text",
|
|
6745
|
+
text: "Connected-account project attestation failed. No account inventory was returned."
|
|
6746
|
+
}
|
|
6747
|
+
],
|
|
6748
|
+
isError: true
|
|
6749
|
+
};
|
|
6750
|
+
}
|
|
6751
|
+
const accounts = parsedAccounts;
|
|
6364
6752
|
if (accounts.length === 0) {
|
|
6365
6753
|
if (format === "json") {
|
|
6366
|
-
const structuredContent = asEnvelope3({
|
|
6754
|
+
const structuredContent = asEnvelope3({
|
|
6755
|
+
accounts: [],
|
|
6756
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
6757
|
+
});
|
|
6367
6758
|
return {
|
|
6368
6759
|
structuredContent,
|
|
6369
6760
|
content: [
|
|
@@ -6378,13 +6769,15 @@ Created with Social Neuron`;
|
|
|
6378
6769
|
content: [
|
|
6379
6770
|
{
|
|
6380
6771
|
type: "text",
|
|
6381
|
-
text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections."
|
|
6772
|
+
text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections." + (projectAutoResolvedNote ? `
|
|
6773
|
+
|
|
6774
|
+
Note: ${projectAutoResolvedNote}` : "")
|
|
6382
6775
|
}
|
|
6383
6776
|
]
|
|
6384
6777
|
};
|
|
6385
6778
|
}
|
|
6386
6779
|
const lines = [
|
|
6387
|
-
`${accounts.length} connected account(s)
|
|
6780
|
+
`${accounts.length} connected account(s) for project ${resolvedProjectId}:`,
|
|
6388
6781
|
""
|
|
6389
6782
|
];
|
|
6390
6783
|
for (const account of accounts) {
|
|
@@ -6396,8 +6789,14 @@ Created with Social Neuron`;
|
|
|
6396
6789
|
` ${platformLower}: ${name} | id=${account.id} | ${project} | status=${status} (connected ${account.created_at.split("T")[0]})`
|
|
6397
6790
|
);
|
|
6398
6791
|
}
|
|
6792
|
+
if (projectAutoResolvedNote) {
|
|
6793
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
6794
|
+
}
|
|
6399
6795
|
if (format === "json") {
|
|
6400
|
-
const structuredContent = asEnvelope3({
|
|
6796
|
+
const structuredContent = asEnvelope3({
|
|
6797
|
+
accounts,
|
|
6798
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
6799
|
+
});
|
|
6401
6800
|
return {
|
|
6402
6801
|
structuredContent,
|
|
6403
6802
|
content: [
|
|
@@ -6592,7 +6991,10 @@ Created with Social Neuron`;
|
|
|
6592
6991
|
if (!Number.isFinite(startDate.getTime())) {
|
|
6593
6992
|
return {
|
|
6594
6993
|
content: [
|
|
6595
|
-
{
|
|
6994
|
+
{
|
|
6995
|
+
type: "text",
|
|
6996
|
+
text: "start_after must be a valid ISO datetime."
|
|
6997
|
+
}
|
|
6596
6998
|
],
|
|
6597
6999
|
isError: true
|
|
6598
7000
|
};
|
|
@@ -6693,11 +7095,14 @@ Created with Social Neuron`;
|
|
|
6693
7095
|
"Schedule all posts in a content plan. Optionally auto-assigns time slots and runs quality checks before scheduling. Supports dry-run mode.",
|
|
6694
7096
|
{
|
|
6695
7097
|
plan: z3.object({
|
|
7098
|
+
project_id: z3.string().uuid().optional(),
|
|
7099
|
+
account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe("Exact connected-account ID per platform."),
|
|
6696
7100
|
posts: z3.array(
|
|
6697
7101
|
z3.object({
|
|
6698
7102
|
id: z3.string(),
|
|
6699
7103
|
caption: z3.string(),
|
|
6700
7104
|
platform: z3.string(),
|
|
7105
|
+
connected_account_id: z3.string().uuid().optional(),
|
|
6701
7106
|
title: z3.string().optional(),
|
|
6702
7107
|
media_url: z3.string().optional(),
|
|
6703
7108
|
schedule_at: z3.string().optional(),
|
|
@@ -6705,6 +7110,12 @@ Created with Social Neuron`;
|
|
|
6705
7110
|
})
|
|
6706
7111
|
)
|
|
6707
7112
|
}).passthrough().optional(),
|
|
7113
|
+
project_id: z3.string().uuid().optional().describe(
|
|
7114
|
+
"Exact brand/project ID. Defaults only when the authenticated user has one project."
|
|
7115
|
+
),
|
|
7116
|
+
account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
|
|
7117
|
+
"Exact connected-account ID per platform for every post in the plan."
|
|
7118
|
+
),
|
|
6708
7119
|
plan_id: z3.string().uuid().optional().describe("Persisted content plan ID from content_plans table"),
|
|
6709
7120
|
auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
|
|
6710
7121
|
dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
|
|
@@ -6721,6 +7132,8 @@ Created with Social Neuron`;
|
|
|
6721
7132
|
async ({
|
|
6722
7133
|
plan,
|
|
6723
7134
|
plan_id,
|
|
7135
|
+
project_id,
|
|
7136
|
+
account_ids,
|
|
6724
7137
|
auto_slot,
|
|
6725
7138
|
dry_run,
|
|
6726
7139
|
response_format,
|
|
@@ -6730,9 +7143,11 @@ Created with Social Neuron`;
|
|
|
6730
7143
|
idempotency_seed
|
|
6731
7144
|
}) => {
|
|
6732
7145
|
try {
|
|
7146
|
+
const effectiveBatchSize = batch_size ?? 4;
|
|
6733
7147
|
let workingPlan = plan;
|
|
6734
7148
|
let effectivePlanId = plan_id;
|
|
6735
|
-
let effectiveProjectId;
|
|
7149
|
+
let effectiveProjectId = project_id;
|
|
7150
|
+
let projectAutoResolvedNote;
|
|
6736
7151
|
let approvalSummary;
|
|
6737
7152
|
if (!workingPlan && plan_id) {
|
|
6738
7153
|
const { data: planResult, error: planError } = await callEdgeFunction("mcp-data", { action: "get-content-plan", plan_id });
|
|
@@ -6779,23 +7194,59 @@ Created with Social Neuron`;
|
|
|
6779
7194
|
posts: postsFromPayload
|
|
6780
7195
|
};
|
|
6781
7196
|
effectivePlanId = stored.id;
|
|
6782
|
-
effectiveProjectId
|
|
7197
|
+
if (effectiveProjectId && stored.project_id && effectiveProjectId !== stored.project_id) {
|
|
7198
|
+
return {
|
|
7199
|
+
content: [
|
|
7200
|
+
{
|
|
7201
|
+
type: "text",
|
|
7202
|
+
text: `project_id ${effectiveProjectId} does not own plan ${plan_id}.`
|
|
7203
|
+
}
|
|
7204
|
+
],
|
|
7205
|
+
isError: true
|
|
7206
|
+
};
|
|
7207
|
+
}
|
|
7208
|
+
effectiveProjectId = stored.project_id ?? effectiveProjectId;
|
|
7209
|
+
}
|
|
7210
|
+
if (!workingPlan) {
|
|
7211
|
+
return {
|
|
7212
|
+
content: [
|
|
7213
|
+
{
|
|
7214
|
+
type: "text",
|
|
7215
|
+
text: "Provide either `plan` (inline) or `plan_id` (persisted) to schedule content."
|
|
7216
|
+
}
|
|
7217
|
+
],
|
|
7218
|
+
isError: true
|
|
7219
|
+
};
|
|
6783
7220
|
}
|
|
6784
|
-
|
|
7221
|
+
const planProjectId = workingPlan.project_id;
|
|
7222
|
+
if (effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0 && effectiveProjectId !== planProjectId) {
|
|
6785
7223
|
return {
|
|
6786
7224
|
content: [
|
|
6787
7225
|
{
|
|
6788
7226
|
type: "text",
|
|
6789
|
-
text:
|
|
7227
|
+
text: `Conflicting project_id values were supplied for the plan (${planProjectId}) and request (${effectiveProjectId}).`
|
|
6790
7228
|
}
|
|
6791
7229
|
],
|
|
6792
7230
|
isError: true
|
|
6793
7231
|
};
|
|
6794
7232
|
}
|
|
7233
|
+
if (!effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0) {
|
|
7234
|
+
effectiveProjectId = planProjectId;
|
|
7235
|
+
}
|
|
6795
7236
|
if (!effectiveProjectId) {
|
|
6796
|
-
const
|
|
6797
|
-
|
|
6798
|
-
|
|
7237
|
+
const projectResolution = await resolveProjectForConnectedAccountTool();
|
|
7238
|
+
effectiveProjectId = projectResolution.projectId;
|
|
7239
|
+
projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
7240
|
+
if (!effectiveProjectId) {
|
|
7241
|
+
return {
|
|
7242
|
+
content: [
|
|
7243
|
+
{
|
|
7244
|
+
type: "text",
|
|
7245
|
+
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."
|
|
7246
|
+
}
|
|
7247
|
+
],
|
|
7248
|
+
isError: true
|
|
7249
|
+
};
|
|
6799
7250
|
}
|
|
6800
7251
|
}
|
|
6801
7252
|
if (effectivePlanId) {
|
|
@@ -7014,6 +7465,61 @@ Created with Social Neuron`;
|
|
|
7014
7465
|
isError: false
|
|
7015
7466
|
};
|
|
7016
7467
|
}
|
|
7468
|
+
const embeddedAccountIds = workingPlan.account_ids ?? {};
|
|
7469
|
+
for (const [platform3, accountId] of Object.entries(account_ids ?? {})) {
|
|
7470
|
+
if (embeddedAccountIds[platform3] && embeddedAccountIds[platform3] !== accountId) {
|
|
7471
|
+
return {
|
|
7472
|
+
content: [
|
|
7473
|
+
{
|
|
7474
|
+
type: "text",
|
|
7475
|
+
text: `Conflicting account_ids values were supplied for ${platform3}.`
|
|
7476
|
+
}
|
|
7477
|
+
],
|
|
7478
|
+
isError: true
|
|
7479
|
+
};
|
|
7480
|
+
}
|
|
7481
|
+
}
|
|
7482
|
+
const requestedPlanAccountIds = {
|
|
7483
|
+
...embeddedAccountIds,
|
|
7484
|
+
...account_ids ?? {}
|
|
7485
|
+
};
|
|
7486
|
+
for (const post of workingPlan.posts) {
|
|
7487
|
+
if (!post.connected_account_id) continue;
|
|
7488
|
+
const key = post.platform.toLowerCase() === "x" ? "twitter" : post.platform.toLowerCase();
|
|
7489
|
+
const existing = requestedPlanAccountIds[key];
|
|
7490
|
+
if (existing && existing !== post.connected_account_id) {
|
|
7491
|
+
return {
|
|
7492
|
+
content: [
|
|
7493
|
+
{
|
|
7494
|
+
type: "text",
|
|
7495
|
+
text: `Plan contains conflicting connected_account_id values for ${post.platform}. Use separate plans when scheduling the same platform through different accounts.`
|
|
7496
|
+
}
|
|
7497
|
+
],
|
|
7498
|
+
isError: true
|
|
7499
|
+
};
|
|
7500
|
+
}
|
|
7501
|
+
requestedPlanAccountIds[key] = post.connected_account_id;
|
|
7502
|
+
}
|
|
7503
|
+
const planPlatforms = Array.from(
|
|
7504
|
+
new Set(workingPlan.posts.map((post) => post.platform))
|
|
7505
|
+
);
|
|
7506
|
+
const planRouting = await resolveConnectedAccountRouting({
|
|
7507
|
+
projectId: effectiveProjectId,
|
|
7508
|
+
platforms: planPlatforms,
|
|
7509
|
+
requestedAccountIds: requestedPlanAccountIds
|
|
7510
|
+
});
|
|
7511
|
+
if (planRouting.error || !planRouting.connectedAccountIds) {
|
|
7512
|
+
return {
|
|
7513
|
+
content: [
|
|
7514
|
+
{
|
|
7515
|
+
type: "text",
|
|
7516
|
+
text: `Cannot schedule plan \u2014 ${planRouting.error ?? "exact connected-account routing could not be established."}`
|
|
7517
|
+
}
|
|
7518
|
+
],
|
|
7519
|
+
isError: true
|
|
7520
|
+
};
|
|
7521
|
+
}
|
|
7522
|
+
const verifiedPlanAccountIds = planRouting.connectedAccountIds;
|
|
7017
7523
|
let scheduled = 0;
|
|
7018
7524
|
let failed = 0;
|
|
7019
7525
|
const results = [];
|
|
@@ -7042,7 +7548,7 @@ Created with Social Neuron`;
|
|
|
7042
7548
|
retryable: false
|
|
7043
7549
|
};
|
|
7044
7550
|
}
|
|
7045
|
-
const normalizedPlatform =
|
|
7551
|
+
const normalizedPlatform = PLATFORM_CASE_MAP2[post.platform.toLowerCase()] ?? post.platform;
|
|
7046
7552
|
const idempotencyKey = buildIdempotencyKey(post);
|
|
7047
7553
|
const { data, error } = await callEdgeFunction(
|
|
7048
7554
|
"schedule-post",
|
|
@@ -7053,6 +7559,13 @@ Created with Social Neuron`;
|
|
|
7053
7559
|
mediaUrl: post.media_url,
|
|
7054
7560
|
scheduledAt: post.schedule_at,
|
|
7055
7561
|
hashtags: post.hashtags,
|
|
7562
|
+
...effectiveProjectId ? {
|
|
7563
|
+
projectId: effectiveProjectId,
|
|
7564
|
+
project_id: effectiveProjectId
|
|
7565
|
+
} : {},
|
|
7566
|
+
connectedAccountIds: {
|
|
7567
|
+
[normalizedPlatform]: verifiedPlanAccountIds[normalizedPlatform]
|
|
7568
|
+
},
|
|
7056
7569
|
...effectivePlanId ? { planId: effectivePlanId } : {},
|
|
7057
7570
|
idempotencyKey
|
|
7058
7571
|
},
|
|
@@ -7111,7 +7624,7 @@ Created with Social Neuron`;
|
|
|
7111
7624
|
const platformBatches = Array.from(grouped.entries()).map(
|
|
7112
7625
|
async ([platform3, platformPosts]) => {
|
|
7113
7626
|
const platformResults = [];
|
|
7114
|
-
const batches = chunk(platformPosts,
|
|
7627
|
+
const batches = chunk(platformPosts, effectiveBatchSize);
|
|
7115
7628
|
for (const batch of batches) {
|
|
7116
7629
|
const settled = await Promise.allSettled(
|
|
7117
7630
|
batch.map((post) => scheduleOne(post))
|
|
@@ -7172,7 +7685,8 @@ Created with Social Neuron`;
|
|
|
7172
7685
|
total_posts: workingPlan.posts.length,
|
|
7173
7686
|
scheduled,
|
|
7174
7687
|
failed
|
|
7175
|
-
}
|
|
7688
|
+
},
|
|
7689
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
7176
7690
|
}),
|
|
7177
7691
|
null,
|
|
7178
7692
|
2
|
|
@@ -7196,6 +7710,9 @@ Created with Social Neuron`;
|
|
|
7196
7710
|
lines.push(
|
|
7197
7711
|
`Scheduled: ${scheduled}/${workingPlan.posts.length} | Failed: ${failed}/${workingPlan.posts.length}`
|
|
7198
7712
|
);
|
|
7713
|
+
if (projectAutoResolvedNote) {
|
|
7714
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
7715
|
+
}
|
|
7199
7716
|
return {
|
|
7200
7717
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
7201
7718
|
isError: failed > 0
|
|
@@ -7215,7 +7732,7 @@ Created with Social Neuron`;
|
|
|
7215
7732
|
}
|
|
7216
7733
|
);
|
|
7217
7734
|
}
|
|
7218
|
-
var
|
|
7735
|
+
var PLATFORM_CASE_MAP2, TIKTOK_AUDIT_APPROVED, MCP_NOT_LIVE_FOR_POSTING, VIDEO_FILE_EXTENSIONS, IMAGE_FILE_EXTENSIONS;
|
|
7219
7736
|
var init_distribution = __esm({
|
|
7220
7737
|
"src/tools/distribution.ts"() {
|
|
7221
7738
|
"use strict";
|
|
@@ -7226,11 +7743,17 @@ var init_distribution = __esm({
|
|
|
7226
7743
|
init_supabase();
|
|
7227
7744
|
init_quality();
|
|
7228
7745
|
init_version();
|
|
7229
|
-
|
|
7746
|
+
init_connected_account_routing();
|
|
7747
|
+
PLATFORM_CASE_MAP2 = {
|
|
7230
7748
|
youtube: "YouTube",
|
|
7231
7749
|
tiktok: "TikTok",
|
|
7232
7750
|
instagram: "Instagram",
|
|
7233
7751
|
twitter: "Twitter",
|
|
7752
|
+
// 'x' is the platform's current branding but connected_account_routing.ts
|
|
7753
|
+
// (and the DB convention) still key on 'Twitter' — keep both aliases
|
|
7754
|
+
// resolving to the same case so schedule_content_plan's platform:'x' posts
|
|
7755
|
+
// don't fall through to an undefined binding (F8, 2026-07-15).
|
|
7756
|
+
x: "Twitter",
|
|
7234
7757
|
linkedin: "LinkedIn",
|
|
7235
7758
|
facebook: "Facebook",
|
|
7236
7759
|
threads: "Threads",
|
|
@@ -7772,15 +8295,35 @@ function registerAnalyticsTools(server) {
|
|
|
7772
8295
|
content_id: z5.string().uuid().optional().describe(
|
|
7773
8296
|
"Filter to a specific content_history ID to see performance of one piece of content."
|
|
7774
8297
|
),
|
|
7775
|
-
project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
8298
|
+
project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
|
|
7776
8299
|
limit: z5.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
|
|
7777
8300
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
7778
8301
|
},
|
|
7779
|
-
async ({
|
|
8302
|
+
async ({
|
|
8303
|
+
platform: platform3,
|
|
8304
|
+
days,
|
|
8305
|
+
content_id,
|
|
8306
|
+
project_id,
|
|
8307
|
+
limit,
|
|
8308
|
+
response_format
|
|
8309
|
+
}) => {
|
|
7780
8310
|
const format = response_format ?? "text";
|
|
7781
8311
|
const lookbackDays = days ?? 30;
|
|
7782
8312
|
const maxPosts = limit ?? 20;
|
|
7783
|
-
const
|
|
8313
|
+
const projectResolution = await resolveProjectStrict(project_id);
|
|
8314
|
+
if (!projectResolution.projectId) {
|
|
8315
|
+
return {
|
|
8316
|
+
content: [
|
|
8317
|
+
{
|
|
8318
|
+
type: "text",
|
|
8319
|
+
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."
|
|
8320
|
+
}
|
|
8321
|
+
],
|
|
8322
|
+
isError: true
|
|
8323
|
+
};
|
|
8324
|
+
}
|
|
8325
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
8326
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
7784
8327
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
7785
8328
|
action: "analytics",
|
|
7786
8329
|
platform: platform3,
|
|
@@ -7791,11 +8334,17 @@ function registerAnalyticsTools(server) {
|
|
|
7791
8334
|
limit: Math.min(maxPosts * 5, 100),
|
|
7792
8335
|
latestOnly: true,
|
|
7793
8336
|
contentId: content_id,
|
|
7794
|
-
|
|
8337
|
+
projectId: resolvedProjectId,
|
|
8338
|
+
project_id: resolvedProjectId
|
|
7795
8339
|
});
|
|
7796
8340
|
if (efError) {
|
|
7797
8341
|
return {
|
|
7798
|
-
content: [
|
|
8342
|
+
content: [
|
|
8343
|
+
{
|
|
8344
|
+
type: "text",
|
|
8345
|
+
text: `Failed to fetch analytics: ${efError}`
|
|
8346
|
+
}
|
|
8347
|
+
],
|
|
7799
8348
|
isError: true
|
|
7800
8349
|
};
|
|
7801
8350
|
}
|
|
@@ -7815,7 +8364,8 @@ function registerAnalyticsTools(server) {
|
|
|
7815
8364
|
totalViews: 0,
|
|
7816
8365
|
totalEngagement: 0,
|
|
7817
8366
|
postCount: 0,
|
|
7818
|
-
posts: []
|
|
8367
|
+
posts: [],
|
|
8368
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
7819
8369
|
});
|
|
7820
8370
|
return {
|
|
7821
8371
|
structuredContent,
|
|
@@ -7831,7 +8381,9 @@ function registerAnalyticsTools(server) {
|
|
|
7831
8381
|
content: [
|
|
7832
8382
|
{
|
|
7833
8383
|
type: "text",
|
|
7834
|
-
text: `No analytics data found for the last ${lookbackDays} days${platform3 ? ` on ${platform3}` : ""}.`
|
|
8384
|
+
text: `No analytics data found for the last ${lookbackDays} days${platform3 ? ` on ${platform3}` : ""}.` + (projectAutoResolvedNote ? `
|
|
8385
|
+
|
|
8386
|
+
Note: ${projectAutoResolvedNote}` : "")
|
|
7835
8387
|
}
|
|
7836
8388
|
]
|
|
7837
8389
|
};
|
|
@@ -7863,20 +8415,27 @@ function registerAnalyticsTools(server) {
|
|
|
7863
8415
|
postCount: posts.length,
|
|
7864
8416
|
posts
|
|
7865
8417
|
};
|
|
7866
|
-
return formatAnalytics(
|
|
8418
|
+
return formatAnalytics(
|
|
8419
|
+
summary,
|
|
8420
|
+
lookbackDays,
|
|
8421
|
+
format,
|
|
8422
|
+
projectAutoResolvedNote
|
|
8423
|
+
);
|
|
7867
8424
|
}
|
|
7868
8425
|
);
|
|
7869
8426
|
server.tool(
|
|
7870
8427
|
"refresh_platform_analytics",
|
|
7871
8428
|
"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.",
|
|
7872
8429
|
{
|
|
7873
|
-
project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
8430
|
+
project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
|
|
7874
8431
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
7875
8432
|
},
|
|
7876
8433
|
async ({ project_id, response_format }) => {
|
|
7877
8434
|
const format = response_format ?? "text";
|
|
7878
8435
|
const userId = await getDefaultUserId();
|
|
7879
|
-
const
|
|
8436
|
+
const projectResolution = await resolveProjectStrict(project_id);
|
|
8437
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
8438
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
7880
8439
|
const rateLimit = checkRateLimit(
|
|
7881
8440
|
"posting",
|
|
7882
8441
|
`refresh_platform_analytics:${userId}:${resolvedProjectId ?? "default"}`
|
|
@@ -7892,25 +8451,48 @@ function registerAnalyticsTools(server) {
|
|
|
7892
8451
|
isError: true
|
|
7893
8452
|
};
|
|
7894
8453
|
}
|
|
8454
|
+
if (!resolvedProjectId) {
|
|
8455
|
+
return {
|
|
8456
|
+
content: [
|
|
8457
|
+
{
|
|
8458
|
+
type: "text",
|
|
8459
|
+
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."
|
|
8460
|
+
}
|
|
8461
|
+
],
|
|
8462
|
+
isError: true
|
|
8463
|
+
};
|
|
8464
|
+
}
|
|
7895
8465
|
const { data, error } = await callEdgeFunction("fetch-analytics", {
|
|
7896
8466
|
userId,
|
|
7897
|
-
|
|
8467
|
+
projectId: resolvedProjectId,
|
|
8468
|
+
project_id: resolvedProjectId
|
|
7898
8469
|
});
|
|
7899
8470
|
if (error) {
|
|
7900
8471
|
return {
|
|
7901
|
-
content: [
|
|
8472
|
+
content: [
|
|
8473
|
+
{
|
|
8474
|
+
type: "text",
|
|
8475
|
+
text: `Error refreshing analytics: ${error}`
|
|
8476
|
+
}
|
|
8477
|
+
],
|
|
7902
8478
|
isError: true
|
|
7903
8479
|
};
|
|
7904
8480
|
}
|
|
7905
8481
|
const result = data;
|
|
7906
8482
|
if (!result.success) {
|
|
7907
8483
|
return {
|
|
7908
|
-
content: [
|
|
8484
|
+
content: [
|
|
8485
|
+
{ type: "text", text: "Analytics refresh failed." }
|
|
8486
|
+
],
|
|
7909
8487
|
isError: true
|
|
7910
8488
|
};
|
|
7911
8489
|
}
|
|
7912
|
-
const queued = (result.results ?? []).filter(
|
|
7913
|
-
|
|
8490
|
+
const queued = (result.results ?? []).filter(
|
|
8491
|
+
(r) => r.status === "queued"
|
|
8492
|
+
).length;
|
|
8493
|
+
const errored = (result.results ?? []).filter(
|
|
8494
|
+
(r) => r.status === "error"
|
|
8495
|
+
).length;
|
|
7914
8496
|
const lines = [
|
|
7915
8497
|
`Analytics refresh triggered successfully.`,
|
|
7916
8498
|
` Posts processed: ${result.postsProcessed}`,
|
|
@@ -7919,13 +8501,17 @@ function registerAnalyticsTools(server) {
|
|
|
7919
8501
|
if (errored > 0) {
|
|
7920
8502
|
lines.push(` Errors: ${errored}`);
|
|
7921
8503
|
}
|
|
8504
|
+
if (projectAutoResolvedNote) {
|
|
8505
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
8506
|
+
}
|
|
7922
8507
|
if (format === "json") {
|
|
7923
8508
|
const structuredContent = asEnvelope4({
|
|
7924
8509
|
success: true,
|
|
7925
8510
|
postsProcessed: result.postsProcessed,
|
|
7926
8511
|
queued,
|
|
7927
8512
|
errored,
|
|
7928
|
-
projectId: resolvedProjectId ?? null
|
|
8513
|
+
projectId: resolvedProjectId ?? null,
|
|
8514
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
7929
8515
|
});
|
|
7930
8516
|
return {
|
|
7931
8517
|
structuredContent,
|
|
@@ -7941,12 +8527,21 @@ function registerAnalyticsTools(server) {
|
|
|
7941
8527
|
}
|
|
7942
8528
|
);
|
|
7943
8529
|
}
|
|
7944
|
-
function formatAnalytics(summary, days, format) {
|
|
7945
|
-
const structuredContent = asEnvelope4({
|
|
8530
|
+
function formatAnalytics(summary, days, format, projectAutoResolvedNote) {
|
|
8531
|
+
const structuredContent = asEnvelope4({
|
|
8532
|
+
...summary,
|
|
8533
|
+
days,
|
|
8534
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
8535
|
+
});
|
|
7946
8536
|
if (format === "json") {
|
|
7947
8537
|
return {
|
|
7948
8538
|
structuredContent,
|
|
7949
|
-
content: [
|
|
8539
|
+
content: [
|
|
8540
|
+
{
|
|
8541
|
+
type: "text",
|
|
8542
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
8543
|
+
}
|
|
8544
|
+
]
|
|
7950
8545
|
};
|
|
7951
8546
|
}
|
|
7952
8547
|
const lines = [
|
|
@@ -7970,6 +8565,9 @@ function formatAnalytics(summary, days, format) {
|
|
|
7970
8565
|
lines.push(line);
|
|
7971
8566
|
}
|
|
7972
8567
|
}
|
|
8568
|
+
if (projectAutoResolvedNote) {
|
|
8569
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
8570
|
+
}
|
|
7973
8571
|
return {
|
|
7974
8572
|
structuredContent,
|
|
7975
8573
|
content: [{ type: "text", text: lines.join("\n") }]
|
|
@@ -9399,15 +9997,64 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
9399
9997
|
start_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Start date in YYYY-MM-DD format."),
|
|
9400
9998
|
end_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("End date in YYYY-MM-DD format."),
|
|
9401
9999
|
video_id: z10.string().optional().describe('YouTube video ID. Required when action is "video".'),
|
|
9402
|
-
max_results: z10.number().min(1).max(50).optional().describe(
|
|
10000
|
+
max_results: z10.number().min(1).max(50).optional().describe(
|
|
10001
|
+
'Max videos to return for "topVideos" action. Defaults to 10.'
|
|
10002
|
+
),
|
|
10003
|
+
connected_account_id: z10.string().uuid().optional().describe(
|
|
10004
|
+
"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."
|
|
10005
|
+
),
|
|
10006
|
+
project_id: z10.string().uuid().optional().describe(
|
|
10007
|
+
"Exact brand/project ID. Defaults only when the authenticated user has one project."
|
|
10008
|
+
),
|
|
9403
10009
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9404
10010
|
},
|
|
9405
|
-
async ({
|
|
10011
|
+
async ({
|
|
10012
|
+
action,
|
|
10013
|
+
start_date,
|
|
10014
|
+
end_date,
|
|
10015
|
+
video_id,
|
|
10016
|
+
max_results,
|
|
10017
|
+
connected_account_id,
|
|
10018
|
+
project_id,
|
|
10019
|
+
response_format
|
|
10020
|
+
}) => {
|
|
9406
10021
|
const format = response_format ?? "text";
|
|
9407
10022
|
if (action === "video" && !video_id) {
|
|
9408
10023
|
return {
|
|
9409
10024
|
content: [
|
|
9410
|
-
{
|
|
10025
|
+
{
|
|
10026
|
+
type: "text",
|
|
10027
|
+
text: 'Error: video_id is required when action is "video".'
|
|
10028
|
+
}
|
|
10029
|
+
],
|
|
10030
|
+
isError: true
|
|
10031
|
+
};
|
|
10032
|
+
}
|
|
10033
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
10034
|
+
if (!resolvedProjectId) {
|
|
10035
|
+
return {
|
|
10036
|
+
content: [
|
|
10037
|
+
{
|
|
10038
|
+
type: "text",
|
|
10039
|
+
text: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
|
|
10040
|
+
}
|
|
10041
|
+
],
|
|
10042
|
+
isError: true
|
|
10043
|
+
};
|
|
10044
|
+
}
|
|
10045
|
+
const routing = await resolveConnectedAccountRouting({
|
|
10046
|
+
projectId: resolvedProjectId,
|
|
10047
|
+
platforms: ["youtube"],
|
|
10048
|
+
requestedAccountIds: connected_account_id ? { youtube: connected_account_id } : void 0
|
|
10049
|
+
});
|
|
10050
|
+
const resolvedAccountId = routing.connectedAccountIds?.YouTube;
|
|
10051
|
+
if (routing.error || !resolvedAccountId) {
|
|
10052
|
+
return {
|
|
10053
|
+
content: [
|
|
10054
|
+
{
|
|
10055
|
+
type: "text",
|
|
10056
|
+
text: routing.error ?? "YouTube: exact connected-account routing could not be established."
|
|
10057
|
+
}
|
|
9411
10058
|
],
|
|
9412
10059
|
isError: true
|
|
9413
10060
|
};
|
|
@@ -9417,11 +10064,19 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
9417
10064
|
startDate: start_date,
|
|
9418
10065
|
endDate: end_date,
|
|
9419
10066
|
videoId: video_id,
|
|
9420
|
-
maxResults: max_results ?? 10
|
|
10067
|
+
maxResults: max_results ?? 10,
|
|
10068
|
+
projectId: resolvedProjectId,
|
|
10069
|
+
project_id: resolvedProjectId,
|
|
10070
|
+
connectedAccountId: resolvedAccountId
|
|
9421
10071
|
});
|
|
9422
10072
|
if (error) {
|
|
9423
10073
|
return {
|
|
9424
|
-
content: [
|
|
10074
|
+
content: [
|
|
10075
|
+
{
|
|
10076
|
+
type: "text",
|
|
10077
|
+
text: `YouTube Analytics error: ${error}`
|
|
10078
|
+
}
|
|
10079
|
+
],
|
|
9425
10080
|
isError: true
|
|
9426
10081
|
};
|
|
9427
10082
|
}
|
|
@@ -9434,7 +10089,12 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
9434
10089
|
{
|
|
9435
10090
|
type: "text",
|
|
9436
10091
|
text: JSON.stringify(
|
|
9437
|
-
asEnvelope7({
|
|
10092
|
+
asEnvelope7({
|
|
10093
|
+
action,
|
|
10094
|
+
startDate: start_date,
|
|
10095
|
+
endDate: end_date,
|
|
10096
|
+
analytics: a
|
|
10097
|
+
}),
|
|
9438
10098
|
null,
|
|
9439
10099
|
2
|
|
9440
10100
|
)
|
|
@@ -9461,7 +10121,10 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
9461
10121
|
if (days.length === 0) {
|
|
9462
10122
|
return {
|
|
9463
10123
|
content: [
|
|
9464
|
-
{
|
|
10124
|
+
{
|
|
10125
|
+
type: "text",
|
|
10126
|
+
text: "No daily analytics data found for this period."
|
|
10127
|
+
}
|
|
9465
10128
|
]
|
|
9466
10129
|
};
|
|
9467
10130
|
}
|
|
@@ -9484,7 +10147,10 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
9484
10147
|
]
|
|
9485
10148
|
};
|
|
9486
10149
|
}
|
|
9487
|
-
const lines = [
|
|
10150
|
+
const lines = [
|
|
10151
|
+
`YouTube Daily Analytics (${start_date} to ${end_date}):`,
|
|
10152
|
+
""
|
|
10153
|
+
];
|
|
9488
10154
|
for (const d of days) {
|
|
9489
10155
|
lines.push(
|
|
9490
10156
|
` ${d.date}: ${d.views.toLocaleString()} views, ${d.watchTimeMinutes.toLocaleString()} min watch, +${d.subscribersGained} subs, ${d.likes} likes, ${d.comments} comments`
|
|
@@ -9531,7 +10197,12 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
9531
10197
|
const videos = result.topVideos ?? [];
|
|
9532
10198
|
if (videos.length === 0) {
|
|
9533
10199
|
return {
|
|
9534
|
-
content: [
|
|
10200
|
+
content: [
|
|
10201
|
+
{
|
|
10202
|
+
type: "text",
|
|
10203
|
+
text: "No top videos found for this period."
|
|
10204
|
+
}
|
|
10205
|
+
]
|
|
9535
10206
|
};
|
|
9536
10207
|
}
|
|
9537
10208
|
if (format === "json") {
|
|
@@ -9553,7 +10224,10 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
9553
10224
|
]
|
|
9554
10225
|
};
|
|
9555
10226
|
}
|
|
9556
|
-
const lines = [
|
|
10227
|
+
const lines = [
|
|
10228
|
+
`Top ${videos.length} YouTube Videos (${start_date} to ${end_date}):`,
|
|
10229
|
+
""
|
|
10230
|
+
];
|
|
9557
10231
|
for (let i = 0; i < videos.length; i++) {
|
|
9558
10232
|
const v = videos[i];
|
|
9559
10233
|
lines.push(
|
|
@@ -9566,11 +10240,18 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
9566
10240
|
}
|
|
9567
10241
|
if (format === "json") {
|
|
9568
10242
|
return {
|
|
9569
|
-
content: [
|
|
10243
|
+
content: [
|
|
10244
|
+
{
|
|
10245
|
+
type: "text",
|
|
10246
|
+
text: JSON.stringify(asEnvelope7(result), null, 2)
|
|
10247
|
+
}
|
|
10248
|
+
]
|
|
9570
10249
|
};
|
|
9571
10250
|
}
|
|
9572
10251
|
return {
|
|
9573
|
-
content: [
|
|
10252
|
+
content: [
|
|
10253
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
10254
|
+
]
|
|
9574
10255
|
};
|
|
9575
10256
|
}
|
|
9576
10257
|
);
|
|
@@ -9579,6 +10260,8 @@ var init_youtube_analytics = __esm({
|
|
|
9579
10260
|
"src/tools/youtube-analytics.ts"() {
|
|
9580
10261
|
"use strict";
|
|
9581
10262
|
init_edge_function();
|
|
10263
|
+
init_supabase();
|
|
10264
|
+
init_connected_account_routing();
|
|
9582
10265
|
init_version();
|
|
9583
10266
|
}
|
|
9584
10267
|
});
|
|
@@ -9594,6 +10277,29 @@ function asEnvelope8(data) {
|
|
|
9594
10277
|
data
|
|
9595
10278
|
};
|
|
9596
10279
|
}
|
|
10280
|
+
async function exactYouTubeRoute(projectId, connectedAccountId) {
|
|
10281
|
+
const resolvedProjectId = projectId ?? await getDefaultProjectId() ?? void 0;
|
|
10282
|
+
if (!resolvedProjectId) {
|
|
10283
|
+
return {
|
|
10284
|
+
error: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
|
|
10285
|
+
};
|
|
10286
|
+
}
|
|
10287
|
+
const routing = await resolveConnectedAccountRouting({
|
|
10288
|
+
projectId: resolvedProjectId,
|
|
10289
|
+
platforms: ["youtube"],
|
|
10290
|
+
requestedAccountIds: connectedAccountId ? { youtube: connectedAccountId } : void 0
|
|
10291
|
+
});
|
|
10292
|
+
const resolvedAccountId = routing.connectedAccountIds?.YouTube;
|
|
10293
|
+
if (routing.error || !resolvedAccountId) {
|
|
10294
|
+
return {
|
|
10295
|
+
error: routing.error ?? "YouTube: exact connected-account routing could not be established."
|
|
10296
|
+
};
|
|
10297
|
+
}
|
|
10298
|
+
return {
|
|
10299
|
+
projectId: resolvedProjectId,
|
|
10300
|
+
connectedAccountId: resolvedAccountId
|
|
10301
|
+
};
|
|
10302
|
+
}
|
|
9597
10303
|
function registerCommentsTools(server) {
|
|
9598
10304
|
server.tool(
|
|
9599
10305
|
"list_comments",
|
|
@@ -9606,19 +10312,42 @@ function registerCommentsTools(server) {
|
|
|
9606
10312
|
page_token: z11.string().optional().describe(
|
|
9607
10313
|
"Pagination cursor from previous list_comments response nextPageToken field. Omit for first page of results."
|
|
9608
10314
|
),
|
|
10315
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
10316
|
+
"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."
|
|
10317
|
+
),
|
|
10318
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
9609
10319
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9610
10320
|
},
|
|
9611
|
-
async ({
|
|
10321
|
+
async ({
|
|
10322
|
+
video_id,
|
|
10323
|
+
max_results,
|
|
10324
|
+
page_token,
|
|
10325
|
+
connected_account_id,
|
|
10326
|
+
project_id,
|
|
10327
|
+
response_format
|
|
10328
|
+
}) => {
|
|
9612
10329
|
const format = response_format ?? "text";
|
|
10330
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
10331
|
+
if ("error" in route) {
|
|
10332
|
+
return {
|
|
10333
|
+
content: [{ type: "text", text: route.error }],
|
|
10334
|
+
isError: true
|
|
10335
|
+
};
|
|
10336
|
+
}
|
|
9613
10337
|
const { data, error } = await callEdgeFunction("youtube-comments", {
|
|
9614
10338
|
action: "list",
|
|
9615
10339
|
videoId: video_id,
|
|
9616
10340
|
maxResults: max_results ?? 50,
|
|
9617
|
-
pageToken: page_token
|
|
10341
|
+
pageToken: page_token,
|
|
10342
|
+
projectId: route.projectId,
|
|
10343
|
+
project_id: route.projectId,
|
|
10344
|
+
connectedAccountId: route.connectedAccountId
|
|
9618
10345
|
});
|
|
9619
10346
|
if (error) {
|
|
9620
10347
|
return {
|
|
9621
|
-
content: [
|
|
10348
|
+
content: [
|
|
10349
|
+
{ type: "text", text: `Error listing comments: ${error}` }
|
|
10350
|
+
],
|
|
9622
10351
|
isError: true
|
|
9623
10352
|
};
|
|
9624
10353
|
}
|
|
@@ -9630,7 +10359,10 @@ function registerCommentsTools(server) {
|
|
|
9630
10359
|
{
|
|
9631
10360
|
type: "text",
|
|
9632
10361
|
text: JSON.stringify(
|
|
9633
|
-
asEnvelope8({
|
|
10362
|
+
asEnvelope8({
|
|
10363
|
+
comments,
|
|
10364
|
+
nextPageToken: result.nextPageToken ?? null
|
|
10365
|
+
}),
|
|
9634
10366
|
null,
|
|
9635
10367
|
2
|
|
9636
10368
|
)
|
|
@@ -9667,12 +10399,31 @@ function registerCommentsTools(server) {
|
|
|
9667
10399
|
"reply_to_comment",
|
|
9668
10400
|
"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.",
|
|
9669
10401
|
{
|
|
9670
|
-
parent_id: z11.string().describe(
|
|
10402
|
+
parent_id: z11.string().describe(
|
|
10403
|
+
"The ID of the parent comment to reply to (from list_comments)."
|
|
10404
|
+
),
|
|
9671
10405
|
text: z11.string().min(1).describe("The reply text."),
|
|
10406
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
10407
|
+
"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."
|
|
10408
|
+
),
|
|
10409
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
9672
10410
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9673
10411
|
},
|
|
9674
|
-
async ({
|
|
10412
|
+
async ({
|
|
10413
|
+
parent_id,
|
|
10414
|
+
text,
|
|
10415
|
+
connected_account_id,
|
|
10416
|
+
project_id,
|
|
10417
|
+
response_format
|
|
10418
|
+
}) => {
|
|
9675
10419
|
const format = response_format ?? "text";
|
|
10420
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
10421
|
+
if ("error" in route) {
|
|
10422
|
+
return {
|
|
10423
|
+
content: [{ type: "text", text: route.error }],
|
|
10424
|
+
isError: true
|
|
10425
|
+
};
|
|
10426
|
+
}
|
|
9676
10427
|
const userId = await getDefaultUserId();
|
|
9677
10428
|
const rateLimit = checkRateLimit("posting", `reply_to_comment:${userId}`);
|
|
9678
10429
|
if (!rateLimit.allowed) {
|
|
@@ -9689,18 +10440,31 @@ function registerCommentsTools(server) {
|
|
|
9689
10440
|
const { data, error } = await callEdgeFunction("youtube-comments", {
|
|
9690
10441
|
action: "reply",
|
|
9691
10442
|
parentId: parent_id,
|
|
9692
|
-
text
|
|
10443
|
+
text,
|
|
10444
|
+
projectId: route.projectId,
|
|
10445
|
+
project_id: route.projectId,
|
|
10446
|
+
connectedAccountId: route.connectedAccountId
|
|
9693
10447
|
});
|
|
9694
10448
|
if (error) {
|
|
9695
10449
|
return {
|
|
9696
|
-
content: [
|
|
10450
|
+
content: [
|
|
10451
|
+
{
|
|
10452
|
+
type: "text",
|
|
10453
|
+
text: `Error replying to comment: ${error}`
|
|
10454
|
+
}
|
|
10455
|
+
],
|
|
9697
10456
|
isError: true
|
|
9698
10457
|
};
|
|
9699
10458
|
}
|
|
9700
10459
|
const result = data;
|
|
9701
10460
|
if (format === "json") {
|
|
9702
10461
|
return {
|
|
9703
|
-
content: [
|
|
10462
|
+
content: [
|
|
10463
|
+
{
|
|
10464
|
+
type: "text",
|
|
10465
|
+
text: JSON.stringify(asEnvelope8(result), null, 2)
|
|
10466
|
+
}
|
|
10467
|
+
]
|
|
9704
10468
|
};
|
|
9705
10469
|
}
|
|
9706
10470
|
return {
|
|
@@ -9721,10 +10485,27 @@ function registerCommentsTools(server) {
|
|
|
9721
10485
|
{
|
|
9722
10486
|
video_id: z11.string().describe("The YouTube video ID to comment on."),
|
|
9723
10487
|
text: z11.string().min(1).describe("The comment text."),
|
|
10488
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
10489
|
+
"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."
|
|
10490
|
+
),
|
|
10491
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
9724
10492
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9725
10493
|
},
|
|
9726
|
-
async ({
|
|
10494
|
+
async ({
|
|
10495
|
+
video_id,
|
|
10496
|
+
text,
|
|
10497
|
+
connected_account_id,
|
|
10498
|
+
project_id,
|
|
10499
|
+
response_format
|
|
10500
|
+
}) => {
|
|
9727
10501
|
const format = response_format ?? "text";
|
|
10502
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
10503
|
+
if ("error" in route) {
|
|
10504
|
+
return {
|
|
10505
|
+
content: [{ type: "text", text: route.error }],
|
|
10506
|
+
isError: true
|
|
10507
|
+
};
|
|
10508
|
+
}
|
|
9728
10509
|
const userId = await getDefaultUserId();
|
|
9729
10510
|
const rateLimit = checkRateLimit("posting", `post_comment:${userId}`);
|
|
9730
10511
|
if (!rateLimit.allowed) {
|
|
@@ -9741,18 +10522,28 @@ function registerCommentsTools(server) {
|
|
|
9741
10522
|
const { data, error } = await callEdgeFunction("youtube-comments", {
|
|
9742
10523
|
action: "post",
|
|
9743
10524
|
videoId: video_id,
|
|
9744
|
-
text
|
|
10525
|
+
text,
|
|
10526
|
+
projectId: route.projectId,
|
|
10527
|
+
project_id: route.projectId,
|
|
10528
|
+
connectedAccountId: route.connectedAccountId
|
|
9745
10529
|
});
|
|
9746
10530
|
if (error) {
|
|
9747
10531
|
return {
|
|
9748
|
-
content: [
|
|
10532
|
+
content: [
|
|
10533
|
+
{ type: "text", text: `Error posting comment: ${error}` }
|
|
10534
|
+
],
|
|
9749
10535
|
isError: true
|
|
9750
10536
|
};
|
|
9751
10537
|
}
|
|
9752
10538
|
const result = data;
|
|
9753
10539
|
if (format === "json") {
|
|
9754
10540
|
return {
|
|
9755
|
-
content: [
|
|
10541
|
+
content: [
|
|
10542
|
+
{
|
|
10543
|
+
type: "text",
|
|
10544
|
+
text: JSON.stringify(asEnvelope8(result), null, 2)
|
|
10545
|
+
}
|
|
10546
|
+
]
|
|
9756
10547
|
};
|
|
9757
10548
|
}
|
|
9758
10549
|
return {
|
|
@@ -9773,10 +10564,27 @@ function registerCommentsTools(server) {
|
|
|
9773
10564
|
{
|
|
9774
10565
|
comment_id: z11.string().describe("The comment ID to moderate."),
|
|
9775
10566
|
moderation_status: z11.enum(["published", "rejected"]).describe('"published" to approve, "rejected" to hide.'),
|
|
10567
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
10568
|
+
"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."
|
|
10569
|
+
),
|
|
10570
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
9776
10571
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9777
10572
|
},
|
|
9778
|
-
async ({
|
|
10573
|
+
async ({
|
|
10574
|
+
comment_id,
|
|
10575
|
+
moderation_status,
|
|
10576
|
+
connected_account_id,
|
|
10577
|
+
project_id,
|
|
10578
|
+
response_format
|
|
10579
|
+
}) => {
|
|
9779
10580
|
const format = response_format ?? "text";
|
|
10581
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
10582
|
+
if ("error" in route) {
|
|
10583
|
+
return {
|
|
10584
|
+
content: [{ type: "text", text: route.error }],
|
|
10585
|
+
isError: true
|
|
10586
|
+
};
|
|
10587
|
+
}
|
|
9780
10588
|
const userId = await getDefaultUserId();
|
|
9781
10589
|
const rateLimit = checkRateLimit("posting", `moderate_comment:${userId}`);
|
|
9782
10590
|
if (!rateLimit.allowed) {
|
|
@@ -9793,11 +10601,19 @@ function registerCommentsTools(server) {
|
|
|
9793
10601
|
const { error } = await callEdgeFunction("youtube-comments", {
|
|
9794
10602
|
action: "moderate",
|
|
9795
10603
|
commentId: comment_id,
|
|
9796
|
-
moderationStatus: moderation_status
|
|
10604
|
+
moderationStatus: moderation_status,
|
|
10605
|
+
projectId: route.projectId,
|
|
10606
|
+
project_id: route.projectId,
|
|
10607
|
+
connectedAccountId: route.connectedAccountId
|
|
9797
10608
|
});
|
|
9798
10609
|
if (error) {
|
|
9799
10610
|
return {
|
|
9800
|
-
content: [
|
|
10611
|
+
content: [
|
|
10612
|
+
{
|
|
10613
|
+
type: "text",
|
|
10614
|
+
text: `Error moderating comment: ${error}`
|
|
10615
|
+
}
|
|
10616
|
+
],
|
|
9801
10617
|
isError: true
|
|
9802
10618
|
};
|
|
9803
10619
|
}
|
|
@@ -9834,10 +10650,26 @@ function registerCommentsTools(server) {
|
|
|
9834
10650
|
"Delete a YouTube comment. Only works for comments owned by the authenticated channel.",
|
|
9835
10651
|
{
|
|
9836
10652
|
comment_id: z11.string().describe("The comment ID to delete."),
|
|
10653
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
10654
|
+
"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."
|
|
10655
|
+
),
|
|
10656
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
9837
10657
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9838
10658
|
},
|
|
9839
|
-
async ({
|
|
10659
|
+
async ({
|
|
10660
|
+
comment_id,
|
|
10661
|
+
connected_account_id,
|
|
10662
|
+
project_id,
|
|
10663
|
+
response_format
|
|
10664
|
+
}) => {
|
|
9840
10665
|
const format = response_format ?? "text";
|
|
10666
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
10667
|
+
if ("error" in route) {
|
|
10668
|
+
return {
|
|
10669
|
+
content: [{ type: "text", text: route.error }],
|
|
10670
|
+
isError: true
|
|
10671
|
+
};
|
|
10672
|
+
}
|
|
9841
10673
|
const userId = await getDefaultUserId();
|
|
9842
10674
|
const rateLimit = checkRateLimit("posting", `delete_comment:${userId}`);
|
|
9843
10675
|
if (!rateLimit.allowed) {
|
|
@@ -9853,11 +10685,16 @@ function registerCommentsTools(server) {
|
|
|
9853
10685
|
}
|
|
9854
10686
|
const { error } = await callEdgeFunction("youtube-comments", {
|
|
9855
10687
|
action: "delete",
|
|
9856
|
-
commentId: comment_id
|
|
10688
|
+
commentId: comment_id,
|
|
10689
|
+
projectId: route.projectId,
|
|
10690
|
+
project_id: route.projectId,
|
|
10691
|
+
connectedAccountId: route.connectedAccountId
|
|
9857
10692
|
});
|
|
9858
10693
|
if (error) {
|
|
9859
10694
|
return {
|
|
9860
|
-
content: [
|
|
10695
|
+
content: [
|
|
10696
|
+
{ type: "text", text: `Error deleting comment: ${error}` }
|
|
10697
|
+
],
|
|
9861
10698
|
isError: true
|
|
9862
10699
|
};
|
|
9863
10700
|
}
|
|
@@ -9866,24 +10703,38 @@ function registerCommentsTools(server) {
|
|
|
9866
10703
|
content: [
|
|
9867
10704
|
{
|
|
9868
10705
|
type: "text",
|
|
9869
|
-
text: JSON.stringify(
|
|
10706
|
+
text: JSON.stringify(
|
|
10707
|
+
asEnvelope8({ success: true, commentId: comment_id }),
|
|
10708
|
+
null,
|
|
10709
|
+
2
|
|
10710
|
+
)
|
|
9870
10711
|
}
|
|
9871
10712
|
]
|
|
9872
10713
|
};
|
|
9873
10714
|
}
|
|
9874
10715
|
return {
|
|
9875
|
-
content: [
|
|
10716
|
+
content: [
|
|
10717
|
+
{
|
|
10718
|
+
type: "text",
|
|
10719
|
+
text: `Comment ${comment_id} deleted successfully.`
|
|
10720
|
+
}
|
|
10721
|
+
]
|
|
9876
10722
|
};
|
|
9877
10723
|
}
|
|
9878
10724
|
);
|
|
9879
10725
|
}
|
|
10726
|
+
var PROJECT_ID_SCHEMA;
|
|
9880
10727
|
var init_comments = __esm({
|
|
9881
10728
|
"src/tools/comments.ts"() {
|
|
9882
10729
|
"use strict";
|
|
9883
10730
|
init_edge_function();
|
|
9884
10731
|
init_rate_limit();
|
|
9885
10732
|
init_supabase();
|
|
10733
|
+
init_connected_account_routing();
|
|
9886
10734
|
init_version();
|
|
10735
|
+
PROJECT_ID_SCHEMA = z11.string().uuid().optional().describe(
|
|
10736
|
+
"Exact brand/project ID. Defaults only when the authenticated user has one project."
|
|
10737
|
+
);
|
|
9887
10738
|
}
|
|
9888
10739
|
});
|
|
9889
10740
|
|
|
@@ -12594,6 +13445,14 @@ var init_plan_approvals = __esm({
|
|
|
12594
13445
|
|
|
12595
13446
|
// src/tools/discovery.ts
|
|
12596
13447
|
import { z as z23 } from "zod";
|
|
13448
|
+
function isHostedTransport() {
|
|
13449
|
+
return process.env.MCP_TRANSPORT === "http";
|
|
13450
|
+
}
|
|
13451
|
+
function isPubliclyDiscoverable(tool) {
|
|
13452
|
+
if (tool.internal || tool.hiddenFromPublicCount) return false;
|
|
13453
|
+
if (tool.localOnly && isHostedTransport()) return false;
|
|
13454
|
+
return true;
|
|
13455
|
+
}
|
|
12597
13456
|
function toolKnowledgeDocument(tool) {
|
|
12598
13457
|
const lines = [
|
|
12599
13458
|
`Tool: ${tool.name}`,
|
|
@@ -12604,7 +13463,8 @@ function toolKnowledgeDocument(tool) {
|
|
|
12604
13463
|
if (tool.task_intent) lines.push(`Task intent: ${tool.task_intent}`);
|
|
12605
13464
|
if (tool.use_when) lines.push(`Use when: ${tool.use_when}`);
|
|
12606
13465
|
if (tool.avoid_when) lines.push(`Avoid when: ${tool.avoid_when}`);
|
|
12607
|
-
if (tool.next_tools?.length)
|
|
13466
|
+
if (tool.next_tools?.length)
|
|
13467
|
+
lines.push(`Common next tools: ${tool.next_tools.join(", ")}`);
|
|
12608
13468
|
return {
|
|
12609
13469
|
id: `tool:${tool.name}`,
|
|
12610
13470
|
title: `MCP tool: ${tool.name}`,
|
|
@@ -12621,9 +13481,7 @@ function toolKnowledgeDocument(tool) {
|
|
|
12621
13481
|
function getKnowledgeDocuments() {
|
|
12622
13482
|
return [
|
|
12623
13483
|
...STATIC_KNOWLEDGE_DOCUMENTS,
|
|
12624
|
-
...TOOL_CATALOG.filter(
|
|
12625
|
-
toolKnowledgeDocument
|
|
12626
|
-
)
|
|
13484
|
+
...TOOL_CATALOG.filter(isPubliclyDiscoverable).map(toolKnowledgeDocument)
|
|
12627
13485
|
];
|
|
12628
13486
|
}
|
|
12629
13487
|
function tokenize(input) {
|
|
@@ -12677,7 +13535,9 @@ function registerDiscoveryTools(server) {
|
|
|
12677
13535
|
};
|
|
12678
13536
|
return {
|
|
12679
13537
|
structuredContent,
|
|
12680
|
-
content: [
|
|
13538
|
+
content: [
|
|
13539
|
+
{ type: "text", text: JSON.stringify(structuredContent) }
|
|
13540
|
+
]
|
|
12681
13541
|
};
|
|
12682
13542
|
}
|
|
12683
13543
|
);
|
|
@@ -12698,10 +13558,14 @@ function registerDiscoveryTools(server) {
|
|
|
12698
13558
|
}
|
|
12699
13559
|
},
|
|
12700
13560
|
async ({ id }) => {
|
|
12701
|
-
const doc = getKnowledgeDocuments().find(
|
|
13561
|
+
const doc = getKnowledgeDocuments().find(
|
|
13562
|
+
(candidate) => candidate.id === id
|
|
13563
|
+
);
|
|
12702
13564
|
if (!doc) {
|
|
12703
13565
|
return {
|
|
12704
|
-
content: [
|
|
13566
|
+
content: [
|
|
13567
|
+
{ type: "text", text: `Document not found: ${id}` }
|
|
13568
|
+
],
|
|
12705
13569
|
isError: true
|
|
12706
13570
|
};
|
|
12707
13571
|
}
|
|
@@ -12714,7 +13578,9 @@ function registerDiscoveryTools(server) {
|
|
|
12714
13578
|
};
|
|
12715
13579
|
return {
|
|
12716
13580
|
structuredContent,
|
|
12717
|
-
content: [
|
|
13581
|
+
content: [
|
|
13582
|
+
{ type: "text", text: JSON.stringify(structuredContent) }
|
|
13583
|
+
]
|
|
12718
13584
|
};
|
|
12719
13585
|
}
|
|
12720
13586
|
);
|
|
@@ -12723,7 +13589,9 @@ function registerDiscoveryTools(server) {
|
|
|
12723
13589
|
'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.',
|
|
12724
13590
|
{
|
|
12725
13591
|
query: z23.string().optional().describe("Search query to filter tools by name or description"),
|
|
12726
|
-
module: z23.string().optional().describe(
|
|
13592
|
+
module: z23.string().optional().describe(
|
|
13593
|
+
'Filter by module name (e.g. "planning", "content", "analytics")'
|
|
13594
|
+
),
|
|
12727
13595
|
scope: z23.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
|
|
12728
13596
|
detail: z23.enum(["name", "summary", "full"]).default("summary").describe(
|
|
12729
13597
|
'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
|
|
@@ -12740,14 +13608,18 @@ function registerDiscoveryTools(server) {
|
|
|
12740
13608
|
if (query) {
|
|
12741
13609
|
results = searchTools(query);
|
|
12742
13610
|
}
|
|
12743
|
-
results = results.filter(
|
|
13611
|
+
results = results.filter(isPubliclyDiscoverable);
|
|
12744
13612
|
if (module) {
|
|
12745
13613
|
const moduleTools = getToolsByModule(module);
|
|
12746
|
-
results = results.filter(
|
|
13614
|
+
results = results.filter(
|
|
13615
|
+
(t) => moduleTools.some((mt) => mt.name === t.name)
|
|
13616
|
+
);
|
|
12747
13617
|
}
|
|
12748
13618
|
if (scope) {
|
|
12749
13619
|
const scopeTools = getToolsByScope(scope);
|
|
12750
|
-
results = results.filter(
|
|
13620
|
+
results = results.filter(
|
|
13621
|
+
(t) => scopeTools.some((st) => st.name === t.name)
|
|
13622
|
+
);
|
|
12751
13623
|
}
|
|
12752
13624
|
if (available_only) {
|
|
12753
13625
|
results = results.filter(isAvailable);
|
|
@@ -12785,7 +13657,12 @@ function registerDiscoveryTools(server) {
|
|
|
12785
13657
|
text: JSON.stringify(
|
|
12786
13658
|
{
|
|
12787
13659
|
toolCount: results.length,
|
|
12788
|
-
...hasKnownScopes ? {
|
|
13660
|
+
...hasKnownScopes ? {
|
|
13661
|
+
scopes: {
|
|
13662
|
+
available: currentScopes,
|
|
13663
|
+
unavailable_matches: unavailableCount
|
|
13664
|
+
}
|
|
13665
|
+
} : {},
|
|
12789
13666
|
tools: output
|
|
12790
13667
|
},
|
|
12791
13668
|
null,
|
|
@@ -12876,7 +13753,10 @@ var init_discovery2 = __esm({
|
|
|
12876
13753
|
import { z as z24 } from "zod";
|
|
12877
13754
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
12878
13755
|
function asEnvelope20(data) {
|
|
12879
|
-
return {
|
|
13756
|
+
return {
|
|
13757
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
13758
|
+
data
|
|
13759
|
+
};
|
|
12880
13760
|
}
|
|
12881
13761
|
function registerPipelineTools(server) {
|
|
12882
13762
|
server.tool(
|
|
@@ -12899,7 +13779,10 @@ function registerPipelineTools(server) {
|
|
|
12899
13779
|
action: "pipeline-readiness",
|
|
12900
13780
|
platforms,
|
|
12901
13781
|
estimated_posts,
|
|
12902
|
-
...resolvedProjectId ? {
|
|
13782
|
+
...resolvedProjectId ? {
|
|
13783
|
+
projectId: resolvedProjectId,
|
|
13784
|
+
project_id: resolvedProjectId
|
|
13785
|
+
} : {}
|
|
12903
13786
|
},
|
|
12904
13787
|
{ timeoutMs: 15e3 }
|
|
12905
13788
|
);
|
|
@@ -12917,16 +13800,24 @@ function registerPipelineTools(server) {
|
|
|
12917
13800
|
const blockers = [];
|
|
12918
13801
|
const warnings = [];
|
|
12919
13802
|
if (!isUnlimited && credits < estimatedCost) {
|
|
12920
|
-
blockers.push(
|
|
13803
|
+
blockers.push(
|
|
13804
|
+
`Insufficient credits: ${credits} available, ~${estimatedCost} needed`
|
|
13805
|
+
);
|
|
12921
13806
|
}
|
|
12922
13807
|
if (missingPlatforms.length > 0) {
|
|
12923
|
-
blockers.push(
|
|
13808
|
+
blockers.push(
|
|
13809
|
+
`Missing connected accounts: ${missingPlatforms.join(", ")}`
|
|
13810
|
+
);
|
|
12924
13811
|
}
|
|
12925
13812
|
if (!hasBrand) {
|
|
12926
|
-
warnings.push(
|
|
13813
|
+
warnings.push(
|
|
13814
|
+
"No brand profile found. Content will use generic voice."
|
|
13815
|
+
);
|
|
12927
13816
|
}
|
|
12928
13817
|
if (pendingApprovals > 0) {
|
|
12929
|
-
warnings.push(
|
|
13818
|
+
warnings.push(
|
|
13819
|
+
`${pendingApprovals} pending approval(s) from previous runs.`
|
|
13820
|
+
);
|
|
12930
13821
|
}
|
|
12931
13822
|
if (!insightsFresh) {
|
|
12932
13823
|
warnings.push(
|
|
@@ -12941,7 +13832,10 @@ function registerPipelineTools(server) {
|
|
|
12941
13832
|
estimated_cost: estimatedCost,
|
|
12942
13833
|
sufficient: credits >= estimatedCost
|
|
12943
13834
|
},
|
|
12944
|
-
connected_accounts: {
|
|
13835
|
+
connected_accounts: {
|
|
13836
|
+
platforms: connectedPlatforms,
|
|
13837
|
+
missing: missingPlatforms
|
|
13838
|
+
},
|
|
12945
13839
|
brand_profile: { exists: hasBrand },
|
|
12946
13840
|
pending_approvals: { count: pendingApprovals },
|
|
12947
13841
|
insights_available: {
|
|
@@ -12955,11 +13849,18 @@ function registerPipelineTools(server) {
|
|
|
12955
13849
|
};
|
|
12956
13850
|
if (format === "json") {
|
|
12957
13851
|
return {
|
|
12958
|
-
content: [
|
|
13852
|
+
content: [
|
|
13853
|
+
{
|
|
13854
|
+
type: "text",
|
|
13855
|
+
text: JSON.stringify(asEnvelope20(result), null, 2)
|
|
13856
|
+
}
|
|
13857
|
+
]
|
|
12959
13858
|
};
|
|
12960
13859
|
}
|
|
12961
13860
|
const lines = [];
|
|
12962
|
-
lines.push(
|
|
13861
|
+
lines.push(
|
|
13862
|
+
`Pipeline Readiness: ${result.ready ? "READY" : "NOT READY"}`
|
|
13863
|
+
);
|
|
12963
13864
|
lines.push("=".repeat(40));
|
|
12964
13865
|
lines.push(
|
|
12965
13866
|
`Credits: ${credits} available, ~${estimatedCost} needed \u2014 ${credits >= estimatedCost ? "OK" : "BLOCKED"}`
|
|
@@ -12967,7 +13868,9 @@ function registerPipelineTools(server) {
|
|
|
12967
13868
|
lines.push(
|
|
12968
13869
|
`Accounts: ${connectedPlatforms.length} connected${missingPlatforms.length > 0 ? ` (missing: ${missingPlatforms.join(", ")})` : " \u2014 OK"}`
|
|
12969
13870
|
);
|
|
12970
|
-
lines.push(
|
|
13871
|
+
lines.push(
|
|
13872
|
+
`Brand: ${hasBrand ? "OK" : "Missing (will use generic voice)"}`
|
|
13873
|
+
);
|
|
12971
13874
|
lines.push(`Pending Approvals: ${pendingApprovals}`);
|
|
12972
13875
|
lines.push(
|
|
12973
13876
|
`Insights: ${insightsFresh ? "Fresh" : insightAge === null ? "None available" : `${insightAge} days old`}`
|
|
@@ -12986,7 +13889,12 @@ function registerPipelineTools(server) {
|
|
|
12986
13889
|
} catch (err) {
|
|
12987
13890
|
const message = sanitizeError(err);
|
|
12988
13891
|
return {
|
|
12989
|
-
content: [
|
|
13892
|
+
content: [
|
|
13893
|
+
{
|
|
13894
|
+
type: "text",
|
|
13895
|
+
text: `Readiness check failed: ${message}`
|
|
13896
|
+
}
|
|
13897
|
+
],
|
|
12990
13898
|
isError: true
|
|
12991
13899
|
};
|
|
12992
13900
|
}
|
|
@@ -13000,6 +13908,9 @@ function registerPipelineTools(server) {
|
|
|
13000
13908
|
topic: z24.string().optional().describe("Content topic (required if no source_url)"),
|
|
13001
13909
|
source_url: z24.string().optional().describe("URL to extract content from"),
|
|
13002
13910
|
platforms: z24.array(PLATFORM_ENUM2).min(1).describe("Target platforms"),
|
|
13911
|
+
account_ids: z24.record(z24.string(), z24.string().uuid()).optional().describe(
|
|
13912
|
+
"Exact connected-account ID per target platform. Required when a project has multiple accounts on one platform."
|
|
13913
|
+
),
|
|
13003
13914
|
days: z24.number().min(1).max(7).default(5).describe("Days to plan"),
|
|
13004
13915
|
posts_per_day: z24.number().min(1).max(3).default(1).describe("Posts per platform per day"),
|
|
13005
13916
|
approval_mode: z24.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe(
|
|
@@ -13021,6 +13932,7 @@ function registerPipelineTools(server) {
|
|
|
13021
13932
|
topic,
|
|
13022
13933
|
source_url,
|
|
13023
13934
|
platforms,
|
|
13935
|
+
account_ids,
|
|
13024
13936
|
days,
|
|
13025
13937
|
posts_per_day,
|
|
13026
13938
|
approval_mode,
|
|
@@ -13038,7 +13950,12 @@ function registerPipelineTools(server) {
|
|
|
13038
13950
|
let creditsUsed = 0;
|
|
13039
13951
|
if (!topic && !source_url) {
|
|
13040
13952
|
return {
|
|
13041
|
-
content: [
|
|
13953
|
+
content: [
|
|
13954
|
+
{
|
|
13955
|
+
type: "text",
|
|
13956
|
+
text: "Either topic or source_url is required."
|
|
13957
|
+
}
|
|
13958
|
+
],
|
|
13042
13959
|
isError: true
|
|
13043
13960
|
};
|
|
13044
13961
|
}
|
|
@@ -13068,6 +13985,35 @@ function registerPipelineTools(server) {
|
|
|
13068
13985
|
}
|
|
13069
13986
|
try {
|
|
13070
13987
|
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
13988
|
+
if (schedulingRequested && !resolvedProjectId) {
|
|
13989
|
+
return {
|
|
13990
|
+
content: [
|
|
13991
|
+
{
|
|
13992
|
+
type: "text",
|
|
13993
|
+
text: "A project_id is required to schedule pipeline output. Configure an explicit project or use an API key scoped to exactly one project."
|
|
13994
|
+
}
|
|
13995
|
+
],
|
|
13996
|
+
isError: true
|
|
13997
|
+
};
|
|
13998
|
+
}
|
|
13999
|
+
if (schedulingRequested) {
|
|
14000
|
+
const preflightRouting = await resolveConnectedAccountRouting({
|
|
14001
|
+
projectId: resolvedProjectId,
|
|
14002
|
+
platforms,
|
|
14003
|
+
requestedAccountIds: account_ids
|
|
14004
|
+
});
|
|
14005
|
+
if (preflightRouting.error || !preflightRouting.connectedAccountIds) {
|
|
14006
|
+
return {
|
|
14007
|
+
content: [
|
|
14008
|
+
{
|
|
14009
|
+
type: "text",
|
|
14010
|
+
text: `Cannot run publishing pipeline \u2014 ${preflightRouting.error ?? "exact connected-account routing could not be established."} (checked before any credits were spent.)`
|
|
14011
|
+
}
|
|
14012
|
+
],
|
|
14013
|
+
isError: true
|
|
14014
|
+
};
|
|
14015
|
+
}
|
|
14016
|
+
}
|
|
13071
14017
|
const estimatedCost = BASE_PLAN_CREDITS + (source_url ? SOURCE_EXTRACTION_CREDITS : 0);
|
|
13072
14018
|
const { data: budgetData } = await callEdgeFunction(
|
|
13073
14019
|
"mcp-data",
|
|
@@ -13121,7 +14067,13 @@ function registerPipelineTools(server) {
|
|
|
13121
14067
|
"social-neuron-ai",
|
|
13122
14068
|
{
|
|
13123
14069
|
type: "generation",
|
|
13124
|
-
prompt: buildPlanPrompt(
|
|
14070
|
+
prompt: buildPlanPrompt(
|
|
14071
|
+
resolvedTopic,
|
|
14072
|
+
platforms,
|
|
14073
|
+
days,
|
|
14074
|
+
posts_per_day,
|
|
14075
|
+
source_url
|
|
14076
|
+
),
|
|
13125
14077
|
model: "gemini-2.5-flash",
|
|
13126
14078
|
responseFormat: "json",
|
|
13127
14079
|
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
@@ -13129,7 +14081,10 @@ function registerPipelineTools(server) {
|
|
|
13129
14081
|
{ timeoutMs: 6e4 }
|
|
13130
14082
|
);
|
|
13131
14083
|
if (planError || !planData) {
|
|
13132
|
-
errors.push({
|
|
14084
|
+
errors.push({
|
|
14085
|
+
stage: "planning",
|
|
14086
|
+
message: planError ?? "No AI response"
|
|
14087
|
+
});
|
|
13133
14088
|
await callEdgeFunction(
|
|
13134
14089
|
"mcp-data",
|
|
13135
14090
|
{
|
|
@@ -13146,7 +14101,10 @@ function registerPipelineTools(server) {
|
|
|
13146
14101
|
);
|
|
13147
14102
|
return {
|
|
13148
14103
|
content: [
|
|
13149
|
-
{
|
|
14104
|
+
{
|
|
14105
|
+
type: "text",
|
|
14106
|
+
text: `Planning failed: ${planError ?? "No AI response"}`
|
|
14107
|
+
}
|
|
13150
14108
|
],
|
|
13151
14109
|
isError: true
|
|
13152
14110
|
};
|
|
@@ -13176,20 +14134,22 @@ function registerPipelineTools(server) {
|
|
|
13176
14134
|
const postsArray = extractJsonArray(rawText);
|
|
13177
14135
|
const requestedPlatformSet = new Set(platforms);
|
|
13178
14136
|
const maxPosts = platforms.length * days * posts_per_day;
|
|
13179
|
-
const parsedPosts = (postsArray ?? []).map(
|
|
13180
|
-
|
|
13181
|
-
|
|
13182
|
-
|
|
13183
|
-
|
|
13184
|
-
|
|
13185
|
-
|
|
13186
|
-
|
|
13187
|
-
|
|
13188
|
-
|
|
13189
|
-
|
|
13190
|
-
|
|
13191
|
-
|
|
13192
|
-
|
|
14137
|
+
const parsedPosts = (postsArray ?? []).map(
|
|
14138
|
+
(p) => ({
|
|
14139
|
+
id: String(p.id ?? randomUUID3().slice(0, 8)),
|
|
14140
|
+
day: Number(p.day ?? 1),
|
|
14141
|
+
date: String(p.date ?? ""),
|
|
14142
|
+
platform: String(p.platform ?? ""),
|
|
14143
|
+
content_type: p.content_type ?? "caption",
|
|
14144
|
+
caption: String(p.caption ?? ""),
|
|
14145
|
+
title: p.title ? String(p.title) : void 0,
|
|
14146
|
+
hashtags: Array.isArray(p.hashtags) ? p.hashtags.map(String) : void 0,
|
|
14147
|
+
hook: String(p.hook ?? ""),
|
|
14148
|
+
angle: String(p.angle ?? ""),
|
|
14149
|
+
visual_direction: p.visual_direction ? String(p.visual_direction) : void 0,
|
|
14150
|
+
media_type: p.media_type ? String(p.media_type) : void 0
|
|
14151
|
+
})
|
|
14152
|
+
);
|
|
13193
14153
|
const platformFilteredPosts = parsedPosts.filter(
|
|
13194
14154
|
(post) => requestedPlatformSet.has(post.platform)
|
|
13195
14155
|
);
|
|
@@ -13272,7 +14232,10 @@ function registerPipelineTools(server) {
|
|
|
13272
14232
|
estimated_credits: estimatedCost,
|
|
13273
14233
|
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
13274
14234
|
},
|
|
13275
|
-
...resolvedProjectId ? {
|
|
14235
|
+
...resolvedProjectId ? {
|
|
14236
|
+
projectId: resolvedProjectId,
|
|
14237
|
+
project_id: resolvedProjectId
|
|
14238
|
+
} : {}
|
|
13276
14239
|
},
|
|
13277
14240
|
{ timeoutMs: 1e4 }
|
|
13278
14241
|
);
|
|
@@ -13325,6 +14288,22 @@ function registerPipelineTools(server) {
|
|
|
13325
14288
|
}
|
|
13326
14289
|
}
|
|
13327
14290
|
let postsScheduled = 0;
|
|
14291
|
+
const pipelineRouting = schedulingRequested && postsApproved > 0 ? await resolveConnectedAccountRouting({
|
|
14292
|
+
projectId: resolvedProjectId,
|
|
14293
|
+
platforms,
|
|
14294
|
+
requestedAccountIds: account_ids
|
|
14295
|
+
}) : void 0;
|
|
14296
|
+
if (pipelineRouting?.error || schedulingRequested && postsApproved > 0 && !pipelineRouting?.connectedAccountIds) {
|
|
14297
|
+
return {
|
|
14298
|
+
content: [
|
|
14299
|
+
{
|
|
14300
|
+
type: "text",
|
|
14301
|
+
text: `Cannot run publishing pipeline \u2014 ${pipelineRouting?.error ?? "exact connected-account routing could not be established."}`
|
|
14302
|
+
}
|
|
14303
|
+
],
|
|
14304
|
+
isError: true
|
|
14305
|
+
};
|
|
14306
|
+
}
|
|
13328
14307
|
if (!dry_run && !skipSet.has("schedule") && postsApproved > 0) {
|
|
13329
14308
|
const approvedPosts = posts.filter((p) => p.status === "approved");
|
|
13330
14309
|
const scheduleBase = /* @__PURE__ */ new Date();
|
|
@@ -13332,10 +14311,27 @@ function registerPipelineTools(server) {
|
|
|
13332
14311
|
const scheduleBaseMs = scheduleBase.getTime();
|
|
13333
14312
|
for (const post of approvedPosts) {
|
|
13334
14313
|
if (creditsUsed >= creditLimit) {
|
|
13335
|
-
errors.push({
|
|
14314
|
+
errors.push({
|
|
14315
|
+
stage: "schedule",
|
|
14316
|
+
message: "Credit limit reached"
|
|
14317
|
+
});
|
|
13336
14318
|
break;
|
|
13337
14319
|
}
|
|
13338
|
-
const scheduledAt = post.schedule_at ?? new Date(
|
|
14320
|
+
const scheduledAt = post.schedule_at ?? new Date(
|
|
14321
|
+
scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5
|
|
14322
|
+
).toISOString();
|
|
14323
|
+
const route = Object.entries(
|
|
14324
|
+
pipelineRouting.connectedAccountIds
|
|
14325
|
+
).find(
|
|
14326
|
+
([platform3]) => platform3.toLowerCase() === post.platform.toLowerCase()
|
|
14327
|
+
);
|
|
14328
|
+
if (!route) {
|
|
14329
|
+
errors.push({
|
|
14330
|
+
stage: "schedule",
|
|
14331
|
+
message: `No verified connected account route for ${post.platform}`
|
|
14332
|
+
});
|
|
14333
|
+
continue;
|
|
14334
|
+
}
|
|
13339
14335
|
try {
|
|
13340
14336
|
const { error: schedError } = await callEdgeFunction(
|
|
13341
14337
|
"schedule-post",
|
|
@@ -13348,7 +14344,11 @@ function registerPipelineTools(server) {
|
|
|
13348
14344
|
scheduledAt,
|
|
13349
14345
|
planId,
|
|
13350
14346
|
idempotencyKey: `pipeline-${planId}-${post.id}`,
|
|
13351
|
-
...resolvedProjectId ? {
|
|
14347
|
+
...resolvedProjectId ? {
|
|
14348
|
+
projectId: resolvedProjectId,
|
|
14349
|
+
project_id: resolvedProjectId
|
|
14350
|
+
} : {},
|
|
14351
|
+
connectedAccountIds: { [route[0]]: route[1] }
|
|
13352
14352
|
},
|
|
13353
14353
|
{ timeoutMs: 15e3 }
|
|
13354
14354
|
);
|
|
@@ -13414,12 +14414,17 @@ function registerPipelineTools(server) {
|
|
|
13414
14414
|
if (response_format === "json") {
|
|
13415
14415
|
return {
|
|
13416
14416
|
content: [
|
|
13417
|
-
{
|
|
14417
|
+
{
|
|
14418
|
+
type: "text",
|
|
14419
|
+
text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
|
|
14420
|
+
}
|
|
13418
14421
|
]
|
|
13419
14422
|
};
|
|
13420
14423
|
}
|
|
13421
14424
|
const lines = [];
|
|
13422
|
-
lines.push(
|
|
14425
|
+
lines.push(
|
|
14426
|
+
`Pipeline ${pipelineId.slice(0, 8)}... ${finalStatus.toUpperCase()}`
|
|
14427
|
+
);
|
|
13423
14428
|
lines.push("=".repeat(40));
|
|
13424
14429
|
lines.push(`Posts generated: ${posts.length}`);
|
|
13425
14430
|
lines.push(`Posts approved: ${postsApproved}`);
|
|
@@ -13458,7 +14463,9 @@ function registerPipelineTools(server) {
|
|
|
13458
14463
|
} catch {
|
|
13459
14464
|
}
|
|
13460
14465
|
return {
|
|
13461
|
-
content: [
|
|
14466
|
+
content: [
|
|
14467
|
+
{ type: "text", text: `Pipeline failed: ${message}` }
|
|
14468
|
+
],
|
|
13462
14469
|
isError: true
|
|
13463
14470
|
};
|
|
13464
14471
|
}
|
|
@@ -13495,7 +14502,12 @@ function registerPipelineTools(server) {
|
|
|
13495
14502
|
}
|
|
13496
14503
|
if (format === "json") {
|
|
13497
14504
|
return {
|
|
13498
|
-
content: [
|
|
14505
|
+
content: [
|
|
14506
|
+
{
|
|
14507
|
+
type: "text",
|
|
14508
|
+
text: JSON.stringify(asEnvelope20(data), null, 2)
|
|
14509
|
+
}
|
|
14510
|
+
]
|
|
13499
14511
|
};
|
|
13500
14512
|
}
|
|
13501
14513
|
const lines = [];
|
|
@@ -13535,10 +14547,19 @@ function registerPipelineTools(server) {
|
|
|
13535
14547
|
},
|
|
13536
14548
|
async ({ plan_id, quality_threshold, response_format }) => {
|
|
13537
14549
|
try {
|
|
13538
|
-
const { data: loadResult, error: loadError } = await callEdgeFunction(
|
|
14550
|
+
const { data: loadResult, error: loadError } = await callEdgeFunction(
|
|
14551
|
+
"mcp-data",
|
|
14552
|
+
{ action: "auto-approve-plan", plan_id },
|
|
14553
|
+
{ timeoutMs: 1e4 }
|
|
14554
|
+
);
|
|
13539
14555
|
if (loadError) {
|
|
13540
14556
|
return {
|
|
13541
|
-
content: [
|
|
14557
|
+
content: [
|
|
14558
|
+
{
|
|
14559
|
+
type: "text",
|
|
14560
|
+
text: `Failed to load plan: ${loadError}`
|
|
14561
|
+
}
|
|
14562
|
+
],
|
|
13542
14563
|
isError: true
|
|
13543
14564
|
};
|
|
13544
14565
|
}
|
|
@@ -13546,7 +14567,10 @@ function registerPipelineTools(server) {
|
|
|
13546
14567
|
if (!stored?.plan_payload) {
|
|
13547
14568
|
return {
|
|
13548
14569
|
content: [
|
|
13549
|
-
{
|
|
14570
|
+
{
|
|
14571
|
+
type: "text",
|
|
14572
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
14573
|
+
}
|
|
13550
14574
|
],
|
|
13551
14575
|
isError: true
|
|
13552
14576
|
};
|
|
@@ -13573,7 +14597,11 @@ function registerPipelineTools(server) {
|
|
|
13573
14597
|
blockers: []
|
|
13574
14598
|
};
|
|
13575
14599
|
autoApproved++;
|
|
13576
|
-
details.push({
|
|
14600
|
+
details.push({
|
|
14601
|
+
post_id: post.id,
|
|
14602
|
+
action: "approved",
|
|
14603
|
+
score: quality.total
|
|
14604
|
+
});
|
|
13577
14605
|
} else if (quality.total >= quality_threshold - 5) {
|
|
13578
14606
|
post.status = "needs_edit";
|
|
13579
14607
|
post.quality = {
|
|
@@ -13583,7 +14611,11 @@ function registerPipelineTools(server) {
|
|
|
13583
14611
|
blockers: quality.blockers
|
|
13584
14612
|
};
|
|
13585
14613
|
flagged++;
|
|
13586
|
-
details.push({
|
|
14614
|
+
details.push({
|
|
14615
|
+
post_id: post.id,
|
|
14616
|
+
action: "flagged",
|
|
14617
|
+
score: quality.total
|
|
14618
|
+
});
|
|
13587
14619
|
} else {
|
|
13588
14620
|
post.status = "rejected";
|
|
13589
14621
|
post.quality = {
|
|
@@ -13593,7 +14625,11 @@ function registerPipelineTools(server) {
|
|
|
13593
14625
|
blockers: quality.blockers
|
|
13594
14626
|
};
|
|
13595
14627
|
rejected++;
|
|
13596
|
-
details.push({
|
|
14628
|
+
details.push({
|
|
14629
|
+
post_id: post.id,
|
|
14630
|
+
action: "rejected",
|
|
14631
|
+
score: quality.total
|
|
14632
|
+
});
|
|
13597
14633
|
}
|
|
13598
14634
|
}
|
|
13599
14635
|
const newStatus = flagged === 0 && rejected === 0 ? "approved" : "in_review";
|
|
@@ -13629,7 +14665,10 @@ function registerPipelineTools(server) {
|
|
|
13629
14665
|
if (response_format === "json") {
|
|
13630
14666
|
return {
|
|
13631
14667
|
content: [
|
|
13632
|
-
{
|
|
14668
|
+
{
|
|
14669
|
+
type: "text",
|
|
14670
|
+
text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
|
|
14671
|
+
}
|
|
13633
14672
|
]
|
|
13634
14673
|
};
|
|
13635
14674
|
}
|
|
@@ -13650,7 +14689,9 @@ function registerPipelineTools(server) {
|
|
|
13650
14689
|
} catch (err) {
|
|
13651
14690
|
const message = sanitizeError(err);
|
|
13652
14691
|
return {
|
|
13653
|
-
content: [
|
|
14692
|
+
content: [
|
|
14693
|
+
{ type: "text", text: `Auto-approve failed: ${message}` }
|
|
14694
|
+
],
|
|
13654
14695
|
isError: true
|
|
13655
14696
|
};
|
|
13656
14697
|
}
|
|
@@ -13684,6 +14725,7 @@ var init_pipeline = __esm({
|
|
|
13684
14725
|
init_quality();
|
|
13685
14726
|
init_version();
|
|
13686
14727
|
init_parse_utils();
|
|
14728
|
+
init_connected_account_routing();
|
|
13687
14729
|
PLATFORM_ENUM2 = z24.enum([
|
|
13688
14730
|
"youtube",
|
|
13689
14731
|
"tiktok",
|
|
@@ -15211,25 +16253,39 @@ function registerCarouselTools(server) {
|
|
|
15211
16253
|
]).optional().describe("Carousel template. Default: hormozi-authority."),
|
|
15212
16254
|
slide_count: z28.number().min(3).max(10).optional().describe("Number of slides (3-10). Default: 7."),
|
|
15213
16255
|
aspect_ratio: z28.enum(["1:1", "4:5", "9:16"]).optional().describe("Aspect ratio for both carousel and images. Default: 1:1."),
|
|
15214
|
-
style: z28.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
|
|
16256
|
+
style: z28.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
|
|
16257
|
+
"Visual style. Default: hormozi for hormozi-authority template."
|
|
16258
|
+
),
|
|
15215
16259
|
image_style_suffix: z28.string().max(500).optional().describe(
|
|
15216
16260
|
'Style suffix appended to every image prompt for visual consistency across slides. Example: "dark moody lighting, cinematic, 35mm film grain".'
|
|
15217
16261
|
),
|
|
15218
16262
|
hook: z28.string().max(300).optional().describe(
|
|
15219
16263
|
"Explicit hook/opener for slide 1. Overrides any hook derived from topic. Keep under 15 words for strongest scroll-stop."
|
|
15220
16264
|
),
|
|
15221
|
-
hook_family: z28.enum([
|
|
16265
|
+
hook_family: z28.enum([
|
|
16266
|
+
"curiosity",
|
|
16267
|
+
"authority",
|
|
16268
|
+
"pain_point",
|
|
16269
|
+
"contrarian",
|
|
16270
|
+
"data_driven"
|
|
16271
|
+
]).optional().describe(
|
|
15222
16272
|
"Hook family tag. Recorded on the carousel so downstream analytics can attribute engagement to hook pattern."
|
|
15223
16273
|
),
|
|
15224
|
-
cta_text: z28.string().max(200).optional().describe(
|
|
15225
|
-
|
|
16274
|
+
cta_text: z28.string().max(200).optional().describe(
|
|
16275
|
+
"Explicit CTA copy for the final slide. If omitted, derived from topic."
|
|
16276
|
+
),
|
|
16277
|
+
cta_url: z28.string().url().optional().describe(
|
|
16278
|
+
"URL promoted on the CTA slide. Defaults to the project landing page."
|
|
16279
|
+
),
|
|
15226
16280
|
tone: z28.string().max(200).optional().describe(
|
|
15227
16281
|
'Voice/tone override. Composes with the brand profile voice when present. Example: "educational, confident, not arrogant".'
|
|
15228
16282
|
),
|
|
15229
16283
|
constraints: z28.string().max(500).optional().describe(
|
|
15230
16284
|
'Content constraints applied at generation time. Example: "No fabricated statistics. Sentence case only. No ALL CAPS."'
|
|
15231
16285
|
),
|
|
15232
|
-
platform: z28.enum(["linkedin", "instagram", "tiktok", "x"]).optional().describe(
|
|
16286
|
+
platform: z28.enum(["linkedin", "instagram", "tiktok", "x"]).optional().describe(
|
|
16287
|
+
"Target platform. Affects tone conventions and slide-count guardrails."
|
|
16288
|
+
),
|
|
15233
16289
|
brand_id: z28.string().optional().describe(
|
|
15234
16290
|
"Brand/project ID to pull visual context from (colors, logo, mood). Falls back to project_id, then default project."
|
|
15235
16291
|
),
|
|
@@ -15261,7 +16317,9 @@ function registerCarouselTools(server) {
|
|
|
15261
16317
|
const slideCount = slide_count ?? 7;
|
|
15262
16318
|
const ratio = aspect_ratio ?? "1:1";
|
|
15263
16319
|
let brandContext = null;
|
|
15264
|
-
const
|
|
16320
|
+
const defaultProjectId = await getDefaultProjectId();
|
|
16321
|
+
const brandProjectId = brand_id || project_id || defaultProjectId;
|
|
16322
|
+
const resolvedProjectId = project_id || defaultProjectId;
|
|
15265
16323
|
if (brandProjectId) {
|
|
15266
16324
|
brandContext = await fetchBrandVisualContext(brandProjectId);
|
|
15267
16325
|
}
|
|
@@ -15270,10 +16328,7 @@ function registerCarouselTools(server) {
|
|
|
15270
16328
|
const totalEstimatedCost = carouselTextCost + slideCount * perImageCost;
|
|
15271
16329
|
const budgetCheck = checkCreditBudget(totalEstimatedCost);
|
|
15272
16330
|
if (!budgetCheck.ok) {
|
|
15273
|
-
return
|
|
15274
|
-
content: [{ type: "text", text: budgetCheck.message }],
|
|
15275
|
-
isError: true
|
|
15276
|
-
};
|
|
16331
|
+
return budgetCheck.error;
|
|
15277
16332
|
}
|
|
15278
16333
|
const assetBudget = checkAssetBudget(slideCount);
|
|
15279
16334
|
if (!assetBudget.ok) {
|
|
@@ -15283,7 +16338,10 @@ function registerCarouselTools(server) {
|
|
|
15283
16338
|
};
|
|
15284
16339
|
}
|
|
15285
16340
|
const userId = await getDefaultUserId();
|
|
15286
|
-
const rateLimit = checkRateLimit(
|
|
16341
|
+
const rateLimit = checkRateLimit(
|
|
16342
|
+
"generation",
|
|
16343
|
+
`create_carousel:${userId}`
|
|
16344
|
+
);
|
|
15287
16345
|
if (!rateLimit.allowed) {
|
|
15288
16346
|
return {
|
|
15289
16347
|
content: [
|
|
@@ -15303,7 +16361,7 @@ function registerCarouselTools(server) {
|
|
|
15303
16361
|
slideCount,
|
|
15304
16362
|
aspectRatio: ratio,
|
|
15305
16363
|
style: resolvedStyle,
|
|
15306
|
-
projectId:
|
|
16364
|
+
projectId: resolvedProjectId,
|
|
15307
16365
|
hook,
|
|
15308
16366
|
hookFamily: hook_family,
|
|
15309
16367
|
ctaText: cta_text,
|
|
@@ -15317,7 +16375,12 @@ function registerCarouselTools(server) {
|
|
|
15317
16375
|
if (carouselError || !carouselData?.carousel) {
|
|
15318
16376
|
const errMsg = carouselError ?? "No carousel data returned";
|
|
15319
16377
|
return {
|
|
15320
|
-
content: [
|
|
16378
|
+
content: [
|
|
16379
|
+
{
|
|
16380
|
+
type: "text",
|
|
16381
|
+
text: `Carousel text generation failed: ${errMsg}`
|
|
16382
|
+
}
|
|
16383
|
+
],
|
|
15321
16384
|
isError: true
|
|
15322
16385
|
};
|
|
15323
16386
|
}
|
|
@@ -15346,7 +16409,8 @@ function registerCarouselTools(server) {
|
|
|
15346
16409
|
{
|
|
15347
16410
|
prompt: imagePrompt,
|
|
15348
16411
|
model: image_model,
|
|
15349
|
-
aspectRatio: ratio
|
|
16412
|
+
aspectRatio: ratio,
|
|
16413
|
+
...resolvedProjectId && { projectId: resolvedProjectId }
|
|
15350
16414
|
},
|
|
15351
16415
|
{ timeoutMs: 3e4 }
|
|
15352
16416
|
);
|
|
@@ -15403,14 +16467,19 @@ function registerCarouselTools(server) {
|
|
|
15403
16467
|
type: "text",
|
|
15404
16468
|
text: JSON.stringify(
|
|
15405
16469
|
{
|
|
15406
|
-
_meta: {
|
|
16470
|
+
_meta: {
|
|
16471
|
+
version: MCP_VERSION,
|
|
16472
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
16473
|
+
},
|
|
15407
16474
|
data: {
|
|
15408
16475
|
carouselId: carousel.id,
|
|
15409
16476
|
templateId,
|
|
15410
16477
|
style: resolvedStyle,
|
|
15411
16478
|
slideCount: carousel.slides.length,
|
|
15412
16479
|
slides: carousel.slides.map((s) => {
|
|
15413
|
-
const job = imageJobs.find(
|
|
16480
|
+
const job = imageJobs.find(
|
|
16481
|
+
(j) => j.slideNumber === s.slideNumber
|
|
16482
|
+
);
|
|
15414
16483
|
return {
|
|
15415
16484
|
...s,
|
|
15416
16485
|
imageJobId: job?.jobId ?? null,
|
|
@@ -15437,9 +16506,17 @@ function registerCarouselTools(server) {
|
|
|
15437
16506
|
credits: {
|
|
15438
16507
|
textGeneration: textCredits,
|
|
15439
16508
|
imagesEstimated: successfulJobs.length * perImageCost,
|
|
15440
|
-
imagesCharged: imageJobs.reduce(
|
|
15441
|
-
|
|
15442
|
-
|
|
16509
|
+
imagesCharged: imageJobs.reduce(
|
|
16510
|
+
(sum, job) => sum + (job.creditsCharged ?? 0),
|
|
16511
|
+
0
|
|
16512
|
+
),
|
|
16513
|
+
imagesRefunded: imageJobs.reduce(
|
|
16514
|
+
(sum, job) => sum + (job.creditsRefunded ?? 0),
|
|
16515
|
+
0
|
|
16516
|
+
),
|
|
16517
|
+
billingUnknownSlides: imageJobs.filter(
|
|
16518
|
+
(job) => job.billingStatus === "unknown"
|
|
16519
|
+
).length,
|
|
15443
16520
|
totalEstimated: textCredits + successfulJobs.length * perImageCost
|
|
15444
16521
|
}
|
|
15445
16522
|
}
|
|
@@ -15467,7 +16544,9 @@ function registerCarouselTools(server) {
|
|
|
15467
16544
|
for (const slide of carousel.slides) {
|
|
15468
16545
|
const job = imageJobs.find((j) => j.slideNumber === slide.slideNumber);
|
|
15469
16546
|
const status = job?.jobId ? `image: ${job.jobId}` : `image FAILED: ${job?.error}`;
|
|
15470
|
-
lines.push(
|
|
16547
|
+
lines.push(
|
|
16548
|
+
` ${slide.slideNumber}. ${slide.headline || "(no headline)"} [${status}]`
|
|
16549
|
+
);
|
|
15471
16550
|
}
|
|
15472
16551
|
if (failedJobs.length > 0) {
|
|
15473
16552
|
lines.push("");
|
|
@@ -15478,7 +16557,9 @@ function registerCarouselTools(server) {
|
|
|
15478
16557
|
const jobIdList = successfulJobs.map((j) => j.jobId).join(", ");
|
|
15479
16558
|
lines.push("");
|
|
15480
16559
|
lines.push("Next steps:");
|
|
15481
|
-
lines.push(
|
|
16560
|
+
lines.push(
|
|
16561
|
+
` 1. Poll each job: check_status with job_id for each of: ${jobIdList}`
|
|
16562
|
+
);
|
|
15482
16563
|
lines.push(
|
|
15483
16564
|
" 2. When all complete: schedule_post with job_ids=[...] and media_type=CAROUSEL_ALBUM"
|
|
15484
16565
|
);
|
|
@@ -16348,28 +17429,33 @@ var init_analytics_pulse = __esm({
|
|
|
16348
17429
|
|
|
16349
17430
|
// src/tools/connections.ts
|
|
16350
17431
|
import { z as z33 } from "zod";
|
|
16351
|
-
function
|
|
17432
|
+
function findActiveAccounts(accounts, platform3, projectId) {
|
|
16352
17433
|
const target = platform3.toLowerCase();
|
|
16353
|
-
return accounts.
|
|
17434
|
+
return accounts.filter((a) => {
|
|
16354
17435
|
const effectiveStatus = a.effective_status || a.status;
|
|
16355
|
-
return a.platform.toLowerCase() === target && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
|
|
16356
|
-
})
|
|
17436
|
+
return a.platform.toLowerCase() === target && a.project_id === projectId && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
|
|
17437
|
+
});
|
|
16357
17438
|
}
|
|
16358
17439
|
function registerConnectionTools(server) {
|
|
16359
17440
|
server.tool(
|
|
16360
17441
|
"start_platform_connection",
|
|
16361
17442
|
"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.",
|
|
16362
17443
|
{
|
|
16363
|
-
platform: z33.enum(PLATFORM_ENUM4).describe(
|
|
16364
|
-
|
|
16365
|
-
|
|
17444
|
+
platform: z33.enum(PLATFORM_ENUM4).describe(
|
|
17445
|
+
"Platform to connect. Lower-case: instagram, tiktok, youtube, etc."
|
|
17446
|
+
),
|
|
17447
|
+
project_id: z33.string().uuid().optional().describe(
|
|
17448
|
+
"Brand/project ID to bind the new social account to. Required when the account has multiple brands."
|
|
16366
17449
|
),
|
|
16367
17450
|
response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
16368
17451
|
},
|
|
16369
17452
|
async ({ platform: platform3, project_id, response_format }) => {
|
|
16370
17453
|
const format = response_format ?? "text";
|
|
16371
17454
|
const userId = await getDefaultUserId();
|
|
16372
|
-
const rl = checkRateLimit(
|
|
17455
|
+
const rl = checkRateLimit(
|
|
17456
|
+
"posting",
|
|
17457
|
+
`start_platform_connection:${userId}`
|
|
17458
|
+
);
|
|
16373
17459
|
if (!rl.allowed) {
|
|
16374
17460
|
return {
|
|
16375
17461
|
content: [
|
|
@@ -16381,12 +17467,27 @@ function registerConnectionTools(server) {
|
|
|
16381
17467
|
isError: true
|
|
16382
17468
|
};
|
|
16383
17469
|
}
|
|
17470
|
+
const projectResolution = await resolveProjectStrict(project_id);
|
|
17471
|
+
if (!projectResolution.projectId) {
|
|
17472
|
+
return {
|
|
17473
|
+
content: [
|
|
17474
|
+
{
|
|
17475
|
+
type: "text",
|
|
17476
|
+
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."
|
|
17477
|
+
}
|
|
17478
|
+
],
|
|
17479
|
+
isError: true
|
|
17480
|
+
};
|
|
17481
|
+
}
|
|
17482
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
17483
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
16384
17484
|
const { data, error } = await callEdgeFunction(
|
|
16385
17485
|
"mcp-data",
|
|
16386
17486
|
{
|
|
16387
17487
|
action: "mint-connection-nonce",
|
|
16388
17488
|
platform: platform3,
|
|
16389
|
-
|
|
17489
|
+
projectId: resolvedProjectId,
|
|
17490
|
+
project_id: resolvedProjectId
|
|
16390
17491
|
},
|
|
16391
17492
|
{ timeoutMs: 1e4 }
|
|
16392
17493
|
);
|
|
@@ -16402,6 +17503,17 @@ function registerConnectionTools(server) {
|
|
|
16402
17503
|
isError: true
|
|
16403
17504
|
};
|
|
16404
17505
|
}
|
|
17506
|
+
if (data.project_id !== resolvedProjectId) {
|
|
17507
|
+
return {
|
|
17508
|
+
content: [
|
|
17509
|
+
{
|
|
17510
|
+
type: "text",
|
|
17511
|
+
text: "Connection link project attestation failed. No OAuth link was returned."
|
|
17512
|
+
}
|
|
17513
|
+
],
|
|
17514
|
+
isError: true
|
|
17515
|
+
};
|
|
17516
|
+
}
|
|
16405
17517
|
if (format === "json") {
|
|
16406
17518
|
return {
|
|
16407
17519
|
content: [
|
|
@@ -16410,10 +17522,11 @@ function registerConnectionTools(server) {
|
|
|
16410
17522
|
text: JSON.stringify(
|
|
16411
17523
|
{
|
|
16412
17524
|
platform: data.platform,
|
|
16413
|
-
project_id:
|
|
17525
|
+
project_id: resolvedProjectId,
|
|
16414
17526
|
deep_link: data.deep_link,
|
|
16415
17527
|
expires_at: data.expires_at,
|
|
16416
|
-
next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection."
|
|
17528
|
+
next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection.",
|
|
17529
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
16417
17530
|
},
|
|
16418
17531
|
null,
|
|
16419
17532
|
2
|
|
@@ -16429,7 +17542,7 @@ function registerConnectionTools(server) {
|
|
|
16429
17542
|
type: "text",
|
|
16430
17543
|
text: [
|
|
16431
17544
|
`${data.platform} connection ready.`,
|
|
16432
|
-
|
|
17545
|
+
`Brand/project: ${resolvedProjectId}`,
|
|
16433
17546
|
"",
|
|
16434
17547
|
'Ask the user to open this link in a browser and click "Connect" on the platform:',
|
|
16435
17548
|
` ${data.deep_link}`,
|
|
@@ -16437,7 +17550,8 @@ function registerConnectionTools(server) {
|
|
|
16437
17550
|
`Link expires at: ${data.expires_at} (~2 minutes).`,
|
|
16438
17551
|
"",
|
|
16439
17552
|
"After they approve, call `wait_for_connection` with the same platform to confirm.",
|
|
16440
|
-
"This is a one-time browser setup \u2014 not another OAuth flow inside Claude."
|
|
17553
|
+
"This is a one-time browser setup \u2014 not another OAuth flow inside Claude.",
|
|
17554
|
+
...projectAutoResolvedNote ? ["", `Note: ${projectAutoResolvedNote}`] : []
|
|
16441
17555
|
].join("\n")
|
|
16442
17556
|
}
|
|
16443
17557
|
],
|
|
@@ -16450,14 +17564,20 @@ function registerConnectionTools(server) {
|
|
|
16450
17564
|
"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.",
|
|
16451
17565
|
{
|
|
16452
17566
|
platform: z33.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
|
|
16453
|
-
project_id: z33.string().optional().describe(
|
|
17567
|
+
project_id: z33.string().uuid().optional().describe(
|
|
16454
17568
|
"Brand/project ID to scope the connection poll. Use the same project_id passed to start_platform_connection."
|
|
16455
17569
|
),
|
|
16456
17570
|
timeout_s: z33.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
|
|
16457
17571
|
poll_interval_s: z33.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
|
|
16458
17572
|
response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
16459
17573
|
},
|
|
16460
|
-
async ({
|
|
17574
|
+
async ({
|
|
17575
|
+
platform: platform3,
|
|
17576
|
+
project_id,
|
|
17577
|
+
timeout_s,
|
|
17578
|
+
poll_interval_s,
|
|
17579
|
+
response_format
|
|
17580
|
+
}) => {
|
|
16461
17581
|
const format = response_format ?? "text";
|
|
16462
17582
|
const startedAt = Date.now();
|
|
16463
17583
|
const timeoutMs = (timeout_s ?? 120) * 1e3;
|
|
@@ -16476,6 +17596,18 @@ function registerConnectionTools(server) {
|
|
|
16476
17596
|
isError: true
|
|
16477
17597
|
};
|
|
16478
17598
|
}
|
|
17599
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
17600
|
+
if (!resolvedProjectId) {
|
|
17601
|
+
return {
|
|
17602
|
+
content: [
|
|
17603
|
+
{
|
|
17604
|
+
type: "text",
|
|
17605
|
+
text: "A project_id is required to wait for a connection. Use the same project_id used to start the OAuth flow."
|
|
17606
|
+
}
|
|
17607
|
+
],
|
|
17608
|
+
isError: true
|
|
17609
|
+
};
|
|
17610
|
+
}
|
|
16479
17611
|
const activeWaits = inFlightWaitsByUser.get(userId) ?? 0;
|
|
16480
17612
|
if (activeWaits >= MAX_CONCURRENT_WAITS_PER_USER) {
|
|
16481
17613
|
return {
|
|
@@ -16497,12 +17629,62 @@ function registerConnectionTools(server) {
|
|
|
16497
17629
|
"mcp-data",
|
|
16498
17630
|
{
|
|
16499
17631
|
action: "connected-accounts",
|
|
16500
|
-
|
|
17632
|
+
projectId: resolvedProjectId,
|
|
17633
|
+
project_id: resolvedProjectId
|
|
16501
17634
|
},
|
|
16502
17635
|
{ timeoutMs: 1e4 }
|
|
16503
17636
|
);
|
|
16504
17637
|
if (!error && data?.success) {
|
|
16505
|
-
const
|
|
17638
|
+
const foundAccounts = findActiveAccounts(
|
|
17639
|
+
data.accounts ?? [],
|
|
17640
|
+
platform3,
|
|
17641
|
+
resolvedProjectId
|
|
17642
|
+
);
|
|
17643
|
+
if (foundAccounts.length > 1) {
|
|
17644
|
+
if (format === "json") {
|
|
17645
|
+
return {
|
|
17646
|
+
content: [
|
|
17647
|
+
{
|
|
17648
|
+
type: "text",
|
|
17649
|
+
text: JSON.stringify(
|
|
17650
|
+
{
|
|
17651
|
+
connected: true,
|
|
17652
|
+
platform: platform3,
|
|
17653
|
+
project_id: resolvedProjectId,
|
|
17654
|
+
accounts: foundAccounts.map((a) => ({
|
|
17655
|
+
id: a.id,
|
|
17656
|
+
username: a.username,
|
|
17657
|
+
connected_at: a.created_at
|
|
17658
|
+
})),
|
|
17659
|
+
attempts,
|
|
17660
|
+
message: `${platform3} has ${foundAccounts.length} active accounts for this project. Pass the exact account_id to schedule_post.`
|
|
17661
|
+
},
|
|
17662
|
+
null,
|
|
17663
|
+
2
|
|
17664
|
+
)
|
|
17665
|
+
}
|
|
17666
|
+
],
|
|
17667
|
+
isError: false
|
|
17668
|
+
};
|
|
17669
|
+
}
|
|
17670
|
+
return {
|
|
17671
|
+
content: [
|
|
17672
|
+
{
|
|
17673
|
+
type: "text",
|
|
17674
|
+
text: [
|
|
17675
|
+
`${platform3} is connected \u2014 ${foundAccounts.length} active accounts found for project ${resolvedProjectId}:`,
|
|
17676
|
+
...foundAccounts.map(
|
|
17677
|
+
(a) => ` ${a.username || "(unnamed)"} (id=${a.id})`
|
|
17678
|
+
),
|
|
17679
|
+
"",
|
|
17680
|
+
"Call schedule_post with the exact account_id (or account_ids) for the one you mean."
|
|
17681
|
+
].join("\n")
|
|
17682
|
+
}
|
|
17683
|
+
],
|
|
17684
|
+
isError: false
|
|
17685
|
+
};
|
|
17686
|
+
}
|
|
17687
|
+
const found = foundAccounts[0];
|
|
16506
17688
|
if (found) {
|
|
16507
17689
|
if (format === "json") {
|
|
16508
17690
|
return {
|
|
@@ -16513,7 +17695,7 @@ function registerConnectionTools(server) {
|
|
|
16513
17695
|
{
|
|
16514
17696
|
connected: true,
|
|
16515
17697
|
platform: found.platform,
|
|
16516
|
-
project_id:
|
|
17698
|
+
project_id: resolvedProjectId,
|
|
16517
17699
|
account_id: found.id,
|
|
16518
17700
|
username: found.username,
|
|
16519
17701
|
connected_at: found.created_at,
|
|
@@ -16533,7 +17715,7 @@ function registerConnectionTools(server) {
|
|
|
16533
17715
|
type: "text",
|
|
16534
17716
|
text: [
|
|
16535
17717
|
`${found.platform} is connected.`,
|
|
16536
|
-
|
|
17718
|
+
`Brand/project: ${resolvedProjectId}`,
|
|
16537
17719
|
`Account: ${found.username || "(unnamed)"} (id=${found.id})`,
|
|
16538
17720
|
`Detected after ${attempts} poll(s) in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s.`,
|
|
16539
17721
|
"Ready to call `schedule_post`."
|
|
@@ -16546,7 +17728,9 @@ function registerConnectionTools(server) {
|
|
|
16546
17728
|
}
|
|
16547
17729
|
const remaining = deadline - Date.now();
|
|
16548
17730
|
if (remaining <= 0) break;
|
|
16549
|
-
await new Promise(
|
|
17731
|
+
await new Promise(
|
|
17732
|
+
(resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining))
|
|
17733
|
+
);
|
|
16550
17734
|
}
|
|
16551
17735
|
const message = `${platform3} did not connect within ${timeout_s ?? 120}s (${attempts} polls). The user may not have completed the browser OAuth yet, or the link expired. Mint a new link with \`start_platform_connection\` and try again, or have the user go directly to socialneuron.com/settings/connections.`;
|
|
16552
17736
|
if (format === "json") {
|
|
@@ -16558,7 +17742,7 @@ function registerConnectionTools(server) {
|
|
|
16558
17742
|
{
|
|
16559
17743
|
connected: false,
|
|
16560
17744
|
platform: platform3,
|
|
16561
|
-
project_id:
|
|
17745
|
+
project_id: resolvedProjectId,
|
|
16562
17746
|
attempts,
|
|
16563
17747
|
timed_out: true,
|
|
16564
17748
|
message
|