@socialneuron/mcp-server 1.8.2 → 1.9.0
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 +27 -0
- package/dist/http.js +1710 -562
- package/dist/index.js +1621 -550
- package/dist/sn.js +1615 -544
- package/package.json +6 -3
- package/tools.lock.json +14 -14
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.0";
|
|
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,49 +408,90 @@ 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";
|
|
470
|
+
}
|
|
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
|
+
);
|
|
246
491
|
}
|
|
492
|
+
throw new Error(
|
|
493
|
+
"Unable to verify absence of an existing Social Neuron Keychain credential. Unlock Keychain Access and retry."
|
|
494
|
+
);
|
|
247
495
|
}
|
|
248
496
|
function linuxSecretRead(key) {
|
|
249
497
|
try {
|
|
@@ -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
|
|
|
@@ -877,250 +1254,47 @@ function classifyError(err) {
|
|
|
877
1254
|
message,
|
|
878
1255
|
errorType: "RATE_LIMIT",
|
|
879
1256
|
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;
|
|
999
|
-
}
|
|
1000
|
-
if (!enrichedBody.projectId && !enrichedBody.project_id) {
|
|
1001
|
-
try {
|
|
1002
|
-
const { getDefaultProjectId: getDefaultProjectId2 } = await Promise.resolve().then(() => (init_supabase(), supabase_exports));
|
|
1003
|
-
const defaultProjectId = await getDefaultProjectId2();
|
|
1004
|
-
if (defaultProjectId) {
|
|
1005
|
-
enrichedBody.projectId = defaultProjectId;
|
|
1006
|
-
enrichedBody.project_id = defaultProjectId;
|
|
1007
|
-
}
|
|
1008
|
-
} catch {
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
let url;
|
|
1012
|
-
let method = options?.method ?? "POST";
|
|
1013
|
-
let headers;
|
|
1014
|
-
let requestBody;
|
|
1015
|
-
if (!apiKey) {
|
|
1016
|
-
clearTimeout(timer);
|
|
1017
|
-
return {
|
|
1018
|
-
data: null,
|
|
1019
|
-
error: "Not authenticated. Run: npx @socialneuron/mcp-server login \u2014 Requires a paid plan (Starter+). See https://socialneuron.com/pricing"
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
url = new URL(`${supabaseUrl}/functions/v1/mcp-gateway`);
|
|
1023
|
-
headers = {
|
|
1024
|
-
Authorization: `Bearer ${apiKey}`,
|
|
1025
|
-
"Content-Type": "application/json"
|
|
1026
|
-
};
|
|
1027
|
-
requestBody = {
|
|
1028
|
-
functionName,
|
|
1029
|
-
body: enrichedBody,
|
|
1030
|
-
query: options?.query,
|
|
1031
|
-
method: method.toUpperCase(),
|
|
1032
|
-
timeoutMs
|
|
1033
|
-
};
|
|
1034
|
-
method = "POST";
|
|
1035
|
-
try {
|
|
1036
|
-
const response = await fetch(url.toString(), {
|
|
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
|
-
}
|
|
1257
|
+
hint: "Wait a moment and retry."
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
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) {
|
|
1269
|
+
try {
|
|
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
|
|
|
@@ -4377,7 +4551,10 @@ function registerContentTools(server) {
|
|
|
4377
4551
|
isError: true
|
|
4378
4552
|
};
|
|
4379
4553
|
}
|
|
4380
|
-
const rateLimit = checkRateLimit(
|
|
4554
|
+
const rateLimit = checkRateLimit(
|
|
4555
|
+
"generation",
|
|
4556
|
+
`generate_video:${userId}`
|
|
4557
|
+
);
|
|
4381
4558
|
if (!rateLimit.allowed) {
|
|
4382
4559
|
return {
|
|
4383
4560
|
content: [
|
|
@@ -4500,7 +4677,14 @@ function registerContentTools(server) {
|
|
|
4500
4677
|
project_id: z2.string().optional().describe("Project ID to associate the generated image with."),
|
|
4501
4678
|
response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
4502
4679
|
},
|
|
4503
|
-
async ({
|
|
4680
|
+
async ({
|
|
4681
|
+
prompt: prompt2,
|
|
4682
|
+
model,
|
|
4683
|
+
aspect_ratio,
|
|
4684
|
+
image_url,
|
|
4685
|
+
project_id,
|
|
4686
|
+
response_format
|
|
4687
|
+
}) => {
|
|
4504
4688
|
const format = response_format ?? "text";
|
|
4505
4689
|
const userId = await getDefaultUserId();
|
|
4506
4690
|
const assetBudget = checkAssetBudget();
|
|
@@ -4518,7 +4702,10 @@ function registerContentTools(server) {
|
|
|
4518
4702
|
isError: true
|
|
4519
4703
|
};
|
|
4520
4704
|
}
|
|
4521
|
-
const rateLimit = checkRateLimit(
|
|
4705
|
+
const rateLimit = checkRateLimit(
|
|
4706
|
+
"generation",
|
|
4707
|
+
`generate_image:${userId}`
|
|
4708
|
+
);
|
|
4522
4709
|
if (!rateLimit.allowed) {
|
|
4523
4710
|
return {
|
|
4524
4711
|
content: [
|
|
@@ -4649,6 +4836,15 @@ function registerContentTools(server) {
|
|
|
4649
4836
|
isError: true
|
|
4650
4837
|
};
|
|
4651
4838
|
}
|
|
4839
|
+
let projectsDisclosure;
|
|
4840
|
+
const ownScopedProjectId = await getDefaultProjectId();
|
|
4841
|
+
if (!ownScopedProjectId) {
|
|
4842
|
+
const ownUserId = await getDefaultUserId().catch(() => null);
|
|
4843
|
+
if (ownUserId) {
|
|
4844
|
+
const list = await listAccessibleProjectsWithAccountStatus(ownUserId);
|
|
4845
|
+
if (list.length > 0) projectsDisclosure = list;
|
|
4846
|
+
}
|
|
4847
|
+
}
|
|
4652
4848
|
if (job.external_id && (job.status === "pending" || job.status === "processing")) {
|
|
4653
4849
|
const { data: liveStatus } = await callEdgeFunction(
|
|
4654
4850
|
"kie-task-status",
|
|
@@ -4672,7 +4868,9 @@ function registerContentTools(server) {
|
|
|
4672
4868
|
if (livePayload.error) {
|
|
4673
4869
|
lines2.push(`Error: ${livePayload.error}`);
|
|
4674
4870
|
}
|
|
4675
|
-
const fallbackDisclosure2 = buildFallbackDisclosureLine(
|
|
4871
|
+
const fallbackDisclosure2 = buildFallbackDisclosureLine(
|
|
4872
|
+
job.result_metadata
|
|
4873
|
+
);
|
|
4676
4874
|
if (fallbackDisclosure2) lines2.push(fallbackDisclosure2);
|
|
4677
4875
|
lines2.push(`Credits: ${job.credits_cost}`);
|
|
4678
4876
|
if (job.billing_status) {
|
|
@@ -4681,6 +4879,14 @@ function registerContentTools(server) {
|
|
|
4681
4879
|
);
|
|
4682
4880
|
}
|
|
4683
4881
|
lines2.push(`Created: ${job.created_at}`);
|
|
4882
|
+
if (projectsDisclosure) {
|
|
4883
|
+
lines2.push(
|
|
4884
|
+
"",
|
|
4885
|
+
`Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
|
|
4886
|
+
(p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
|
|
4887
|
+
).join("; ")}`
|
|
4888
|
+
);
|
|
4889
|
+
}
|
|
4684
4890
|
if (format === "json") {
|
|
4685
4891
|
return {
|
|
4686
4892
|
content: [
|
|
@@ -4697,7 +4903,8 @@ function registerContentTools(server) {
|
|
|
4697
4903
|
// `job` + `liveStatus`; do not duplicate them here.
|
|
4698
4904
|
asEnvelope2({
|
|
4699
4905
|
...liveStatus,
|
|
4700
|
-
...livePayload
|
|
4906
|
+
...livePayload,
|
|
4907
|
+
...projectsDisclosure ? { projects: projectsDisclosure } : {}
|
|
4701
4908
|
}),
|
|
4702
4909
|
null,
|
|
4703
4910
|
2
|
|
@@ -4740,7 +4947,9 @@ function registerContentTools(server) {
|
|
|
4740
4947
|
if (job.error_message) {
|
|
4741
4948
|
lines.push(`Error: ${job.error_message}`);
|
|
4742
4949
|
}
|
|
4743
|
-
const fallbackDisclosure = buildFallbackDisclosureLine(
|
|
4950
|
+
const fallbackDisclosure = buildFallbackDisclosureLine(
|
|
4951
|
+
job.result_metadata
|
|
4952
|
+
);
|
|
4744
4953
|
if (fallbackDisclosure) lines.push(fallbackDisclosure);
|
|
4745
4954
|
lines.push(`Credits: ${job.credits_cost}`);
|
|
4746
4955
|
if (job.billing_status) {
|
|
@@ -4752,10 +4961,19 @@ function registerContentTools(server) {
|
|
|
4752
4961
|
if (job.completed_at) {
|
|
4753
4962
|
lines.push(`Completed: ${job.completed_at}`);
|
|
4754
4963
|
}
|
|
4964
|
+
if (projectsDisclosure) {
|
|
4965
|
+
lines.push(
|
|
4966
|
+
"",
|
|
4967
|
+
`Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
|
|
4968
|
+
(p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
|
|
4969
|
+
).join("; ")}`
|
|
4970
|
+
);
|
|
4971
|
+
}
|
|
4755
4972
|
if (format === "json") {
|
|
4756
4973
|
const enriched = {
|
|
4757
4974
|
...job,
|
|
4758
|
-
...buildCheckStatusPayload(job)
|
|
4975
|
+
...buildCheckStatusPayload(job),
|
|
4976
|
+
...projectsDisclosure ? { projects: projectsDisclosure } : {}
|
|
4759
4977
|
};
|
|
4760
4978
|
return {
|
|
4761
4979
|
content: [
|
|
@@ -5499,6 +5717,122 @@ var init_ssrf = __esm({
|
|
|
5499
5717
|
}
|
|
5500
5718
|
});
|
|
5501
5719
|
|
|
5720
|
+
// src/lib/connected-account-routing.ts
|
|
5721
|
+
function canonicalPlatform(value) {
|
|
5722
|
+
const normalized = value.trim().toLowerCase();
|
|
5723
|
+
return normalized === "x" ? "twitter" : normalized;
|
|
5724
|
+
}
|
|
5725
|
+
function providerPlatform(value) {
|
|
5726
|
+
return PLATFORM_CASE_MAP[canonicalPlatform(value)] ?? value;
|
|
5727
|
+
}
|
|
5728
|
+
function isUsable(account) {
|
|
5729
|
+
const status = account.effective_status ?? account.status;
|
|
5730
|
+
return status === "active" || status === "expires_soon";
|
|
5731
|
+
}
|
|
5732
|
+
function normalizeRequestedIds(requested) {
|
|
5733
|
+
const ids = /* @__PURE__ */ new Map();
|
|
5734
|
+
for (const [platform3, accountId] of Object.entries(requested ?? {})) {
|
|
5735
|
+
const canonical = canonicalPlatform(platform3);
|
|
5736
|
+
const existing = ids.get(canonical);
|
|
5737
|
+
if (existing && existing !== accountId) {
|
|
5738
|
+
return {
|
|
5739
|
+
error: `Conflicting account IDs were supplied for ${platform3} and its platform alias.`
|
|
5740
|
+
};
|
|
5741
|
+
}
|
|
5742
|
+
ids.set(canonical, accountId);
|
|
5743
|
+
}
|
|
5744
|
+
return { ids };
|
|
5745
|
+
}
|
|
5746
|
+
async function resolveConnectedAccountRouting(input) {
|
|
5747
|
+
const normalizedRequested = normalizeRequestedIds(input.requestedAccountIds);
|
|
5748
|
+
if (normalizedRequested.error) return { error: normalizedRequested.error };
|
|
5749
|
+
const targetPlatforms = new Set(input.platforms.map(canonicalPlatform));
|
|
5750
|
+
for (const requestedPlatform of normalizedRequested.ids?.keys() ?? []) {
|
|
5751
|
+
if (!targetPlatforms.has(requestedPlatform)) {
|
|
5752
|
+
return {
|
|
5753
|
+
error: `An account ID was supplied for untargeted platform ${requestedPlatform}.`
|
|
5754
|
+
};
|
|
5755
|
+
}
|
|
5756
|
+
}
|
|
5757
|
+
const { data, error } = await callEdgeFunction(
|
|
5758
|
+
"mcp-data",
|
|
5759
|
+
{
|
|
5760
|
+
action: "connected-accounts",
|
|
5761
|
+
projectId: input.projectId,
|
|
5762
|
+
project_id: input.projectId
|
|
5763
|
+
},
|
|
5764
|
+
{ timeoutMs: 1e4 }
|
|
5765
|
+
);
|
|
5766
|
+
if (error || !Array.isArray(data?.accounts)) {
|
|
5767
|
+
return {
|
|
5768
|
+
error: `Connected-account verification failed: ${error ?? data?.error ?? "no account inventory returned"}.`
|
|
5769
|
+
};
|
|
5770
|
+
}
|
|
5771
|
+
const connectedAccountIds = {};
|
|
5772
|
+
for (const platform3 of input.platforms) {
|
|
5773
|
+
const canonical = canonicalPlatform(platform3);
|
|
5774
|
+
const displayPlatform = providerPlatform(platform3);
|
|
5775
|
+
const platformAccounts = data.accounts.filter(
|
|
5776
|
+
(account) => canonicalPlatform(account.platform) === canonical && account.project_id === input.projectId && isUsable(account)
|
|
5777
|
+
);
|
|
5778
|
+
const requestedId = normalizedRequested.ids?.get(canonical);
|
|
5779
|
+
let selected;
|
|
5780
|
+
if (requestedId) {
|
|
5781
|
+
selected = data.accounts.find((account) => account.id === requestedId);
|
|
5782
|
+
if (!selected) {
|
|
5783
|
+
return {
|
|
5784
|
+
error: `${displayPlatform}: account ${requestedId} is not available for project_id ${input.projectId}.`
|
|
5785
|
+
};
|
|
5786
|
+
}
|
|
5787
|
+
if (canonicalPlatform(selected.platform) !== canonical) {
|
|
5788
|
+
return {
|
|
5789
|
+
error: `${displayPlatform}: account ${requestedId} belongs to ${selected.platform}.`
|
|
5790
|
+
};
|
|
5791
|
+
}
|
|
5792
|
+
if (selected.project_id !== input.projectId) {
|
|
5793
|
+
return {
|
|
5794
|
+
error: `${displayPlatform}: account ${requestedId} is not bound to project_id ${input.projectId}.`
|
|
5795
|
+
};
|
|
5796
|
+
}
|
|
5797
|
+
if (!isUsable(selected)) {
|
|
5798
|
+
return {
|
|
5799
|
+
error: `${displayPlatform}: account ${requestedId} is ${selected.effective_status ?? selected.status}.`
|
|
5800
|
+
};
|
|
5801
|
+
}
|
|
5802
|
+
} else if (platformAccounts.length === 1) {
|
|
5803
|
+
selected = platformAccounts[0];
|
|
5804
|
+
} else if (platformAccounts.length === 0) {
|
|
5805
|
+
return {
|
|
5806
|
+
error: `${displayPlatform}: no active account is bound to project_id ${input.projectId}.`
|
|
5807
|
+
};
|
|
5808
|
+
} else {
|
|
5809
|
+
return {
|
|
5810
|
+
error: `${displayPlatform}: multiple active accounts are bound to project_id ${input.projectId}; pass the exact account ID returned by list_connected_accounts.`
|
|
5811
|
+
};
|
|
5812
|
+
}
|
|
5813
|
+
connectedAccountIds[displayPlatform] = selected.id;
|
|
5814
|
+
}
|
|
5815
|
+
return { connectedAccountIds };
|
|
5816
|
+
}
|
|
5817
|
+
var PLATFORM_CASE_MAP;
|
|
5818
|
+
var init_connected_account_routing = __esm({
|
|
5819
|
+
"src/lib/connected-account-routing.ts"() {
|
|
5820
|
+
"use strict";
|
|
5821
|
+
init_edge_function();
|
|
5822
|
+
PLATFORM_CASE_MAP = {
|
|
5823
|
+
youtube: "YouTube",
|
|
5824
|
+
tiktok: "TikTok",
|
|
5825
|
+
instagram: "Instagram",
|
|
5826
|
+
twitter: "Twitter",
|
|
5827
|
+
x: "Twitter",
|
|
5828
|
+
linkedin: "LinkedIn",
|
|
5829
|
+
facebook: "Facebook",
|
|
5830
|
+
threads: "Threads",
|
|
5831
|
+
bluesky: "Bluesky"
|
|
5832
|
+
};
|
|
5833
|
+
}
|
|
5834
|
+
});
|
|
5835
|
+
|
|
5502
5836
|
// src/tools/distribution.ts
|
|
5503
5837
|
import { z as z3 } from "zod";
|
|
5504
5838
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -5611,21 +5945,6 @@ async function validatePublishMediaUrl(url) {
|
|
|
5611
5945
|
function accountEffectiveStatus(account) {
|
|
5612
5946
|
return account.effective_status || account.status;
|
|
5613
5947
|
}
|
|
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
5948
|
async function rehostExternalUrl(mediaUrl, projectId) {
|
|
5630
5949
|
const ssrf = await validateUrlForSSRF(mediaUrl);
|
|
5631
5950
|
if (!ssrf.isValid) {
|
|
@@ -5761,17 +6080,17 @@ function registerDistributionTools(server) {
|
|
|
5761
6080
|
schedule_at: z3.string().optional().describe(
|
|
5762
6081
|
'ISO 8601 UTC datetime for scheduled posting (e.g. "2026-03-20T14:00:00Z"). Omit to post immediately. Must be in the future.'
|
|
5763
6082
|
),
|
|
5764
|
-
project_id: z3.string().optional().describe(
|
|
6083
|
+
project_id: z3.string().uuid().optional().describe(
|
|
5765
6084
|
"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
6085
|
),
|
|
5767
6086
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
|
|
5768
6087
|
attribution: z3.boolean().optional().describe(
|
|
5769
6088
|
'If true, appends "Created with Social Neuron" to the caption. Default: false.'
|
|
5770
6089
|
),
|
|
5771
|
-
account_id: z3.string().optional().describe(
|
|
5772
|
-
"Connected account ID to post from.
|
|
6090
|
+
account_id: z3.string().uuid().optional().describe(
|
|
6091
|
+
"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
6092
|
),
|
|
5774
|
-
account_ids: z3.record(z3.string(), z3.string()).optional().describe(
|
|
6093
|
+
account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
|
|
5775
6094
|
'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
6095
|
),
|
|
5777
6096
|
auto_rehost: z3.boolean().optional().describe(
|
|
@@ -5815,6 +6134,45 @@ function registerDistributionTools(server) {
|
|
|
5815
6134
|
isError: true
|
|
5816
6135
|
};
|
|
5817
6136
|
}
|
|
6137
|
+
const projectResolution = await resolveProjectForConnectedAccountTool(
|
|
6138
|
+
project_id,
|
|
6139
|
+
platforms
|
|
6140
|
+
);
|
|
6141
|
+
if (!projectResolution.projectId) {
|
|
6142
|
+
return {
|
|
6143
|
+
content: [
|
|
6144
|
+
{
|
|
6145
|
+
type: "text",
|
|
6146
|
+
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."
|
|
6147
|
+
}
|
|
6148
|
+
],
|
|
6149
|
+
isError: true
|
|
6150
|
+
};
|
|
6151
|
+
}
|
|
6152
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
6153
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
6154
|
+
if (account_id && account_ids) {
|
|
6155
|
+
return {
|
|
6156
|
+
content: [
|
|
6157
|
+
{
|
|
6158
|
+
type: "text",
|
|
6159
|
+
text: "Pass either account_id or account_ids, not both."
|
|
6160
|
+
}
|
|
6161
|
+
],
|
|
6162
|
+
isError: true
|
|
6163
|
+
};
|
|
6164
|
+
}
|
|
6165
|
+
if (account_id && platforms.length !== 1) {
|
|
6166
|
+
return {
|
|
6167
|
+
content: [
|
|
6168
|
+
{
|
|
6169
|
+
type: "text",
|
|
6170
|
+
text: "account_id is valid only for a single target platform. Use account_ids for multi-platform publishing."
|
|
6171
|
+
}
|
|
6172
|
+
],
|
|
6173
|
+
isError: true
|
|
6174
|
+
};
|
|
6175
|
+
}
|
|
5818
6176
|
const userId = await getDefaultUserId();
|
|
5819
6177
|
const rateLimit = checkRateLimit("posting", `schedule_post:${userId}`);
|
|
5820
6178
|
if (!rateLimit.allowed) {
|
|
@@ -5919,11 +6277,16 @@ function registerDistributionTools(server) {
|
|
|
5919
6277
|
}
|
|
5920
6278
|
const resolvedJobs = resolved;
|
|
5921
6279
|
resolvedMediaUrls = resolvedJobs.map((item) => item.url);
|
|
5922
|
-
resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map(
|
|
6280
|
+
resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map(
|
|
6281
|
+
(item) => item.trustedR2
|
|
6282
|
+
);
|
|
5923
6283
|
}
|
|
5924
6284
|
const shouldRehost = auto_rehost !== false;
|
|
5925
6285
|
if (shouldRehost && resolvedMediaUrl && !resolvedMediaUrlIsTrustedR2) {
|
|
5926
|
-
const rehost = await rehostExternalUrl(
|
|
6286
|
+
const rehost = await rehostExternalUrl(
|
|
6287
|
+
resolvedMediaUrl,
|
|
6288
|
+
resolvedProjectId
|
|
6289
|
+
);
|
|
5927
6290
|
if ("error" in rehost) {
|
|
5928
6291
|
return {
|
|
5929
6292
|
content: [
|
|
@@ -5941,7 +6304,7 @@ function registerDistributionTools(server) {
|
|
|
5941
6304
|
if (shouldRehost && resolvedMediaUrls && resolvedMediaUrls.length > 0) {
|
|
5942
6305
|
const rehosted = await Promise.all(
|
|
5943
6306
|
resolvedMediaUrls.map(
|
|
5944
|
-
(u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u,
|
|
6307
|
+
(u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, resolvedProjectId)
|
|
5945
6308
|
)
|
|
5946
6309
|
);
|
|
5947
6310
|
const failIdx = rehosted.findIndex((r) => "error" in r);
|
|
@@ -6008,7 +6371,7 @@ function registerDistributionTools(server) {
|
|
|
6008
6371
|
}
|
|
6009
6372
|
}
|
|
6010
6373
|
const normalizedPlatforms = platforms.map(
|
|
6011
|
-
(p) =>
|
|
6374
|
+
(p) => PLATFORM_CASE_MAP2[p.toLowerCase()] || p
|
|
6012
6375
|
);
|
|
6013
6376
|
const blockedPlatforms = normalizedPlatforms.filter(
|
|
6014
6377
|
(p) => MCP_NOT_LIVE_FOR_POSTING.has(p)
|
|
@@ -6024,92 +6387,27 @@ function registerDistributionTools(server) {
|
|
|
6024
6387
|
isError: true
|
|
6025
6388
|
};
|
|
6026
6389
|
}
|
|
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;
|
|
6390
|
+
let requestedAccountIds;
|
|
6102
6391
|
if (account_id) {
|
|
6103
|
-
|
|
6104
|
-
for (const p of normalizedPlatforms) {
|
|
6105
|
-
connectedAccountIds[p] = account_id;
|
|
6106
|
-
}
|
|
6392
|
+
requestedAccountIds = { [normalizedPlatforms[0]]: account_id };
|
|
6107
6393
|
} else if (account_ids) {
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6394
|
+
requestedAccountIds = account_ids;
|
|
6395
|
+
}
|
|
6396
|
+
const routing = await resolveConnectedAccountRouting({
|
|
6397
|
+
projectId: resolvedProjectId,
|
|
6398
|
+
platforms: normalizedPlatforms,
|
|
6399
|
+
requestedAccountIds
|
|
6400
|
+
});
|
|
6401
|
+
if (routing.error || !routing.connectedAccountIds) {
|
|
6402
|
+
return {
|
|
6403
|
+
content: [
|
|
6404
|
+
{
|
|
6405
|
+
type: "text",
|
|
6406
|
+
text: `Cannot post \u2014 ${routing.error ?? "exact connected-account routing could not be established."}`
|
|
6407
|
+
}
|
|
6408
|
+
],
|
|
6409
|
+
isError: true
|
|
6410
|
+
};
|
|
6113
6411
|
}
|
|
6114
6412
|
let finalCaption = caption;
|
|
6115
6413
|
if (attribution && finalCaption) {
|
|
@@ -6163,8 +6461,9 @@ Created with Social Neuron`;
|
|
|
6163
6461
|
title,
|
|
6164
6462
|
hashtags,
|
|
6165
6463
|
scheduledAt: schedule_at,
|
|
6166
|
-
projectId:
|
|
6167
|
-
|
|
6464
|
+
projectId: resolvedProjectId,
|
|
6465
|
+
project_id: resolvedProjectId,
|
|
6466
|
+
connectedAccountIds: routing.connectedAccountIds,
|
|
6168
6467
|
...normalizedPlatformMetadata ? {
|
|
6169
6468
|
platformMetadata: convertPlatformMetadata(
|
|
6170
6469
|
normalizedPlatformMetadata
|
|
@@ -6199,10 +6498,14 @@ Created with Social Neuron`;
|
|
|
6199
6498
|
isError: true
|
|
6200
6499
|
};
|
|
6201
6500
|
}
|
|
6501
|
+
const responseData = projectAutoResolvedNote ? { ...data, project_auto_resolved: projectAutoResolvedNote } : data;
|
|
6202
6502
|
const lines = [
|
|
6203
6503
|
data.success ? "Post scheduled successfully." : "Post scheduling had errors.",
|
|
6204
6504
|
`Scheduled for: ${data.scheduledAt}`
|
|
6205
6505
|
];
|
|
6506
|
+
if (projectAutoResolvedNote) {
|
|
6507
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
6508
|
+
}
|
|
6206
6509
|
if (tiktokAutoInboxApplied) {
|
|
6207
6510
|
lines.push(
|
|
6208
6511
|
"",
|
|
@@ -6220,7 +6523,7 @@ Created with Social Neuron`;
|
|
|
6220
6523
|
}
|
|
6221
6524
|
}
|
|
6222
6525
|
if (format === "json") {
|
|
6223
|
-
const structuredContent = asEnvelope3(
|
|
6526
|
+
const structuredContent = asEnvelope3(responseData);
|
|
6224
6527
|
return {
|
|
6225
6528
|
structuredContent,
|
|
6226
6529
|
content: [
|
|
@@ -6233,7 +6536,7 @@ Created with Social Neuron`;
|
|
|
6233
6536
|
};
|
|
6234
6537
|
}
|
|
6235
6538
|
return {
|
|
6236
|
-
structuredContent: asEnvelope3(
|
|
6539
|
+
structuredContent: asEnvelope3(responseData),
|
|
6237
6540
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
6238
6541
|
isError: !data.success
|
|
6239
6542
|
};
|
|
@@ -6247,7 +6550,9 @@ Created with Social Neuron`;
|
|
|
6247
6550
|
project_id: z3.string().uuid().optional().describe(
|
|
6248
6551
|
"Brand/project ID that owns the post. Defaults to the authenticated key's project or the account default."
|
|
6249
6552
|
),
|
|
6250
|
-
scheduled_at: z3.string().datetime({ offset: true }).describe(
|
|
6553
|
+
scheduled_at: z3.string().datetime({ offset: true }).describe(
|
|
6554
|
+
"New future publish time as an ISO 8601 datetime with timezone."
|
|
6555
|
+
),
|
|
6251
6556
|
expected_scheduled_at: z3.string().datetime({ offset: true }).optional().describe(
|
|
6252
6557
|
"Optional current schedule timestamp. If it changed since you read it, the update is rejected instead of silently overwriting it."
|
|
6253
6558
|
),
|
|
@@ -6303,7 +6608,11 @@ Created with Social Neuron`;
|
|
|
6303
6608
|
projectId: resolvedProjectId,
|
|
6304
6609
|
project_id: resolvedProjectId,
|
|
6305
6610
|
scheduled_at: next.toISOString(),
|
|
6306
|
-
...expected_scheduled_at ? {
|
|
6611
|
+
...expected_scheduled_at ? {
|
|
6612
|
+
expected_scheduled_at: new Date(
|
|
6613
|
+
expected_scheduled_at
|
|
6614
|
+
).toISOString()
|
|
6615
|
+
} : {}
|
|
6307
6616
|
});
|
|
6308
6617
|
if (error || !result?.success) {
|
|
6309
6618
|
const code = result?.error ?? error ?? "reschedule_failed";
|
|
@@ -6336,17 +6645,34 @@ Created with Social Neuron`;
|
|
|
6336
6645
|
"list_connected_accounts",
|
|
6337
6646
|
"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
6647
|
{
|
|
6339
|
-
project_id: z3.string().optional().describe(
|
|
6648
|
+
project_id: z3.string().uuid().optional().describe(
|
|
6340
6649
|
"Brand/project ID to scope connected accounts. Use the same project_id when calling schedule_post."
|
|
6341
6650
|
),
|
|
6342
|
-
include_all: z3.boolean().optional().describe(
|
|
6651
|
+
include_all: z3.boolean().optional().describe(
|
|
6652
|
+
"If true, include expired or inactive accounts as well as usable accounts."
|
|
6653
|
+
),
|
|
6343
6654
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6344
6655
|
},
|
|
6345
6656
|
async ({ project_id, include_all, response_format }) => {
|
|
6346
6657
|
const format = response_format ?? "text";
|
|
6658
|
+
const projectResolution = await resolveProjectForConnectedAccountTool(project_id);
|
|
6659
|
+
if (!projectResolution.projectId) {
|
|
6660
|
+
return {
|
|
6661
|
+
content: [
|
|
6662
|
+
{
|
|
6663
|
+
type: "text",
|
|
6664
|
+
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."
|
|
6665
|
+
}
|
|
6666
|
+
],
|
|
6667
|
+
isError: true
|
|
6668
|
+
};
|
|
6669
|
+
}
|
|
6670
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
6671
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
6347
6672
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
6348
6673
|
action: "connected-accounts",
|
|
6349
|
-
|
|
6674
|
+
projectId: resolvedProjectId,
|
|
6675
|
+
project_id: resolvedProjectId,
|
|
6350
6676
|
...include_all ? { includeAll: true } : {}
|
|
6351
6677
|
});
|
|
6352
6678
|
if (efError || !result?.success) {
|
|
@@ -6360,10 +6686,27 @@ Created with Social Neuron`;
|
|
|
6360
6686
|
isError: true
|
|
6361
6687
|
};
|
|
6362
6688
|
}
|
|
6363
|
-
const
|
|
6689
|
+
const parsedAccounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
|
|
6690
|
+
if (parsedAccounts.some(
|
|
6691
|
+
(account) => account.project_id !== resolvedProjectId
|
|
6692
|
+
)) {
|
|
6693
|
+
return {
|
|
6694
|
+
content: [
|
|
6695
|
+
{
|
|
6696
|
+
type: "text",
|
|
6697
|
+
text: "Connected-account project attestation failed. No account inventory was returned."
|
|
6698
|
+
}
|
|
6699
|
+
],
|
|
6700
|
+
isError: true
|
|
6701
|
+
};
|
|
6702
|
+
}
|
|
6703
|
+
const accounts = parsedAccounts;
|
|
6364
6704
|
if (accounts.length === 0) {
|
|
6365
6705
|
if (format === "json") {
|
|
6366
|
-
const structuredContent = asEnvelope3({
|
|
6706
|
+
const structuredContent = asEnvelope3({
|
|
6707
|
+
accounts: [],
|
|
6708
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
6709
|
+
});
|
|
6367
6710
|
return {
|
|
6368
6711
|
structuredContent,
|
|
6369
6712
|
content: [
|
|
@@ -6378,13 +6721,15 @@ Created with Social Neuron`;
|
|
|
6378
6721
|
content: [
|
|
6379
6722
|
{
|
|
6380
6723
|
type: "text",
|
|
6381
|
-
text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections."
|
|
6724
|
+
text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections." + (projectAutoResolvedNote ? `
|
|
6725
|
+
|
|
6726
|
+
Note: ${projectAutoResolvedNote}` : "")
|
|
6382
6727
|
}
|
|
6383
6728
|
]
|
|
6384
6729
|
};
|
|
6385
6730
|
}
|
|
6386
6731
|
const lines = [
|
|
6387
|
-
`${accounts.length} connected account(s)
|
|
6732
|
+
`${accounts.length} connected account(s) for project ${resolvedProjectId}:`,
|
|
6388
6733
|
""
|
|
6389
6734
|
];
|
|
6390
6735
|
for (const account of accounts) {
|
|
@@ -6396,8 +6741,14 @@ Created with Social Neuron`;
|
|
|
6396
6741
|
` ${platformLower}: ${name} | id=${account.id} | ${project} | status=${status} (connected ${account.created_at.split("T")[0]})`
|
|
6397
6742
|
);
|
|
6398
6743
|
}
|
|
6744
|
+
if (projectAutoResolvedNote) {
|
|
6745
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
6746
|
+
}
|
|
6399
6747
|
if (format === "json") {
|
|
6400
|
-
const structuredContent = asEnvelope3({
|
|
6748
|
+
const structuredContent = asEnvelope3({
|
|
6749
|
+
accounts,
|
|
6750
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
6751
|
+
});
|
|
6401
6752
|
return {
|
|
6402
6753
|
structuredContent,
|
|
6403
6754
|
content: [
|
|
@@ -6592,7 +6943,10 @@ Created with Social Neuron`;
|
|
|
6592
6943
|
if (!Number.isFinite(startDate.getTime())) {
|
|
6593
6944
|
return {
|
|
6594
6945
|
content: [
|
|
6595
|
-
{
|
|
6946
|
+
{
|
|
6947
|
+
type: "text",
|
|
6948
|
+
text: "start_after must be a valid ISO datetime."
|
|
6949
|
+
}
|
|
6596
6950
|
],
|
|
6597
6951
|
isError: true
|
|
6598
6952
|
};
|
|
@@ -6693,11 +7047,14 @@ Created with Social Neuron`;
|
|
|
6693
7047
|
"Schedule all posts in a content plan. Optionally auto-assigns time slots and runs quality checks before scheduling. Supports dry-run mode.",
|
|
6694
7048
|
{
|
|
6695
7049
|
plan: z3.object({
|
|
7050
|
+
project_id: z3.string().uuid().optional(),
|
|
7051
|
+
account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe("Exact connected-account ID per platform."),
|
|
6696
7052
|
posts: z3.array(
|
|
6697
7053
|
z3.object({
|
|
6698
7054
|
id: z3.string(),
|
|
6699
7055
|
caption: z3.string(),
|
|
6700
7056
|
platform: z3.string(),
|
|
7057
|
+
connected_account_id: z3.string().uuid().optional(),
|
|
6701
7058
|
title: z3.string().optional(),
|
|
6702
7059
|
media_url: z3.string().optional(),
|
|
6703
7060
|
schedule_at: z3.string().optional(),
|
|
@@ -6705,6 +7062,12 @@ Created with Social Neuron`;
|
|
|
6705
7062
|
})
|
|
6706
7063
|
)
|
|
6707
7064
|
}).passthrough().optional(),
|
|
7065
|
+
project_id: z3.string().uuid().optional().describe(
|
|
7066
|
+
"Exact brand/project ID. Defaults only when the authenticated user has one project."
|
|
7067
|
+
),
|
|
7068
|
+
account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
|
|
7069
|
+
"Exact connected-account ID per platform for every post in the plan."
|
|
7070
|
+
),
|
|
6708
7071
|
plan_id: z3.string().uuid().optional().describe("Persisted content plan ID from content_plans table"),
|
|
6709
7072
|
auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
|
|
6710
7073
|
dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
|
|
@@ -6721,6 +7084,8 @@ Created with Social Neuron`;
|
|
|
6721
7084
|
async ({
|
|
6722
7085
|
plan,
|
|
6723
7086
|
plan_id,
|
|
7087
|
+
project_id,
|
|
7088
|
+
account_ids,
|
|
6724
7089
|
auto_slot,
|
|
6725
7090
|
dry_run,
|
|
6726
7091
|
response_format,
|
|
@@ -6730,9 +7095,11 @@ Created with Social Neuron`;
|
|
|
6730
7095
|
idempotency_seed
|
|
6731
7096
|
}) => {
|
|
6732
7097
|
try {
|
|
7098
|
+
const effectiveBatchSize = batch_size ?? 4;
|
|
6733
7099
|
let workingPlan = plan;
|
|
6734
7100
|
let effectivePlanId = plan_id;
|
|
6735
|
-
let effectiveProjectId;
|
|
7101
|
+
let effectiveProjectId = project_id;
|
|
7102
|
+
let projectAutoResolvedNote;
|
|
6736
7103
|
let approvalSummary;
|
|
6737
7104
|
if (!workingPlan && plan_id) {
|
|
6738
7105
|
const { data: planResult, error: planError } = await callEdgeFunction("mcp-data", { action: "get-content-plan", plan_id });
|
|
@@ -6779,7 +7146,18 @@ Created with Social Neuron`;
|
|
|
6779
7146
|
posts: postsFromPayload
|
|
6780
7147
|
};
|
|
6781
7148
|
effectivePlanId = stored.id;
|
|
6782
|
-
effectiveProjectId
|
|
7149
|
+
if (effectiveProjectId && stored.project_id && effectiveProjectId !== stored.project_id) {
|
|
7150
|
+
return {
|
|
7151
|
+
content: [
|
|
7152
|
+
{
|
|
7153
|
+
type: "text",
|
|
7154
|
+
text: `project_id ${effectiveProjectId} does not own plan ${plan_id}.`
|
|
7155
|
+
}
|
|
7156
|
+
],
|
|
7157
|
+
isError: true
|
|
7158
|
+
};
|
|
7159
|
+
}
|
|
7160
|
+
effectiveProjectId = stored.project_id ?? effectiveProjectId;
|
|
6783
7161
|
}
|
|
6784
7162
|
if (!workingPlan) {
|
|
6785
7163
|
return {
|
|
@@ -6792,10 +7170,35 @@ Created with Social Neuron`;
|
|
|
6792
7170
|
isError: true
|
|
6793
7171
|
};
|
|
6794
7172
|
}
|
|
7173
|
+
const planProjectId = workingPlan.project_id;
|
|
7174
|
+
if (effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0 && effectiveProjectId !== planProjectId) {
|
|
7175
|
+
return {
|
|
7176
|
+
content: [
|
|
7177
|
+
{
|
|
7178
|
+
type: "text",
|
|
7179
|
+
text: `Conflicting project_id values were supplied for the plan (${planProjectId}) and request (${effectiveProjectId}).`
|
|
7180
|
+
}
|
|
7181
|
+
],
|
|
7182
|
+
isError: true
|
|
7183
|
+
};
|
|
7184
|
+
}
|
|
7185
|
+
if (!effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0) {
|
|
7186
|
+
effectiveProjectId = planProjectId;
|
|
7187
|
+
}
|
|
6795
7188
|
if (!effectiveProjectId) {
|
|
6796
|
-
const
|
|
6797
|
-
|
|
6798
|
-
|
|
7189
|
+
const projectResolution = await resolveProjectForConnectedAccountTool();
|
|
7190
|
+
effectiveProjectId = projectResolution.projectId;
|
|
7191
|
+
projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
7192
|
+
if (!effectiveProjectId) {
|
|
7193
|
+
return {
|
|
7194
|
+
content: [
|
|
7195
|
+
{
|
|
7196
|
+
type: "text",
|
|
7197
|
+
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."
|
|
7198
|
+
}
|
|
7199
|
+
],
|
|
7200
|
+
isError: true
|
|
7201
|
+
};
|
|
6799
7202
|
}
|
|
6800
7203
|
}
|
|
6801
7204
|
if (effectivePlanId) {
|
|
@@ -7014,6 +7417,61 @@ Created with Social Neuron`;
|
|
|
7014
7417
|
isError: false
|
|
7015
7418
|
};
|
|
7016
7419
|
}
|
|
7420
|
+
const embeddedAccountIds = workingPlan.account_ids ?? {};
|
|
7421
|
+
for (const [platform3, accountId] of Object.entries(account_ids ?? {})) {
|
|
7422
|
+
if (embeddedAccountIds[platform3] && embeddedAccountIds[platform3] !== accountId) {
|
|
7423
|
+
return {
|
|
7424
|
+
content: [
|
|
7425
|
+
{
|
|
7426
|
+
type: "text",
|
|
7427
|
+
text: `Conflicting account_ids values were supplied for ${platform3}.`
|
|
7428
|
+
}
|
|
7429
|
+
],
|
|
7430
|
+
isError: true
|
|
7431
|
+
};
|
|
7432
|
+
}
|
|
7433
|
+
}
|
|
7434
|
+
const requestedPlanAccountIds = {
|
|
7435
|
+
...embeddedAccountIds,
|
|
7436
|
+
...account_ids ?? {}
|
|
7437
|
+
};
|
|
7438
|
+
for (const post of workingPlan.posts) {
|
|
7439
|
+
if (!post.connected_account_id) continue;
|
|
7440
|
+
const key = post.platform.toLowerCase() === "x" ? "twitter" : post.platform.toLowerCase();
|
|
7441
|
+
const existing = requestedPlanAccountIds[key];
|
|
7442
|
+
if (existing && existing !== post.connected_account_id) {
|
|
7443
|
+
return {
|
|
7444
|
+
content: [
|
|
7445
|
+
{
|
|
7446
|
+
type: "text",
|
|
7447
|
+
text: `Plan contains conflicting connected_account_id values for ${post.platform}. Use separate plans when scheduling the same platform through different accounts.`
|
|
7448
|
+
}
|
|
7449
|
+
],
|
|
7450
|
+
isError: true
|
|
7451
|
+
};
|
|
7452
|
+
}
|
|
7453
|
+
requestedPlanAccountIds[key] = post.connected_account_id;
|
|
7454
|
+
}
|
|
7455
|
+
const planPlatforms = Array.from(
|
|
7456
|
+
new Set(workingPlan.posts.map((post) => post.platform))
|
|
7457
|
+
);
|
|
7458
|
+
const planRouting = await resolveConnectedAccountRouting({
|
|
7459
|
+
projectId: effectiveProjectId,
|
|
7460
|
+
platforms: planPlatforms,
|
|
7461
|
+
requestedAccountIds: requestedPlanAccountIds
|
|
7462
|
+
});
|
|
7463
|
+
if (planRouting.error || !planRouting.connectedAccountIds) {
|
|
7464
|
+
return {
|
|
7465
|
+
content: [
|
|
7466
|
+
{
|
|
7467
|
+
type: "text",
|
|
7468
|
+
text: `Cannot schedule plan \u2014 ${planRouting.error ?? "exact connected-account routing could not be established."}`
|
|
7469
|
+
}
|
|
7470
|
+
],
|
|
7471
|
+
isError: true
|
|
7472
|
+
};
|
|
7473
|
+
}
|
|
7474
|
+
const verifiedPlanAccountIds = planRouting.connectedAccountIds;
|
|
7017
7475
|
let scheduled = 0;
|
|
7018
7476
|
let failed = 0;
|
|
7019
7477
|
const results = [];
|
|
@@ -7042,7 +7500,7 @@ Created with Social Neuron`;
|
|
|
7042
7500
|
retryable: false
|
|
7043
7501
|
};
|
|
7044
7502
|
}
|
|
7045
|
-
const normalizedPlatform =
|
|
7503
|
+
const normalizedPlatform = PLATFORM_CASE_MAP2[post.platform.toLowerCase()] ?? post.platform;
|
|
7046
7504
|
const idempotencyKey = buildIdempotencyKey(post);
|
|
7047
7505
|
const { data, error } = await callEdgeFunction(
|
|
7048
7506
|
"schedule-post",
|
|
@@ -7053,6 +7511,13 @@ Created with Social Neuron`;
|
|
|
7053
7511
|
mediaUrl: post.media_url,
|
|
7054
7512
|
scheduledAt: post.schedule_at,
|
|
7055
7513
|
hashtags: post.hashtags,
|
|
7514
|
+
...effectiveProjectId ? {
|
|
7515
|
+
projectId: effectiveProjectId,
|
|
7516
|
+
project_id: effectiveProjectId
|
|
7517
|
+
} : {},
|
|
7518
|
+
connectedAccountIds: {
|
|
7519
|
+
[normalizedPlatform]: verifiedPlanAccountIds[normalizedPlatform]
|
|
7520
|
+
},
|
|
7056
7521
|
...effectivePlanId ? { planId: effectivePlanId } : {},
|
|
7057
7522
|
idempotencyKey
|
|
7058
7523
|
},
|
|
@@ -7111,7 +7576,7 @@ Created with Social Neuron`;
|
|
|
7111
7576
|
const platformBatches = Array.from(grouped.entries()).map(
|
|
7112
7577
|
async ([platform3, platformPosts]) => {
|
|
7113
7578
|
const platformResults = [];
|
|
7114
|
-
const batches = chunk(platformPosts,
|
|
7579
|
+
const batches = chunk(platformPosts, effectiveBatchSize);
|
|
7115
7580
|
for (const batch of batches) {
|
|
7116
7581
|
const settled = await Promise.allSettled(
|
|
7117
7582
|
batch.map((post) => scheduleOne(post))
|
|
@@ -7172,7 +7637,8 @@ Created with Social Neuron`;
|
|
|
7172
7637
|
total_posts: workingPlan.posts.length,
|
|
7173
7638
|
scheduled,
|
|
7174
7639
|
failed
|
|
7175
|
-
}
|
|
7640
|
+
},
|
|
7641
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
7176
7642
|
}),
|
|
7177
7643
|
null,
|
|
7178
7644
|
2
|
|
@@ -7196,6 +7662,9 @@ Created with Social Neuron`;
|
|
|
7196
7662
|
lines.push(
|
|
7197
7663
|
`Scheduled: ${scheduled}/${workingPlan.posts.length} | Failed: ${failed}/${workingPlan.posts.length}`
|
|
7198
7664
|
);
|
|
7665
|
+
if (projectAutoResolvedNote) {
|
|
7666
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
7667
|
+
}
|
|
7199
7668
|
return {
|
|
7200
7669
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
7201
7670
|
isError: failed > 0
|
|
@@ -7215,7 +7684,7 @@ Created with Social Neuron`;
|
|
|
7215
7684
|
}
|
|
7216
7685
|
);
|
|
7217
7686
|
}
|
|
7218
|
-
var
|
|
7687
|
+
var PLATFORM_CASE_MAP2, TIKTOK_AUDIT_APPROVED, MCP_NOT_LIVE_FOR_POSTING, VIDEO_FILE_EXTENSIONS, IMAGE_FILE_EXTENSIONS;
|
|
7219
7688
|
var init_distribution = __esm({
|
|
7220
7689
|
"src/tools/distribution.ts"() {
|
|
7221
7690
|
"use strict";
|
|
@@ -7226,11 +7695,17 @@ var init_distribution = __esm({
|
|
|
7226
7695
|
init_supabase();
|
|
7227
7696
|
init_quality();
|
|
7228
7697
|
init_version();
|
|
7229
|
-
|
|
7698
|
+
init_connected_account_routing();
|
|
7699
|
+
PLATFORM_CASE_MAP2 = {
|
|
7230
7700
|
youtube: "YouTube",
|
|
7231
7701
|
tiktok: "TikTok",
|
|
7232
7702
|
instagram: "Instagram",
|
|
7233
7703
|
twitter: "Twitter",
|
|
7704
|
+
// 'x' is the platform's current branding but connected_account_routing.ts
|
|
7705
|
+
// (and the DB convention) still key on 'Twitter' — keep both aliases
|
|
7706
|
+
// resolving to the same case so schedule_content_plan's platform:'x' posts
|
|
7707
|
+
// don't fall through to an undefined binding (F8, 2026-07-15).
|
|
7708
|
+
x: "Twitter",
|
|
7234
7709
|
linkedin: "LinkedIn",
|
|
7235
7710
|
facebook: "Facebook",
|
|
7236
7711
|
threads: "Threads",
|
|
@@ -7772,15 +8247,35 @@ function registerAnalyticsTools(server) {
|
|
|
7772
8247
|
content_id: z5.string().uuid().optional().describe(
|
|
7773
8248
|
"Filter to a specific content_history ID to see performance of one piece of content."
|
|
7774
8249
|
),
|
|
7775
|
-
project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
8250
|
+
project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
|
|
7776
8251
|
limit: z5.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
|
|
7777
8252
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
7778
8253
|
},
|
|
7779
|
-
async ({
|
|
8254
|
+
async ({
|
|
8255
|
+
platform: platform3,
|
|
8256
|
+
days,
|
|
8257
|
+
content_id,
|
|
8258
|
+
project_id,
|
|
8259
|
+
limit,
|
|
8260
|
+
response_format
|
|
8261
|
+
}) => {
|
|
7780
8262
|
const format = response_format ?? "text";
|
|
7781
8263
|
const lookbackDays = days ?? 30;
|
|
7782
8264
|
const maxPosts = limit ?? 20;
|
|
7783
|
-
const
|
|
8265
|
+
const projectResolution = await resolveProjectStrict(project_id);
|
|
8266
|
+
if (!projectResolution.projectId) {
|
|
8267
|
+
return {
|
|
8268
|
+
content: [
|
|
8269
|
+
{
|
|
8270
|
+
type: "text",
|
|
8271
|
+
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."
|
|
8272
|
+
}
|
|
8273
|
+
],
|
|
8274
|
+
isError: true
|
|
8275
|
+
};
|
|
8276
|
+
}
|
|
8277
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
8278
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
7784
8279
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
7785
8280
|
action: "analytics",
|
|
7786
8281
|
platform: platform3,
|
|
@@ -7791,11 +8286,17 @@ function registerAnalyticsTools(server) {
|
|
|
7791
8286
|
limit: Math.min(maxPosts * 5, 100),
|
|
7792
8287
|
latestOnly: true,
|
|
7793
8288
|
contentId: content_id,
|
|
7794
|
-
|
|
8289
|
+
projectId: resolvedProjectId,
|
|
8290
|
+
project_id: resolvedProjectId
|
|
7795
8291
|
});
|
|
7796
8292
|
if (efError) {
|
|
7797
8293
|
return {
|
|
7798
|
-
content: [
|
|
8294
|
+
content: [
|
|
8295
|
+
{
|
|
8296
|
+
type: "text",
|
|
8297
|
+
text: `Failed to fetch analytics: ${efError}`
|
|
8298
|
+
}
|
|
8299
|
+
],
|
|
7799
8300
|
isError: true
|
|
7800
8301
|
};
|
|
7801
8302
|
}
|
|
@@ -7815,7 +8316,8 @@ function registerAnalyticsTools(server) {
|
|
|
7815
8316
|
totalViews: 0,
|
|
7816
8317
|
totalEngagement: 0,
|
|
7817
8318
|
postCount: 0,
|
|
7818
|
-
posts: []
|
|
8319
|
+
posts: [],
|
|
8320
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
7819
8321
|
});
|
|
7820
8322
|
return {
|
|
7821
8323
|
structuredContent,
|
|
@@ -7831,7 +8333,9 @@ function registerAnalyticsTools(server) {
|
|
|
7831
8333
|
content: [
|
|
7832
8334
|
{
|
|
7833
8335
|
type: "text",
|
|
7834
|
-
text: `No analytics data found for the last ${lookbackDays} days${platform3 ? ` on ${platform3}` : ""}.`
|
|
8336
|
+
text: `No analytics data found for the last ${lookbackDays} days${platform3 ? ` on ${platform3}` : ""}.` + (projectAutoResolvedNote ? `
|
|
8337
|
+
|
|
8338
|
+
Note: ${projectAutoResolvedNote}` : "")
|
|
7835
8339
|
}
|
|
7836
8340
|
]
|
|
7837
8341
|
};
|
|
@@ -7863,20 +8367,27 @@ function registerAnalyticsTools(server) {
|
|
|
7863
8367
|
postCount: posts.length,
|
|
7864
8368
|
posts
|
|
7865
8369
|
};
|
|
7866
|
-
return formatAnalytics(
|
|
8370
|
+
return formatAnalytics(
|
|
8371
|
+
summary,
|
|
8372
|
+
lookbackDays,
|
|
8373
|
+
format,
|
|
8374
|
+
projectAutoResolvedNote
|
|
8375
|
+
);
|
|
7867
8376
|
}
|
|
7868
8377
|
);
|
|
7869
8378
|
server.tool(
|
|
7870
8379
|
"refresh_platform_analytics",
|
|
7871
8380
|
"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
8381
|
{
|
|
7873
|
-
project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
8382
|
+
project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
|
|
7874
8383
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
7875
8384
|
},
|
|
7876
8385
|
async ({ project_id, response_format }) => {
|
|
7877
8386
|
const format = response_format ?? "text";
|
|
7878
8387
|
const userId = await getDefaultUserId();
|
|
7879
|
-
const
|
|
8388
|
+
const projectResolution = await resolveProjectStrict(project_id);
|
|
8389
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
8390
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
7880
8391
|
const rateLimit = checkRateLimit(
|
|
7881
8392
|
"posting",
|
|
7882
8393
|
`refresh_platform_analytics:${userId}:${resolvedProjectId ?? "default"}`
|
|
@@ -7892,25 +8403,48 @@ function registerAnalyticsTools(server) {
|
|
|
7892
8403
|
isError: true
|
|
7893
8404
|
};
|
|
7894
8405
|
}
|
|
8406
|
+
if (!resolvedProjectId) {
|
|
8407
|
+
return {
|
|
8408
|
+
content: [
|
|
8409
|
+
{
|
|
8410
|
+
type: "text",
|
|
8411
|
+
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."
|
|
8412
|
+
}
|
|
8413
|
+
],
|
|
8414
|
+
isError: true
|
|
8415
|
+
};
|
|
8416
|
+
}
|
|
7895
8417
|
const { data, error } = await callEdgeFunction("fetch-analytics", {
|
|
7896
8418
|
userId,
|
|
7897
|
-
|
|
8419
|
+
projectId: resolvedProjectId,
|
|
8420
|
+
project_id: resolvedProjectId
|
|
7898
8421
|
});
|
|
7899
8422
|
if (error) {
|
|
7900
8423
|
return {
|
|
7901
|
-
content: [
|
|
8424
|
+
content: [
|
|
8425
|
+
{
|
|
8426
|
+
type: "text",
|
|
8427
|
+
text: `Error refreshing analytics: ${error}`
|
|
8428
|
+
}
|
|
8429
|
+
],
|
|
7902
8430
|
isError: true
|
|
7903
8431
|
};
|
|
7904
8432
|
}
|
|
7905
8433
|
const result = data;
|
|
7906
8434
|
if (!result.success) {
|
|
7907
8435
|
return {
|
|
7908
|
-
content: [
|
|
8436
|
+
content: [
|
|
8437
|
+
{ type: "text", text: "Analytics refresh failed." }
|
|
8438
|
+
],
|
|
7909
8439
|
isError: true
|
|
7910
8440
|
};
|
|
7911
8441
|
}
|
|
7912
|
-
const queued = (result.results ?? []).filter(
|
|
7913
|
-
|
|
8442
|
+
const queued = (result.results ?? []).filter(
|
|
8443
|
+
(r) => r.status === "queued"
|
|
8444
|
+
).length;
|
|
8445
|
+
const errored = (result.results ?? []).filter(
|
|
8446
|
+
(r) => r.status === "error"
|
|
8447
|
+
).length;
|
|
7914
8448
|
const lines = [
|
|
7915
8449
|
`Analytics refresh triggered successfully.`,
|
|
7916
8450
|
` Posts processed: ${result.postsProcessed}`,
|
|
@@ -7919,13 +8453,17 @@ function registerAnalyticsTools(server) {
|
|
|
7919
8453
|
if (errored > 0) {
|
|
7920
8454
|
lines.push(` Errors: ${errored}`);
|
|
7921
8455
|
}
|
|
8456
|
+
if (projectAutoResolvedNote) {
|
|
8457
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
8458
|
+
}
|
|
7922
8459
|
if (format === "json") {
|
|
7923
8460
|
const structuredContent = asEnvelope4({
|
|
7924
8461
|
success: true,
|
|
7925
8462
|
postsProcessed: result.postsProcessed,
|
|
7926
8463
|
queued,
|
|
7927
8464
|
errored,
|
|
7928
|
-
projectId: resolvedProjectId ?? null
|
|
8465
|
+
projectId: resolvedProjectId ?? null,
|
|
8466
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
7929
8467
|
});
|
|
7930
8468
|
return {
|
|
7931
8469
|
structuredContent,
|
|
@@ -7941,12 +8479,21 @@ function registerAnalyticsTools(server) {
|
|
|
7941
8479
|
}
|
|
7942
8480
|
);
|
|
7943
8481
|
}
|
|
7944
|
-
function formatAnalytics(summary, days, format) {
|
|
7945
|
-
const structuredContent = asEnvelope4({
|
|
8482
|
+
function formatAnalytics(summary, days, format, projectAutoResolvedNote) {
|
|
8483
|
+
const structuredContent = asEnvelope4({
|
|
8484
|
+
...summary,
|
|
8485
|
+
days,
|
|
8486
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
8487
|
+
});
|
|
7946
8488
|
if (format === "json") {
|
|
7947
8489
|
return {
|
|
7948
8490
|
structuredContent,
|
|
7949
|
-
content: [
|
|
8491
|
+
content: [
|
|
8492
|
+
{
|
|
8493
|
+
type: "text",
|
|
8494
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
8495
|
+
}
|
|
8496
|
+
]
|
|
7950
8497
|
};
|
|
7951
8498
|
}
|
|
7952
8499
|
const lines = [
|
|
@@ -7970,6 +8517,9 @@ function formatAnalytics(summary, days, format) {
|
|
|
7970
8517
|
lines.push(line);
|
|
7971
8518
|
}
|
|
7972
8519
|
}
|
|
8520
|
+
if (projectAutoResolvedNote) {
|
|
8521
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
8522
|
+
}
|
|
7973
8523
|
return {
|
|
7974
8524
|
structuredContent,
|
|
7975
8525
|
content: [{ type: "text", text: lines.join("\n") }]
|
|
@@ -9399,15 +9949,64 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
9399
9949
|
start_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Start date in YYYY-MM-DD format."),
|
|
9400
9950
|
end_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("End date in YYYY-MM-DD format."),
|
|
9401
9951
|
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(
|
|
9952
|
+
max_results: z10.number().min(1).max(50).optional().describe(
|
|
9953
|
+
'Max videos to return for "topVideos" action. Defaults to 10.'
|
|
9954
|
+
),
|
|
9955
|
+
connected_account_id: z10.string().uuid().optional().describe(
|
|
9956
|
+
"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."
|
|
9957
|
+
),
|
|
9958
|
+
project_id: z10.string().uuid().optional().describe(
|
|
9959
|
+
"Exact brand/project ID. Defaults only when the authenticated user has one project."
|
|
9960
|
+
),
|
|
9403
9961
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9404
9962
|
},
|
|
9405
|
-
async ({
|
|
9963
|
+
async ({
|
|
9964
|
+
action,
|
|
9965
|
+
start_date,
|
|
9966
|
+
end_date,
|
|
9967
|
+
video_id,
|
|
9968
|
+
max_results,
|
|
9969
|
+
connected_account_id,
|
|
9970
|
+
project_id,
|
|
9971
|
+
response_format
|
|
9972
|
+
}) => {
|
|
9406
9973
|
const format = response_format ?? "text";
|
|
9407
9974
|
if (action === "video" && !video_id) {
|
|
9408
9975
|
return {
|
|
9409
9976
|
content: [
|
|
9410
|
-
{
|
|
9977
|
+
{
|
|
9978
|
+
type: "text",
|
|
9979
|
+
text: 'Error: video_id is required when action is "video".'
|
|
9980
|
+
}
|
|
9981
|
+
],
|
|
9982
|
+
isError: true
|
|
9983
|
+
};
|
|
9984
|
+
}
|
|
9985
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
9986
|
+
if (!resolvedProjectId) {
|
|
9987
|
+
return {
|
|
9988
|
+
content: [
|
|
9989
|
+
{
|
|
9990
|
+
type: "text",
|
|
9991
|
+
text: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
|
|
9992
|
+
}
|
|
9993
|
+
],
|
|
9994
|
+
isError: true
|
|
9995
|
+
};
|
|
9996
|
+
}
|
|
9997
|
+
const routing = await resolveConnectedAccountRouting({
|
|
9998
|
+
projectId: resolvedProjectId,
|
|
9999
|
+
platforms: ["youtube"],
|
|
10000
|
+
requestedAccountIds: connected_account_id ? { youtube: connected_account_id } : void 0
|
|
10001
|
+
});
|
|
10002
|
+
const resolvedAccountId = routing.connectedAccountIds?.YouTube;
|
|
10003
|
+
if (routing.error || !resolvedAccountId) {
|
|
10004
|
+
return {
|
|
10005
|
+
content: [
|
|
10006
|
+
{
|
|
10007
|
+
type: "text",
|
|
10008
|
+
text: routing.error ?? "YouTube: exact connected-account routing could not be established."
|
|
10009
|
+
}
|
|
9411
10010
|
],
|
|
9412
10011
|
isError: true
|
|
9413
10012
|
};
|
|
@@ -9417,11 +10016,19 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
9417
10016
|
startDate: start_date,
|
|
9418
10017
|
endDate: end_date,
|
|
9419
10018
|
videoId: video_id,
|
|
9420
|
-
maxResults: max_results ?? 10
|
|
10019
|
+
maxResults: max_results ?? 10,
|
|
10020
|
+
projectId: resolvedProjectId,
|
|
10021
|
+
project_id: resolvedProjectId,
|
|
10022
|
+
connectedAccountId: resolvedAccountId
|
|
9421
10023
|
});
|
|
9422
10024
|
if (error) {
|
|
9423
10025
|
return {
|
|
9424
|
-
content: [
|
|
10026
|
+
content: [
|
|
10027
|
+
{
|
|
10028
|
+
type: "text",
|
|
10029
|
+
text: `YouTube Analytics error: ${error}`
|
|
10030
|
+
}
|
|
10031
|
+
],
|
|
9425
10032
|
isError: true
|
|
9426
10033
|
};
|
|
9427
10034
|
}
|
|
@@ -9434,7 +10041,12 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
9434
10041
|
{
|
|
9435
10042
|
type: "text",
|
|
9436
10043
|
text: JSON.stringify(
|
|
9437
|
-
asEnvelope7({
|
|
10044
|
+
asEnvelope7({
|
|
10045
|
+
action,
|
|
10046
|
+
startDate: start_date,
|
|
10047
|
+
endDate: end_date,
|
|
10048
|
+
analytics: a
|
|
10049
|
+
}),
|
|
9438
10050
|
null,
|
|
9439
10051
|
2
|
|
9440
10052
|
)
|
|
@@ -9461,7 +10073,10 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
9461
10073
|
if (days.length === 0) {
|
|
9462
10074
|
return {
|
|
9463
10075
|
content: [
|
|
9464
|
-
{
|
|
10076
|
+
{
|
|
10077
|
+
type: "text",
|
|
10078
|
+
text: "No daily analytics data found for this period."
|
|
10079
|
+
}
|
|
9465
10080
|
]
|
|
9466
10081
|
};
|
|
9467
10082
|
}
|
|
@@ -9484,7 +10099,10 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
9484
10099
|
]
|
|
9485
10100
|
};
|
|
9486
10101
|
}
|
|
9487
|
-
const lines = [
|
|
10102
|
+
const lines = [
|
|
10103
|
+
`YouTube Daily Analytics (${start_date} to ${end_date}):`,
|
|
10104
|
+
""
|
|
10105
|
+
];
|
|
9488
10106
|
for (const d of days) {
|
|
9489
10107
|
lines.push(
|
|
9490
10108
|
` ${d.date}: ${d.views.toLocaleString()} views, ${d.watchTimeMinutes.toLocaleString()} min watch, +${d.subscribersGained} subs, ${d.likes} likes, ${d.comments} comments`
|
|
@@ -9531,7 +10149,12 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
9531
10149
|
const videos = result.topVideos ?? [];
|
|
9532
10150
|
if (videos.length === 0) {
|
|
9533
10151
|
return {
|
|
9534
|
-
content: [
|
|
10152
|
+
content: [
|
|
10153
|
+
{
|
|
10154
|
+
type: "text",
|
|
10155
|
+
text: "No top videos found for this period."
|
|
10156
|
+
}
|
|
10157
|
+
]
|
|
9535
10158
|
};
|
|
9536
10159
|
}
|
|
9537
10160
|
if (format === "json") {
|
|
@@ -9553,7 +10176,10 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
9553
10176
|
]
|
|
9554
10177
|
};
|
|
9555
10178
|
}
|
|
9556
|
-
const lines = [
|
|
10179
|
+
const lines = [
|
|
10180
|
+
`Top ${videos.length} YouTube Videos (${start_date} to ${end_date}):`,
|
|
10181
|
+
""
|
|
10182
|
+
];
|
|
9557
10183
|
for (let i = 0; i < videos.length; i++) {
|
|
9558
10184
|
const v = videos[i];
|
|
9559
10185
|
lines.push(
|
|
@@ -9566,11 +10192,18 @@ function registerYouTubeAnalyticsTools(server) {
|
|
|
9566
10192
|
}
|
|
9567
10193
|
if (format === "json") {
|
|
9568
10194
|
return {
|
|
9569
|
-
content: [
|
|
10195
|
+
content: [
|
|
10196
|
+
{
|
|
10197
|
+
type: "text",
|
|
10198
|
+
text: JSON.stringify(asEnvelope7(result), null, 2)
|
|
10199
|
+
}
|
|
10200
|
+
]
|
|
9570
10201
|
};
|
|
9571
10202
|
}
|
|
9572
10203
|
return {
|
|
9573
|
-
content: [
|
|
10204
|
+
content: [
|
|
10205
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
10206
|
+
]
|
|
9574
10207
|
};
|
|
9575
10208
|
}
|
|
9576
10209
|
);
|
|
@@ -9579,6 +10212,8 @@ var init_youtube_analytics = __esm({
|
|
|
9579
10212
|
"src/tools/youtube-analytics.ts"() {
|
|
9580
10213
|
"use strict";
|
|
9581
10214
|
init_edge_function();
|
|
10215
|
+
init_supabase();
|
|
10216
|
+
init_connected_account_routing();
|
|
9582
10217
|
init_version();
|
|
9583
10218
|
}
|
|
9584
10219
|
});
|
|
@@ -9594,6 +10229,29 @@ function asEnvelope8(data) {
|
|
|
9594
10229
|
data
|
|
9595
10230
|
};
|
|
9596
10231
|
}
|
|
10232
|
+
async function exactYouTubeRoute(projectId, connectedAccountId) {
|
|
10233
|
+
const resolvedProjectId = projectId ?? await getDefaultProjectId() ?? void 0;
|
|
10234
|
+
if (!resolvedProjectId) {
|
|
10235
|
+
return {
|
|
10236
|
+
error: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
|
|
10237
|
+
};
|
|
10238
|
+
}
|
|
10239
|
+
const routing = await resolveConnectedAccountRouting({
|
|
10240
|
+
projectId: resolvedProjectId,
|
|
10241
|
+
platforms: ["youtube"],
|
|
10242
|
+
requestedAccountIds: connectedAccountId ? { youtube: connectedAccountId } : void 0
|
|
10243
|
+
});
|
|
10244
|
+
const resolvedAccountId = routing.connectedAccountIds?.YouTube;
|
|
10245
|
+
if (routing.error || !resolvedAccountId) {
|
|
10246
|
+
return {
|
|
10247
|
+
error: routing.error ?? "YouTube: exact connected-account routing could not be established."
|
|
10248
|
+
};
|
|
10249
|
+
}
|
|
10250
|
+
return {
|
|
10251
|
+
projectId: resolvedProjectId,
|
|
10252
|
+
connectedAccountId: resolvedAccountId
|
|
10253
|
+
};
|
|
10254
|
+
}
|
|
9597
10255
|
function registerCommentsTools(server) {
|
|
9598
10256
|
server.tool(
|
|
9599
10257
|
"list_comments",
|
|
@@ -9606,19 +10264,42 @@ function registerCommentsTools(server) {
|
|
|
9606
10264
|
page_token: z11.string().optional().describe(
|
|
9607
10265
|
"Pagination cursor from previous list_comments response nextPageToken field. Omit for first page of results."
|
|
9608
10266
|
),
|
|
10267
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
10268
|
+
"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."
|
|
10269
|
+
),
|
|
10270
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
9609
10271
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9610
10272
|
},
|
|
9611
|
-
async ({
|
|
10273
|
+
async ({
|
|
10274
|
+
video_id,
|
|
10275
|
+
max_results,
|
|
10276
|
+
page_token,
|
|
10277
|
+
connected_account_id,
|
|
10278
|
+
project_id,
|
|
10279
|
+
response_format
|
|
10280
|
+
}) => {
|
|
9612
10281
|
const format = response_format ?? "text";
|
|
10282
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
10283
|
+
if ("error" in route) {
|
|
10284
|
+
return {
|
|
10285
|
+
content: [{ type: "text", text: route.error }],
|
|
10286
|
+
isError: true
|
|
10287
|
+
};
|
|
10288
|
+
}
|
|
9613
10289
|
const { data, error } = await callEdgeFunction("youtube-comments", {
|
|
9614
10290
|
action: "list",
|
|
9615
10291
|
videoId: video_id,
|
|
9616
10292
|
maxResults: max_results ?? 50,
|
|
9617
|
-
pageToken: page_token
|
|
10293
|
+
pageToken: page_token,
|
|
10294
|
+
projectId: route.projectId,
|
|
10295
|
+
project_id: route.projectId,
|
|
10296
|
+
connectedAccountId: route.connectedAccountId
|
|
9618
10297
|
});
|
|
9619
10298
|
if (error) {
|
|
9620
10299
|
return {
|
|
9621
|
-
content: [
|
|
10300
|
+
content: [
|
|
10301
|
+
{ type: "text", text: `Error listing comments: ${error}` }
|
|
10302
|
+
],
|
|
9622
10303
|
isError: true
|
|
9623
10304
|
};
|
|
9624
10305
|
}
|
|
@@ -9630,7 +10311,10 @@ function registerCommentsTools(server) {
|
|
|
9630
10311
|
{
|
|
9631
10312
|
type: "text",
|
|
9632
10313
|
text: JSON.stringify(
|
|
9633
|
-
asEnvelope8({
|
|
10314
|
+
asEnvelope8({
|
|
10315
|
+
comments,
|
|
10316
|
+
nextPageToken: result.nextPageToken ?? null
|
|
10317
|
+
}),
|
|
9634
10318
|
null,
|
|
9635
10319
|
2
|
|
9636
10320
|
)
|
|
@@ -9667,12 +10351,31 @@ function registerCommentsTools(server) {
|
|
|
9667
10351
|
"reply_to_comment",
|
|
9668
10352
|
"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
10353
|
{
|
|
9670
|
-
parent_id: z11.string().describe(
|
|
10354
|
+
parent_id: z11.string().describe(
|
|
10355
|
+
"The ID of the parent comment to reply to (from list_comments)."
|
|
10356
|
+
),
|
|
9671
10357
|
text: z11.string().min(1).describe("The reply text."),
|
|
10358
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
10359
|
+
"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."
|
|
10360
|
+
),
|
|
10361
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
9672
10362
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9673
10363
|
},
|
|
9674
|
-
async ({
|
|
10364
|
+
async ({
|
|
10365
|
+
parent_id,
|
|
10366
|
+
text,
|
|
10367
|
+
connected_account_id,
|
|
10368
|
+
project_id,
|
|
10369
|
+
response_format
|
|
10370
|
+
}) => {
|
|
9675
10371
|
const format = response_format ?? "text";
|
|
10372
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
10373
|
+
if ("error" in route) {
|
|
10374
|
+
return {
|
|
10375
|
+
content: [{ type: "text", text: route.error }],
|
|
10376
|
+
isError: true
|
|
10377
|
+
};
|
|
10378
|
+
}
|
|
9676
10379
|
const userId = await getDefaultUserId();
|
|
9677
10380
|
const rateLimit = checkRateLimit("posting", `reply_to_comment:${userId}`);
|
|
9678
10381
|
if (!rateLimit.allowed) {
|
|
@@ -9689,18 +10392,31 @@ function registerCommentsTools(server) {
|
|
|
9689
10392
|
const { data, error } = await callEdgeFunction("youtube-comments", {
|
|
9690
10393
|
action: "reply",
|
|
9691
10394
|
parentId: parent_id,
|
|
9692
|
-
text
|
|
10395
|
+
text,
|
|
10396
|
+
projectId: route.projectId,
|
|
10397
|
+
project_id: route.projectId,
|
|
10398
|
+
connectedAccountId: route.connectedAccountId
|
|
9693
10399
|
});
|
|
9694
10400
|
if (error) {
|
|
9695
10401
|
return {
|
|
9696
|
-
content: [
|
|
10402
|
+
content: [
|
|
10403
|
+
{
|
|
10404
|
+
type: "text",
|
|
10405
|
+
text: `Error replying to comment: ${error}`
|
|
10406
|
+
}
|
|
10407
|
+
],
|
|
9697
10408
|
isError: true
|
|
9698
10409
|
};
|
|
9699
10410
|
}
|
|
9700
10411
|
const result = data;
|
|
9701
10412
|
if (format === "json") {
|
|
9702
10413
|
return {
|
|
9703
|
-
content: [
|
|
10414
|
+
content: [
|
|
10415
|
+
{
|
|
10416
|
+
type: "text",
|
|
10417
|
+
text: JSON.stringify(asEnvelope8(result), null, 2)
|
|
10418
|
+
}
|
|
10419
|
+
]
|
|
9704
10420
|
};
|
|
9705
10421
|
}
|
|
9706
10422
|
return {
|
|
@@ -9721,10 +10437,27 @@ function registerCommentsTools(server) {
|
|
|
9721
10437
|
{
|
|
9722
10438
|
video_id: z11.string().describe("The YouTube video ID to comment on."),
|
|
9723
10439
|
text: z11.string().min(1).describe("The comment text."),
|
|
10440
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
10441
|
+
"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."
|
|
10442
|
+
),
|
|
10443
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
9724
10444
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9725
10445
|
},
|
|
9726
|
-
async ({
|
|
10446
|
+
async ({
|
|
10447
|
+
video_id,
|
|
10448
|
+
text,
|
|
10449
|
+
connected_account_id,
|
|
10450
|
+
project_id,
|
|
10451
|
+
response_format
|
|
10452
|
+
}) => {
|
|
9727
10453
|
const format = response_format ?? "text";
|
|
10454
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
10455
|
+
if ("error" in route) {
|
|
10456
|
+
return {
|
|
10457
|
+
content: [{ type: "text", text: route.error }],
|
|
10458
|
+
isError: true
|
|
10459
|
+
};
|
|
10460
|
+
}
|
|
9728
10461
|
const userId = await getDefaultUserId();
|
|
9729
10462
|
const rateLimit = checkRateLimit("posting", `post_comment:${userId}`);
|
|
9730
10463
|
if (!rateLimit.allowed) {
|
|
@@ -9741,18 +10474,28 @@ function registerCommentsTools(server) {
|
|
|
9741
10474
|
const { data, error } = await callEdgeFunction("youtube-comments", {
|
|
9742
10475
|
action: "post",
|
|
9743
10476
|
videoId: video_id,
|
|
9744
|
-
text
|
|
10477
|
+
text,
|
|
10478
|
+
projectId: route.projectId,
|
|
10479
|
+
project_id: route.projectId,
|
|
10480
|
+
connectedAccountId: route.connectedAccountId
|
|
9745
10481
|
});
|
|
9746
10482
|
if (error) {
|
|
9747
10483
|
return {
|
|
9748
|
-
content: [
|
|
10484
|
+
content: [
|
|
10485
|
+
{ type: "text", text: `Error posting comment: ${error}` }
|
|
10486
|
+
],
|
|
9749
10487
|
isError: true
|
|
9750
10488
|
};
|
|
9751
10489
|
}
|
|
9752
10490
|
const result = data;
|
|
9753
10491
|
if (format === "json") {
|
|
9754
10492
|
return {
|
|
9755
|
-
content: [
|
|
10493
|
+
content: [
|
|
10494
|
+
{
|
|
10495
|
+
type: "text",
|
|
10496
|
+
text: JSON.stringify(asEnvelope8(result), null, 2)
|
|
10497
|
+
}
|
|
10498
|
+
]
|
|
9756
10499
|
};
|
|
9757
10500
|
}
|
|
9758
10501
|
return {
|
|
@@ -9773,10 +10516,27 @@ function registerCommentsTools(server) {
|
|
|
9773
10516
|
{
|
|
9774
10517
|
comment_id: z11.string().describe("The comment ID to moderate."),
|
|
9775
10518
|
moderation_status: z11.enum(["published", "rejected"]).describe('"published" to approve, "rejected" to hide.'),
|
|
10519
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
10520
|
+
"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."
|
|
10521
|
+
),
|
|
10522
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
9776
10523
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9777
10524
|
},
|
|
9778
|
-
async ({
|
|
10525
|
+
async ({
|
|
10526
|
+
comment_id,
|
|
10527
|
+
moderation_status,
|
|
10528
|
+
connected_account_id,
|
|
10529
|
+
project_id,
|
|
10530
|
+
response_format
|
|
10531
|
+
}) => {
|
|
9779
10532
|
const format = response_format ?? "text";
|
|
10533
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
10534
|
+
if ("error" in route) {
|
|
10535
|
+
return {
|
|
10536
|
+
content: [{ type: "text", text: route.error }],
|
|
10537
|
+
isError: true
|
|
10538
|
+
};
|
|
10539
|
+
}
|
|
9780
10540
|
const userId = await getDefaultUserId();
|
|
9781
10541
|
const rateLimit = checkRateLimit("posting", `moderate_comment:${userId}`);
|
|
9782
10542
|
if (!rateLimit.allowed) {
|
|
@@ -9793,11 +10553,19 @@ function registerCommentsTools(server) {
|
|
|
9793
10553
|
const { error } = await callEdgeFunction("youtube-comments", {
|
|
9794
10554
|
action: "moderate",
|
|
9795
10555
|
commentId: comment_id,
|
|
9796
|
-
moderationStatus: moderation_status
|
|
10556
|
+
moderationStatus: moderation_status,
|
|
10557
|
+
projectId: route.projectId,
|
|
10558
|
+
project_id: route.projectId,
|
|
10559
|
+
connectedAccountId: route.connectedAccountId
|
|
9797
10560
|
});
|
|
9798
10561
|
if (error) {
|
|
9799
10562
|
return {
|
|
9800
|
-
content: [
|
|
10563
|
+
content: [
|
|
10564
|
+
{
|
|
10565
|
+
type: "text",
|
|
10566
|
+
text: `Error moderating comment: ${error}`
|
|
10567
|
+
}
|
|
10568
|
+
],
|
|
9801
10569
|
isError: true
|
|
9802
10570
|
};
|
|
9803
10571
|
}
|
|
@@ -9834,10 +10602,26 @@ function registerCommentsTools(server) {
|
|
|
9834
10602
|
"Delete a YouTube comment. Only works for comments owned by the authenticated channel.",
|
|
9835
10603
|
{
|
|
9836
10604
|
comment_id: z11.string().describe("The comment ID to delete."),
|
|
10605
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
10606
|
+
"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."
|
|
10607
|
+
),
|
|
10608
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
9837
10609
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9838
10610
|
},
|
|
9839
|
-
async ({
|
|
10611
|
+
async ({
|
|
10612
|
+
comment_id,
|
|
10613
|
+
connected_account_id,
|
|
10614
|
+
project_id,
|
|
10615
|
+
response_format
|
|
10616
|
+
}) => {
|
|
9840
10617
|
const format = response_format ?? "text";
|
|
10618
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
10619
|
+
if ("error" in route) {
|
|
10620
|
+
return {
|
|
10621
|
+
content: [{ type: "text", text: route.error }],
|
|
10622
|
+
isError: true
|
|
10623
|
+
};
|
|
10624
|
+
}
|
|
9841
10625
|
const userId = await getDefaultUserId();
|
|
9842
10626
|
const rateLimit = checkRateLimit("posting", `delete_comment:${userId}`);
|
|
9843
10627
|
if (!rateLimit.allowed) {
|
|
@@ -9853,11 +10637,16 @@ function registerCommentsTools(server) {
|
|
|
9853
10637
|
}
|
|
9854
10638
|
const { error } = await callEdgeFunction("youtube-comments", {
|
|
9855
10639
|
action: "delete",
|
|
9856
|
-
commentId: comment_id
|
|
10640
|
+
commentId: comment_id,
|
|
10641
|
+
projectId: route.projectId,
|
|
10642
|
+
project_id: route.projectId,
|
|
10643
|
+
connectedAccountId: route.connectedAccountId
|
|
9857
10644
|
});
|
|
9858
10645
|
if (error) {
|
|
9859
10646
|
return {
|
|
9860
|
-
content: [
|
|
10647
|
+
content: [
|
|
10648
|
+
{ type: "text", text: `Error deleting comment: ${error}` }
|
|
10649
|
+
],
|
|
9861
10650
|
isError: true
|
|
9862
10651
|
};
|
|
9863
10652
|
}
|
|
@@ -9866,24 +10655,38 @@ function registerCommentsTools(server) {
|
|
|
9866
10655
|
content: [
|
|
9867
10656
|
{
|
|
9868
10657
|
type: "text",
|
|
9869
|
-
text: JSON.stringify(
|
|
10658
|
+
text: JSON.stringify(
|
|
10659
|
+
asEnvelope8({ success: true, commentId: comment_id }),
|
|
10660
|
+
null,
|
|
10661
|
+
2
|
|
10662
|
+
)
|
|
9870
10663
|
}
|
|
9871
10664
|
]
|
|
9872
10665
|
};
|
|
9873
10666
|
}
|
|
9874
10667
|
return {
|
|
9875
|
-
content: [
|
|
10668
|
+
content: [
|
|
10669
|
+
{
|
|
10670
|
+
type: "text",
|
|
10671
|
+
text: `Comment ${comment_id} deleted successfully.`
|
|
10672
|
+
}
|
|
10673
|
+
]
|
|
9876
10674
|
};
|
|
9877
10675
|
}
|
|
9878
10676
|
);
|
|
9879
10677
|
}
|
|
10678
|
+
var PROJECT_ID_SCHEMA;
|
|
9880
10679
|
var init_comments = __esm({
|
|
9881
10680
|
"src/tools/comments.ts"() {
|
|
9882
10681
|
"use strict";
|
|
9883
10682
|
init_edge_function();
|
|
9884
10683
|
init_rate_limit();
|
|
9885
10684
|
init_supabase();
|
|
10685
|
+
init_connected_account_routing();
|
|
9886
10686
|
init_version();
|
|
10687
|
+
PROJECT_ID_SCHEMA = z11.string().uuid().optional().describe(
|
|
10688
|
+
"Exact brand/project ID. Defaults only when the authenticated user has one project."
|
|
10689
|
+
);
|
|
9887
10690
|
}
|
|
9888
10691
|
});
|
|
9889
10692
|
|
|
@@ -12876,7 +13679,10 @@ var init_discovery2 = __esm({
|
|
|
12876
13679
|
import { z as z24 } from "zod";
|
|
12877
13680
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
12878
13681
|
function asEnvelope20(data) {
|
|
12879
|
-
return {
|
|
13682
|
+
return {
|
|
13683
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
13684
|
+
data
|
|
13685
|
+
};
|
|
12880
13686
|
}
|
|
12881
13687
|
function registerPipelineTools(server) {
|
|
12882
13688
|
server.tool(
|
|
@@ -12899,7 +13705,10 @@ function registerPipelineTools(server) {
|
|
|
12899
13705
|
action: "pipeline-readiness",
|
|
12900
13706
|
platforms,
|
|
12901
13707
|
estimated_posts,
|
|
12902
|
-
...resolvedProjectId ? {
|
|
13708
|
+
...resolvedProjectId ? {
|
|
13709
|
+
projectId: resolvedProjectId,
|
|
13710
|
+
project_id: resolvedProjectId
|
|
13711
|
+
} : {}
|
|
12903
13712
|
},
|
|
12904
13713
|
{ timeoutMs: 15e3 }
|
|
12905
13714
|
);
|
|
@@ -12917,16 +13726,24 @@ function registerPipelineTools(server) {
|
|
|
12917
13726
|
const blockers = [];
|
|
12918
13727
|
const warnings = [];
|
|
12919
13728
|
if (!isUnlimited && credits < estimatedCost) {
|
|
12920
|
-
blockers.push(
|
|
13729
|
+
blockers.push(
|
|
13730
|
+
`Insufficient credits: ${credits} available, ~${estimatedCost} needed`
|
|
13731
|
+
);
|
|
12921
13732
|
}
|
|
12922
13733
|
if (missingPlatforms.length > 0) {
|
|
12923
|
-
blockers.push(
|
|
13734
|
+
blockers.push(
|
|
13735
|
+
`Missing connected accounts: ${missingPlatforms.join(", ")}`
|
|
13736
|
+
);
|
|
12924
13737
|
}
|
|
12925
13738
|
if (!hasBrand) {
|
|
12926
|
-
warnings.push(
|
|
13739
|
+
warnings.push(
|
|
13740
|
+
"No brand profile found. Content will use generic voice."
|
|
13741
|
+
);
|
|
12927
13742
|
}
|
|
12928
13743
|
if (pendingApprovals > 0) {
|
|
12929
|
-
warnings.push(
|
|
13744
|
+
warnings.push(
|
|
13745
|
+
`${pendingApprovals} pending approval(s) from previous runs.`
|
|
13746
|
+
);
|
|
12930
13747
|
}
|
|
12931
13748
|
if (!insightsFresh) {
|
|
12932
13749
|
warnings.push(
|
|
@@ -12941,7 +13758,10 @@ function registerPipelineTools(server) {
|
|
|
12941
13758
|
estimated_cost: estimatedCost,
|
|
12942
13759
|
sufficient: credits >= estimatedCost
|
|
12943
13760
|
},
|
|
12944
|
-
connected_accounts: {
|
|
13761
|
+
connected_accounts: {
|
|
13762
|
+
platforms: connectedPlatforms,
|
|
13763
|
+
missing: missingPlatforms
|
|
13764
|
+
},
|
|
12945
13765
|
brand_profile: { exists: hasBrand },
|
|
12946
13766
|
pending_approvals: { count: pendingApprovals },
|
|
12947
13767
|
insights_available: {
|
|
@@ -12955,11 +13775,18 @@ function registerPipelineTools(server) {
|
|
|
12955
13775
|
};
|
|
12956
13776
|
if (format === "json") {
|
|
12957
13777
|
return {
|
|
12958
|
-
content: [
|
|
13778
|
+
content: [
|
|
13779
|
+
{
|
|
13780
|
+
type: "text",
|
|
13781
|
+
text: JSON.stringify(asEnvelope20(result), null, 2)
|
|
13782
|
+
}
|
|
13783
|
+
]
|
|
12959
13784
|
};
|
|
12960
13785
|
}
|
|
12961
13786
|
const lines = [];
|
|
12962
|
-
lines.push(
|
|
13787
|
+
lines.push(
|
|
13788
|
+
`Pipeline Readiness: ${result.ready ? "READY" : "NOT READY"}`
|
|
13789
|
+
);
|
|
12963
13790
|
lines.push("=".repeat(40));
|
|
12964
13791
|
lines.push(
|
|
12965
13792
|
`Credits: ${credits} available, ~${estimatedCost} needed \u2014 ${credits >= estimatedCost ? "OK" : "BLOCKED"}`
|
|
@@ -12967,7 +13794,9 @@ function registerPipelineTools(server) {
|
|
|
12967
13794
|
lines.push(
|
|
12968
13795
|
`Accounts: ${connectedPlatforms.length} connected${missingPlatforms.length > 0 ? ` (missing: ${missingPlatforms.join(", ")})` : " \u2014 OK"}`
|
|
12969
13796
|
);
|
|
12970
|
-
lines.push(
|
|
13797
|
+
lines.push(
|
|
13798
|
+
`Brand: ${hasBrand ? "OK" : "Missing (will use generic voice)"}`
|
|
13799
|
+
);
|
|
12971
13800
|
lines.push(`Pending Approvals: ${pendingApprovals}`);
|
|
12972
13801
|
lines.push(
|
|
12973
13802
|
`Insights: ${insightsFresh ? "Fresh" : insightAge === null ? "None available" : `${insightAge} days old`}`
|
|
@@ -12986,7 +13815,12 @@ function registerPipelineTools(server) {
|
|
|
12986
13815
|
} catch (err) {
|
|
12987
13816
|
const message = sanitizeError(err);
|
|
12988
13817
|
return {
|
|
12989
|
-
content: [
|
|
13818
|
+
content: [
|
|
13819
|
+
{
|
|
13820
|
+
type: "text",
|
|
13821
|
+
text: `Readiness check failed: ${message}`
|
|
13822
|
+
}
|
|
13823
|
+
],
|
|
12990
13824
|
isError: true
|
|
12991
13825
|
};
|
|
12992
13826
|
}
|
|
@@ -13000,6 +13834,9 @@ function registerPipelineTools(server) {
|
|
|
13000
13834
|
topic: z24.string().optional().describe("Content topic (required if no source_url)"),
|
|
13001
13835
|
source_url: z24.string().optional().describe("URL to extract content from"),
|
|
13002
13836
|
platforms: z24.array(PLATFORM_ENUM2).min(1).describe("Target platforms"),
|
|
13837
|
+
account_ids: z24.record(z24.string(), z24.string().uuid()).optional().describe(
|
|
13838
|
+
"Exact connected-account ID per target platform. Required when a project has multiple accounts on one platform."
|
|
13839
|
+
),
|
|
13003
13840
|
days: z24.number().min(1).max(7).default(5).describe("Days to plan"),
|
|
13004
13841
|
posts_per_day: z24.number().min(1).max(3).default(1).describe("Posts per platform per day"),
|
|
13005
13842
|
approval_mode: z24.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe(
|
|
@@ -13021,6 +13858,7 @@ function registerPipelineTools(server) {
|
|
|
13021
13858
|
topic,
|
|
13022
13859
|
source_url,
|
|
13023
13860
|
platforms,
|
|
13861
|
+
account_ids,
|
|
13024
13862
|
days,
|
|
13025
13863
|
posts_per_day,
|
|
13026
13864
|
approval_mode,
|
|
@@ -13038,7 +13876,12 @@ function registerPipelineTools(server) {
|
|
|
13038
13876
|
let creditsUsed = 0;
|
|
13039
13877
|
if (!topic && !source_url) {
|
|
13040
13878
|
return {
|
|
13041
|
-
content: [
|
|
13879
|
+
content: [
|
|
13880
|
+
{
|
|
13881
|
+
type: "text",
|
|
13882
|
+
text: "Either topic or source_url is required."
|
|
13883
|
+
}
|
|
13884
|
+
],
|
|
13042
13885
|
isError: true
|
|
13043
13886
|
};
|
|
13044
13887
|
}
|
|
@@ -13068,6 +13911,35 @@ function registerPipelineTools(server) {
|
|
|
13068
13911
|
}
|
|
13069
13912
|
try {
|
|
13070
13913
|
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
13914
|
+
if (schedulingRequested && !resolvedProjectId) {
|
|
13915
|
+
return {
|
|
13916
|
+
content: [
|
|
13917
|
+
{
|
|
13918
|
+
type: "text",
|
|
13919
|
+
text: "A project_id is required to schedule pipeline output. Configure an explicit project or use an API key scoped to exactly one project."
|
|
13920
|
+
}
|
|
13921
|
+
],
|
|
13922
|
+
isError: true
|
|
13923
|
+
};
|
|
13924
|
+
}
|
|
13925
|
+
if (schedulingRequested) {
|
|
13926
|
+
const preflightRouting = await resolveConnectedAccountRouting({
|
|
13927
|
+
projectId: resolvedProjectId,
|
|
13928
|
+
platforms,
|
|
13929
|
+
requestedAccountIds: account_ids
|
|
13930
|
+
});
|
|
13931
|
+
if (preflightRouting.error || !preflightRouting.connectedAccountIds) {
|
|
13932
|
+
return {
|
|
13933
|
+
content: [
|
|
13934
|
+
{
|
|
13935
|
+
type: "text",
|
|
13936
|
+
text: `Cannot run publishing pipeline \u2014 ${preflightRouting.error ?? "exact connected-account routing could not be established."} (checked before any credits were spent.)`
|
|
13937
|
+
}
|
|
13938
|
+
],
|
|
13939
|
+
isError: true
|
|
13940
|
+
};
|
|
13941
|
+
}
|
|
13942
|
+
}
|
|
13071
13943
|
const estimatedCost = BASE_PLAN_CREDITS + (source_url ? SOURCE_EXTRACTION_CREDITS : 0);
|
|
13072
13944
|
const { data: budgetData } = await callEdgeFunction(
|
|
13073
13945
|
"mcp-data",
|
|
@@ -13121,7 +13993,13 @@ function registerPipelineTools(server) {
|
|
|
13121
13993
|
"social-neuron-ai",
|
|
13122
13994
|
{
|
|
13123
13995
|
type: "generation",
|
|
13124
|
-
prompt: buildPlanPrompt(
|
|
13996
|
+
prompt: buildPlanPrompt(
|
|
13997
|
+
resolvedTopic,
|
|
13998
|
+
platforms,
|
|
13999
|
+
days,
|
|
14000
|
+
posts_per_day,
|
|
14001
|
+
source_url
|
|
14002
|
+
),
|
|
13125
14003
|
model: "gemini-2.5-flash",
|
|
13126
14004
|
responseFormat: "json",
|
|
13127
14005
|
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
@@ -13129,7 +14007,10 @@ function registerPipelineTools(server) {
|
|
|
13129
14007
|
{ timeoutMs: 6e4 }
|
|
13130
14008
|
);
|
|
13131
14009
|
if (planError || !planData) {
|
|
13132
|
-
errors.push({
|
|
14010
|
+
errors.push({
|
|
14011
|
+
stage: "planning",
|
|
14012
|
+
message: planError ?? "No AI response"
|
|
14013
|
+
});
|
|
13133
14014
|
await callEdgeFunction(
|
|
13134
14015
|
"mcp-data",
|
|
13135
14016
|
{
|
|
@@ -13146,7 +14027,10 @@ function registerPipelineTools(server) {
|
|
|
13146
14027
|
);
|
|
13147
14028
|
return {
|
|
13148
14029
|
content: [
|
|
13149
|
-
{
|
|
14030
|
+
{
|
|
14031
|
+
type: "text",
|
|
14032
|
+
text: `Planning failed: ${planError ?? "No AI response"}`
|
|
14033
|
+
}
|
|
13150
14034
|
],
|
|
13151
14035
|
isError: true
|
|
13152
14036
|
};
|
|
@@ -13176,20 +14060,22 @@ function registerPipelineTools(server) {
|
|
|
13176
14060
|
const postsArray = extractJsonArray(rawText);
|
|
13177
14061
|
const requestedPlatformSet = new Set(platforms);
|
|
13178
14062
|
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
|
-
|
|
14063
|
+
const parsedPosts = (postsArray ?? []).map(
|
|
14064
|
+
(p) => ({
|
|
14065
|
+
id: String(p.id ?? randomUUID3().slice(0, 8)),
|
|
14066
|
+
day: Number(p.day ?? 1),
|
|
14067
|
+
date: String(p.date ?? ""),
|
|
14068
|
+
platform: String(p.platform ?? ""),
|
|
14069
|
+
content_type: p.content_type ?? "caption",
|
|
14070
|
+
caption: String(p.caption ?? ""),
|
|
14071
|
+
title: p.title ? String(p.title) : void 0,
|
|
14072
|
+
hashtags: Array.isArray(p.hashtags) ? p.hashtags.map(String) : void 0,
|
|
14073
|
+
hook: String(p.hook ?? ""),
|
|
14074
|
+
angle: String(p.angle ?? ""),
|
|
14075
|
+
visual_direction: p.visual_direction ? String(p.visual_direction) : void 0,
|
|
14076
|
+
media_type: p.media_type ? String(p.media_type) : void 0
|
|
14077
|
+
})
|
|
14078
|
+
);
|
|
13193
14079
|
const platformFilteredPosts = parsedPosts.filter(
|
|
13194
14080
|
(post) => requestedPlatformSet.has(post.platform)
|
|
13195
14081
|
);
|
|
@@ -13272,7 +14158,10 @@ function registerPipelineTools(server) {
|
|
|
13272
14158
|
estimated_credits: estimatedCost,
|
|
13273
14159
|
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
13274
14160
|
},
|
|
13275
|
-
...resolvedProjectId ? {
|
|
14161
|
+
...resolvedProjectId ? {
|
|
14162
|
+
projectId: resolvedProjectId,
|
|
14163
|
+
project_id: resolvedProjectId
|
|
14164
|
+
} : {}
|
|
13276
14165
|
},
|
|
13277
14166
|
{ timeoutMs: 1e4 }
|
|
13278
14167
|
);
|
|
@@ -13325,6 +14214,22 @@ function registerPipelineTools(server) {
|
|
|
13325
14214
|
}
|
|
13326
14215
|
}
|
|
13327
14216
|
let postsScheduled = 0;
|
|
14217
|
+
const pipelineRouting = schedulingRequested && postsApproved > 0 ? await resolveConnectedAccountRouting({
|
|
14218
|
+
projectId: resolvedProjectId,
|
|
14219
|
+
platforms,
|
|
14220
|
+
requestedAccountIds: account_ids
|
|
14221
|
+
}) : void 0;
|
|
14222
|
+
if (pipelineRouting?.error || schedulingRequested && postsApproved > 0 && !pipelineRouting?.connectedAccountIds) {
|
|
14223
|
+
return {
|
|
14224
|
+
content: [
|
|
14225
|
+
{
|
|
14226
|
+
type: "text",
|
|
14227
|
+
text: `Cannot run publishing pipeline \u2014 ${pipelineRouting?.error ?? "exact connected-account routing could not be established."}`
|
|
14228
|
+
}
|
|
14229
|
+
],
|
|
14230
|
+
isError: true
|
|
14231
|
+
};
|
|
14232
|
+
}
|
|
13328
14233
|
if (!dry_run && !skipSet.has("schedule") && postsApproved > 0) {
|
|
13329
14234
|
const approvedPosts = posts.filter((p) => p.status === "approved");
|
|
13330
14235
|
const scheduleBase = /* @__PURE__ */ new Date();
|
|
@@ -13332,10 +14237,27 @@ function registerPipelineTools(server) {
|
|
|
13332
14237
|
const scheduleBaseMs = scheduleBase.getTime();
|
|
13333
14238
|
for (const post of approvedPosts) {
|
|
13334
14239
|
if (creditsUsed >= creditLimit) {
|
|
13335
|
-
errors.push({
|
|
14240
|
+
errors.push({
|
|
14241
|
+
stage: "schedule",
|
|
14242
|
+
message: "Credit limit reached"
|
|
14243
|
+
});
|
|
13336
14244
|
break;
|
|
13337
14245
|
}
|
|
13338
|
-
const scheduledAt = post.schedule_at ?? new Date(
|
|
14246
|
+
const scheduledAt = post.schedule_at ?? new Date(
|
|
14247
|
+
scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5
|
|
14248
|
+
).toISOString();
|
|
14249
|
+
const route = Object.entries(
|
|
14250
|
+
pipelineRouting.connectedAccountIds
|
|
14251
|
+
).find(
|
|
14252
|
+
([platform3]) => platform3.toLowerCase() === post.platform.toLowerCase()
|
|
14253
|
+
);
|
|
14254
|
+
if (!route) {
|
|
14255
|
+
errors.push({
|
|
14256
|
+
stage: "schedule",
|
|
14257
|
+
message: `No verified connected account route for ${post.platform}`
|
|
14258
|
+
});
|
|
14259
|
+
continue;
|
|
14260
|
+
}
|
|
13339
14261
|
try {
|
|
13340
14262
|
const { error: schedError } = await callEdgeFunction(
|
|
13341
14263
|
"schedule-post",
|
|
@@ -13348,7 +14270,11 @@ function registerPipelineTools(server) {
|
|
|
13348
14270
|
scheduledAt,
|
|
13349
14271
|
planId,
|
|
13350
14272
|
idempotencyKey: `pipeline-${planId}-${post.id}`,
|
|
13351
|
-
...resolvedProjectId ? {
|
|
14273
|
+
...resolvedProjectId ? {
|
|
14274
|
+
projectId: resolvedProjectId,
|
|
14275
|
+
project_id: resolvedProjectId
|
|
14276
|
+
} : {},
|
|
14277
|
+
connectedAccountIds: { [route[0]]: route[1] }
|
|
13352
14278
|
},
|
|
13353
14279
|
{ timeoutMs: 15e3 }
|
|
13354
14280
|
);
|
|
@@ -13414,12 +14340,17 @@ function registerPipelineTools(server) {
|
|
|
13414
14340
|
if (response_format === "json") {
|
|
13415
14341
|
return {
|
|
13416
14342
|
content: [
|
|
13417
|
-
{
|
|
14343
|
+
{
|
|
14344
|
+
type: "text",
|
|
14345
|
+
text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
|
|
14346
|
+
}
|
|
13418
14347
|
]
|
|
13419
14348
|
};
|
|
13420
14349
|
}
|
|
13421
14350
|
const lines = [];
|
|
13422
|
-
lines.push(
|
|
14351
|
+
lines.push(
|
|
14352
|
+
`Pipeline ${pipelineId.slice(0, 8)}... ${finalStatus.toUpperCase()}`
|
|
14353
|
+
);
|
|
13423
14354
|
lines.push("=".repeat(40));
|
|
13424
14355
|
lines.push(`Posts generated: ${posts.length}`);
|
|
13425
14356
|
lines.push(`Posts approved: ${postsApproved}`);
|
|
@@ -13458,7 +14389,9 @@ function registerPipelineTools(server) {
|
|
|
13458
14389
|
} catch {
|
|
13459
14390
|
}
|
|
13460
14391
|
return {
|
|
13461
|
-
content: [
|
|
14392
|
+
content: [
|
|
14393
|
+
{ type: "text", text: `Pipeline failed: ${message}` }
|
|
14394
|
+
],
|
|
13462
14395
|
isError: true
|
|
13463
14396
|
};
|
|
13464
14397
|
}
|
|
@@ -13495,7 +14428,12 @@ function registerPipelineTools(server) {
|
|
|
13495
14428
|
}
|
|
13496
14429
|
if (format === "json") {
|
|
13497
14430
|
return {
|
|
13498
|
-
content: [
|
|
14431
|
+
content: [
|
|
14432
|
+
{
|
|
14433
|
+
type: "text",
|
|
14434
|
+
text: JSON.stringify(asEnvelope20(data), null, 2)
|
|
14435
|
+
}
|
|
14436
|
+
]
|
|
13499
14437
|
};
|
|
13500
14438
|
}
|
|
13501
14439
|
const lines = [];
|
|
@@ -13535,10 +14473,19 @@ function registerPipelineTools(server) {
|
|
|
13535
14473
|
},
|
|
13536
14474
|
async ({ plan_id, quality_threshold, response_format }) => {
|
|
13537
14475
|
try {
|
|
13538
|
-
const { data: loadResult, error: loadError } = await callEdgeFunction(
|
|
14476
|
+
const { data: loadResult, error: loadError } = await callEdgeFunction(
|
|
14477
|
+
"mcp-data",
|
|
14478
|
+
{ action: "auto-approve-plan", plan_id },
|
|
14479
|
+
{ timeoutMs: 1e4 }
|
|
14480
|
+
);
|
|
13539
14481
|
if (loadError) {
|
|
13540
14482
|
return {
|
|
13541
|
-
content: [
|
|
14483
|
+
content: [
|
|
14484
|
+
{
|
|
14485
|
+
type: "text",
|
|
14486
|
+
text: `Failed to load plan: ${loadError}`
|
|
14487
|
+
}
|
|
14488
|
+
],
|
|
13542
14489
|
isError: true
|
|
13543
14490
|
};
|
|
13544
14491
|
}
|
|
@@ -13546,7 +14493,10 @@ function registerPipelineTools(server) {
|
|
|
13546
14493
|
if (!stored?.plan_payload) {
|
|
13547
14494
|
return {
|
|
13548
14495
|
content: [
|
|
13549
|
-
{
|
|
14496
|
+
{
|
|
14497
|
+
type: "text",
|
|
14498
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
14499
|
+
}
|
|
13550
14500
|
],
|
|
13551
14501
|
isError: true
|
|
13552
14502
|
};
|
|
@@ -13573,7 +14523,11 @@ function registerPipelineTools(server) {
|
|
|
13573
14523
|
blockers: []
|
|
13574
14524
|
};
|
|
13575
14525
|
autoApproved++;
|
|
13576
|
-
details.push({
|
|
14526
|
+
details.push({
|
|
14527
|
+
post_id: post.id,
|
|
14528
|
+
action: "approved",
|
|
14529
|
+
score: quality.total
|
|
14530
|
+
});
|
|
13577
14531
|
} else if (quality.total >= quality_threshold - 5) {
|
|
13578
14532
|
post.status = "needs_edit";
|
|
13579
14533
|
post.quality = {
|
|
@@ -13583,7 +14537,11 @@ function registerPipelineTools(server) {
|
|
|
13583
14537
|
blockers: quality.blockers
|
|
13584
14538
|
};
|
|
13585
14539
|
flagged++;
|
|
13586
|
-
details.push({
|
|
14540
|
+
details.push({
|
|
14541
|
+
post_id: post.id,
|
|
14542
|
+
action: "flagged",
|
|
14543
|
+
score: quality.total
|
|
14544
|
+
});
|
|
13587
14545
|
} else {
|
|
13588
14546
|
post.status = "rejected";
|
|
13589
14547
|
post.quality = {
|
|
@@ -13593,7 +14551,11 @@ function registerPipelineTools(server) {
|
|
|
13593
14551
|
blockers: quality.blockers
|
|
13594
14552
|
};
|
|
13595
14553
|
rejected++;
|
|
13596
|
-
details.push({
|
|
14554
|
+
details.push({
|
|
14555
|
+
post_id: post.id,
|
|
14556
|
+
action: "rejected",
|
|
14557
|
+
score: quality.total
|
|
14558
|
+
});
|
|
13597
14559
|
}
|
|
13598
14560
|
}
|
|
13599
14561
|
const newStatus = flagged === 0 && rejected === 0 ? "approved" : "in_review";
|
|
@@ -13629,7 +14591,10 @@ function registerPipelineTools(server) {
|
|
|
13629
14591
|
if (response_format === "json") {
|
|
13630
14592
|
return {
|
|
13631
14593
|
content: [
|
|
13632
|
-
{
|
|
14594
|
+
{
|
|
14595
|
+
type: "text",
|
|
14596
|
+
text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
|
|
14597
|
+
}
|
|
13633
14598
|
]
|
|
13634
14599
|
};
|
|
13635
14600
|
}
|
|
@@ -13650,7 +14615,9 @@ function registerPipelineTools(server) {
|
|
|
13650
14615
|
} catch (err) {
|
|
13651
14616
|
const message = sanitizeError(err);
|
|
13652
14617
|
return {
|
|
13653
|
-
content: [
|
|
14618
|
+
content: [
|
|
14619
|
+
{ type: "text", text: `Auto-approve failed: ${message}` }
|
|
14620
|
+
],
|
|
13654
14621
|
isError: true
|
|
13655
14622
|
};
|
|
13656
14623
|
}
|
|
@@ -13684,6 +14651,7 @@ var init_pipeline = __esm({
|
|
|
13684
14651
|
init_quality();
|
|
13685
14652
|
init_version();
|
|
13686
14653
|
init_parse_utils();
|
|
14654
|
+
init_connected_account_routing();
|
|
13687
14655
|
PLATFORM_ENUM2 = z24.enum([
|
|
13688
14656
|
"youtube",
|
|
13689
14657
|
"tiktok",
|
|
@@ -16348,28 +17316,33 @@ var init_analytics_pulse = __esm({
|
|
|
16348
17316
|
|
|
16349
17317
|
// src/tools/connections.ts
|
|
16350
17318
|
import { z as z33 } from "zod";
|
|
16351
|
-
function
|
|
17319
|
+
function findActiveAccounts(accounts, platform3, projectId) {
|
|
16352
17320
|
const target = platform3.toLowerCase();
|
|
16353
|
-
return accounts.
|
|
17321
|
+
return accounts.filter((a) => {
|
|
16354
17322
|
const effectiveStatus = a.effective_status || a.status;
|
|
16355
|
-
return a.platform.toLowerCase() === target && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
|
|
16356
|
-
})
|
|
17323
|
+
return a.platform.toLowerCase() === target && a.project_id === projectId && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
|
|
17324
|
+
});
|
|
16357
17325
|
}
|
|
16358
17326
|
function registerConnectionTools(server) {
|
|
16359
17327
|
server.tool(
|
|
16360
17328
|
"start_platform_connection",
|
|
16361
17329
|
"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
17330
|
{
|
|
16363
|
-
platform: z33.enum(PLATFORM_ENUM4).describe(
|
|
16364
|
-
|
|
16365
|
-
|
|
17331
|
+
platform: z33.enum(PLATFORM_ENUM4).describe(
|
|
17332
|
+
"Platform to connect. Lower-case: instagram, tiktok, youtube, etc."
|
|
17333
|
+
),
|
|
17334
|
+
project_id: z33.string().uuid().optional().describe(
|
|
17335
|
+
"Brand/project ID to bind the new social account to. Required when the account has multiple brands."
|
|
16366
17336
|
),
|
|
16367
17337
|
response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
16368
17338
|
},
|
|
16369
17339
|
async ({ platform: platform3, project_id, response_format }) => {
|
|
16370
17340
|
const format = response_format ?? "text";
|
|
16371
17341
|
const userId = await getDefaultUserId();
|
|
16372
|
-
const rl = checkRateLimit(
|
|
17342
|
+
const rl = checkRateLimit(
|
|
17343
|
+
"posting",
|
|
17344
|
+
`start_platform_connection:${userId}`
|
|
17345
|
+
);
|
|
16373
17346
|
if (!rl.allowed) {
|
|
16374
17347
|
return {
|
|
16375
17348
|
content: [
|
|
@@ -16381,12 +17354,27 @@ function registerConnectionTools(server) {
|
|
|
16381
17354
|
isError: true
|
|
16382
17355
|
};
|
|
16383
17356
|
}
|
|
17357
|
+
const projectResolution = await resolveProjectStrict(project_id);
|
|
17358
|
+
if (!projectResolution.projectId) {
|
|
17359
|
+
return {
|
|
17360
|
+
content: [
|
|
17361
|
+
{
|
|
17362
|
+
type: "text",
|
|
17363
|
+
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."
|
|
17364
|
+
}
|
|
17365
|
+
],
|
|
17366
|
+
isError: true
|
|
17367
|
+
};
|
|
17368
|
+
}
|
|
17369
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
17370
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
16384
17371
|
const { data, error } = await callEdgeFunction(
|
|
16385
17372
|
"mcp-data",
|
|
16386
17373
|
{
|
|
16387
17374
|
action: "mint-connection-nonce",
|
|
16388
17375
|
platform: platform3,
|
|
16389
|
-
|
|
17376
|
+
projectId: resolvedProjectId,
|
|
17377
|
+
project_id: resolvedProjectId
|
|
16390
17378
|
},
|
|
16391
17379
|
{ timeoutMs: 1e4 }
|
|
16392
17380
|
);
|
|
@@ -16402,6 +17390,17 @@ function registerConnectionTools(server) {
|
|
|
16402
17390
|
isError: true
|
|
16403
17391
|
};
|
|
16404
17392
|
}
|
|
17393
|
+
if (data.project_id !== resolvedProjectId) {
|
|
17394
|
+
return {
|
|
17395
|
+
content: [
|
|
17396
|
+
{
|
|
17397
|
+
type: "text",
|
|
17398
|
+
text: "Connection link project attestation failed. No OAuth link was returned."
|
|
17399
|
+
}
|
|
17400
|
+
],
|
|
17401
|
+
isError: true
|
|
17402
|
+
};
|
|
17403
|
+
}
|
|
16405
17404
|
if (format === "json") {
|
|
16406
17405
|
return {
|
|
16407
17406
|
content: [
|
|
@@ -16410,10 +17409,11 @@ function registerConnectionTools(server) {
|
|
|
16410
17409
|
text: JSON.stringify(
|
|
16411
17410
|
{
|
|
16412
17411
|
platform: data.platform,
|
|
16413
|
-
project_id:
|
|
17412
|
+
project_id: resolvedProjectId,
|
|
16414
17413
|
deep_link: data.deep_link,
|
|
16415
17414
|
expires_at: data.expires_at,
|
|
16416
|
-
next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection."
|
|
17415
|
+
next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection.",
|
|
17416
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
16417
17417
|
},
|
|
16418
17418
|
null,
|
|
16419
17419
|
2
|
|
@@ -16429,7 +17429,7 @@ function registerConnectionTools(server) {
|
|
|
16429
17429
|
type: "text",
|
|
16430
17430
|
text: [
|
|
16431
17431
|
`${data.platform} connection ready.`,
|
|
16432
|
-
|
|
17432
|
+
`Brand/project: ${resolvedProjectId}`,
|
|
16433
17433
|
"",
|
|
16434
17434
|
'Ask the user to open this link in a browser and click "Connect" on the platform:',
|
|
16435
17435
|
` ${data.deep_link}`,
|
|
@@ -16437,7 +17437,8 @@ function registerConnectionTools(server) {
|
|
|
16437
17437
|
`Link expires at: ${data.expires_at} (~2 minutes).`,
|
|
16438
17438
|
"",
|
|
16439
17439
|
"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."
|
|
17440
|
+
"This is a one-time browser setup \u2014 not another OAuth flow inside Claude.",
|
|
17441
|
+
...projectAutoResolvedNote ? ["", `Note: ${projectAutoResolvedNote}`] : []
|
|
16441
17442
|
].join("\n")
|
|
16442
17443
|
}
|
|
16443
17444
|
],
|
|
@@ -16450,14 +17451,20 @@ function registerConnectionTools(server) {
|
|
|
16450
17451
|
"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
17452
|
{
|
|
16452
17453
|
platform: z33.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
|
|
16453
|
-
project_id: z33.string().optional().describe(
|
|
17454
|
+
project_id: z33.string().uuid().optional().describe(
|
|
16454
17455
|
"Brand/project ID to scope the connection poll. Use the same project_id passed to start_platform_connection."
|
|
16455
17456
|
),
|
|
16456
17457
|
timeout_s: z33.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
|
|
16457
17458
|
poll_interval_s: z33.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
|
|
16458
17459
|
response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
16459
17460
|
},
|
|
16460
|
-
async ({
|
|
17461
|
+
async ({
|
|
17462
|
+
platform: platform3,
|
|
17463
|
+
project_id,
|
|
17464
|
+
timeout_s,
|
|
17465
|
+
poll_interval_s,
|
|
17466
|
+
response_format
|
|
17467
|
+
}) => {
|
|
16461
17468
|
const format = response_format ?? "text";
|
|
16462
17469
|
const startedAt = Date.now();
|
|
16463
17470
|
const timeoutMs = (timeout_s ?? 120) * 1e3;
|
|
@@ -16476,6 +17483,18 @@ function registerConnectionTools(server) {
|
|
|
16476
17483
|
isError: true
|
|
16477
17484
|
};
|
|
16478
17485
|
}
|
|
17486
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
17487
|
+
if (!resolvedProjectId) {
|
|
17488
|
+
return {
|
|
17489
|
+
content: [
|
|
17490
|
+
{
|
|
17491
|
+
type: "text",
|
|
17492
|
+
text: "A project_id is required to wait for a connection. Use the same project_id used to start the OAuth flow."
|
|
17493
|
+
}
|
|
17494
|
+
],
|
|
17495
|
+
isError: true
|
|
17496
|
+
};
|
|
17497
|
+
}
|
|
16479
17498
|
const activeWaits = inFlightWaitsByUser.get(userId) ?? 0;
|
|
16480
17499
|
if (activeWaits >= MAX_CONCURRENT_WAITS_PER_USER) {
|
|
16481
17500
|
return {
|
|
@@ -16497,12 +17516,62 @@ function registerConnectionTools(server) {
|
|
|
16497
17516
|
"mcp-data",
|
|
16498
17517
|
{
|
|
16499
17518
|
action: "connected-accounts",
|
|
16500
|
-
|
|
17519
|
+
projectId: resolvedProjectId,
|
|
17520
|
+
project_id: resolvedProjectId
|
|
16501
17521
|
},
|
|
16502
17522
|
{ timeoutMs: 1e4 }
|
|
16503
17523
|
);
|
|
16504
17524
|
if (!error && data?.success) {
|
|
16505
|
-
const
|
|
17525
|
+
const foundAccounts = findActiveAccounts(
|
|
17526
|
+
data.accounts ?? [],
|
|
17527
|
+
platform3,
|
|
17528
|
+
resolvedProjectId
|
|
17529
|
+
);
|
|
17530
|
+
if (foundAccounts.length > 1) {
|
|
17531
|
+
if (format === "json") {
|
|
17532
|
+
return {
|
|
17533
|
+
content: [
|
|
17534
|
+
{
|
|
17535
|
+
type: "text",
|
|
17536
|
+
text: JSON.stringify(
|
|
17537
|
+
{
|
|
17538
|
+
connected: true,
|
|
17539
|
+
platform: platform3,
|
|
17540
|
+
project_id: resolvedProjectId,
|
|
17541
|
+
accounts: foundAccounts.map((a) => ({
|
|
17542
|
+
id: a.id,
|
|
17543
|
+
username: a.username,
|
|
17544
|
+
connected_at: a.created_at
|
|
17545
|
+
})),
|
|
17546
|
+
attempts,
|
|
17547
|
+
message: `${platform3} has ${foundAccounts.length} active accounts for this project. Pass the exact account_id to schedule_post.`
|
|
17548
|
+
},
|
|
17549
|
+
null,
|
|
17550
|
+
2
|
|
17551
|
+
)
|
|
17552
|
+
}
|
|
17553
|
+
],
|
|
17554
|
+
isError: false
|
|
17555
|
+
};
|
|
17556
|
+
}
|
|
17557
|
+
return {
|
|
17558
|
+
content: [
|
|
17559
|
+
{
|
|
17560
|
+
type: "text",
|
|
17561
|
+
text: [
|
|
17562
|
+
`${platform3} is connected \u2014 ${foundAccounts.length} active accounts found for project ${resolvedProjectId}:`,
|
|
17563
|
+
...foundAccounts.map(
|
|
17564
|
+
(a) => ` ${a.username || "(unnamed)"} (id=${a.id})`
|
|
17565
|
+
),
|
|
17566
|
+
"",
|
|
17567
|
+
"Call schedule_post with the exact account_id (or account_ids) for the one you mean."
|
|
17568
|
+
].join("\n")
|
|
17569
|
+
}
|
|
17570
|
+
],
|
|
17571
|
+
isError: false
|
|
17572
|
+
};
|
|
17573
|
+
}
|
|
17574
|
+
const found = foundAccounts[0];
|
|
16506
17575
|
if (found) {
|
|
16507
17576
|
if (format === "json") {
|
|
16508
17577
|
return {
|
|
@@ -16513,7 +17582,7 @@ function registerConnectionTools(server) {
|
|
|
16513
17582
|
{
|
|
16514
17583
|
connected: true,
|
|
16515
17584
|
platform: found.platform,
|
|
16516
|
-
project_id:
|
|
17585
|
+
project_id: resolvedProjectId,
|
|
16517
17586
|
account_id: found.id,
|
|
16518
17587
|
username: found.username,
|
|
16519
17588
|
connected_at: found.created_at,
|
|
@@ -16533,7 +17602,7 @@ function registerConnectionTools(server) {
|
|
|
16533
17602
|
type: "text",
|
|
16534
17603
|
text: [
|
|
16535
17604
|
`${found.platform} is connected.`,
|
|
16536
|
-
|
|
17605
|
+
`Brand/project: ${resolvedProjectId}`,
|
|
16537
17606
|
`Account: ${found.username || "(unnamed)"} (id=${found.id})`,
|
|
16538
17607
|
`Detected after ${attempts} poll(s) in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s.`,
|
|
16539
17608
|
"Ready to call `schedule_post`."
|
|
@@ -16546,7 +17615,9 @@ function registerConnectionTools(server) {
|
|
|
16546
17615
|
}
|
|
16547
17616
|
const remaining = deadline - Date.now();
|
|
16548
17617
|
if (remaining <= 0) break;
|
|
16549
|
-
await new Promise(
|
|
17618
|
+
await new Promise(
|
|
17619
|
+
(resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining))
|
|
17620
|
+
);
|
|
16550
17621
|
}
|
|
16551
17622
|
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
17623
|
if (format === "json") {
|
|
@@ -16558,7 +17629,7 @@ function registerConnectionTools(server) {
|
|
|
16558
17629
|
{
|
|
16559
17630
|
connected: false,
|
|
16560
17631
|
platform: platform3,
|
|
16561
|
-
project_id:
|
|
17632
|
+
project_id: resolvedProjectId,
|
|
16562
17633
|
attempts,
|
|
16563
17634
|
timed_out: true,
|
|
16564
17635
|
message
|