@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/index.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
|
|
|
@@ -384,6 +384,209 @@ var init_request_context = __esm({
|
|
|
384
384
|
}
|
|
385
385
|
});
|
|
386
386
|
|
|
387
|
+
// src/lib/edge-function.ts
|
|
388
|
+
var edge_function_exports = {};
|
|
389
|
+
__export(edge_function_exports, {
|
|
390
|
+
callEdgeFunction: () => callEdgeFunction
|
|
391
|
+
});
|
|
392
|
+
function safeGatewayError(responseText, status) {
|
|
393
|
+
try {
|
|
394
|
+
const parsed = JSON.parse(responseText);
|
|
395
|
+
const nested = parsed.error && typeof parsed.error === "object" ? parsed.error : null;
|
|
396
|
+
const candidates = [
|
|
397
|
+
nested?.error_type,
|
|
398
|
+
nested?.code,
|
|
399
|
+
parsed.error_type,
|
|
400
|
+
parsed.error_code,
|
|
401
|
+
parsed.code,
|
|
402
|
+
typeof parsed.error === "string" ? parsed.error : null
|
|
403
|
+
];
|
|
404
|
+
for (const value of candidates) {
|
|
405
|
+
if (typeof value !== "string") continue;
|
|
406
|
+
const normalized = value.trim().toLowerCase();
|
|
407
|
+
if (SAFE_GATEWAY_ERROR_CODES.has(normalized)) return normalized;
|
|
408
|
+
const embedded = [...SAFE_GATEWAY_ERROR_CODES].find((code) => normalized.includes(code));
|
|
409
|
+
if (embedded) return embedded;
|
|
410
|
+
}
|
|
411
|
+
} catch {
|
|
412
|
+
}
|
|
413
|
+
return `Backend request failed (HTTP ${status}).`;
|
|
414
|
+
}
|
|
415
|
+
function safeFailureData(responseText) {
|
|
416
|
+
try {
|
|
417
|
+
const parsed = JSON.parse(responseText);
|
|
418
|
+
const metric = (name) => {
|
|
419
|
+
const value = parsed[name];
|
|
420
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
421
|
+
};
|
|
422
|
+
const billingStatus = typeof parsed.billing_status === "string" && SAFE_BILLING_STATUSES.has(parsed.billing_status) ? parsed.billing_status : void 0;
|
|
423
|
+
const failureReason = parsed.failure_reason === "generation_failed" || parsed.failure_reason === "authentication_failed" || parsed.failure_reason === "cancelled_by_user" ? parsed.failure_reason : void 0;
|
|
424
|
+
const jobStatus = typeof parsed.status === "string" && SAFE_JOB_STATUSES.has(parsed.status) ? parsed.status : void 0;
|
|
425
|
+
const safe = {
|
|
426
|
+
...jobStatus ? { status: jobStatus } : {},
|
|
427
|
+
...metric("credits_reserved") !== void 0 ? { credits_reserved: metric("credits_reserved") } : {},
|
|
428
|
+
...metric("credits_charged") !== void 0 ? { credits_charged: metric("credits_charged") } : {},
|
|
429
|
+
...metric("credits_refunded") !== void 0 ? { credits_refunded: metric("credits_refunded") } : {},
|
|
430
|
+
...billingStatus ? { billing_status: billingStatus } : {},
|
|
431
|
+
...failureReason ? { failure_reason: failureReason } : {}
|
|
432
|
+
};
|
|
433
|
+
return Object.keys(safe).length > 0 ? safe : null;
|
|
434
|
+
} catch {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function getApiKeyOrNull() {
|
|
439
|
+
const envKey = process.env.SOCIALNEURON_API_KEY;
|
|
440
|
+
if (envKey && envKey.trim().length) return envKey.trim();
|
|
441
|
+
const requestToken = getRequestToken();
|
|
442
|
+
if (requestToken) return requestToken;
|
|
443
|
+
return getAuthenticatedApiKey();
|
|
444
|
+
}
|
|
445
|
+
async function callEdgeFunction(functionName, body, options) {
|
|
446
|
+
const supabaseUrl = getSupabaseUrl();
|
|
447
|
+
const apiKey = getApiKeyOrNull();
|
|
448
|
+
const controller = new AbortController();
|
|
449
|
+
const timeoutMs = options?.timeoutMs ?? 6e4;
|
|
450
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
451
|
+
const enrichedBody = { ...body };
|
|
452
|
+
if (!enrichedBody.userId && !enrichedBody.user_id) {
|
|
453
|
+
try {
|
|
454
|
+
const defaultId = await getDefaultUserId();
|
|
455
|
+
enrichedBody.userId = defaultId;
|
|
456
|
+
enrichedBody.user_id = defaultId;
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
} else {
|
|
460
|
+
if (enrichedBody.userId && !enrichedBody.user_id) enrichedBody.user_id = enrichedBody.userId;
|
|
461
|
+
if (enrichedBody.user_id && !enrichedBody.userId) enrichedBody.userId = enrichedBody.user_id;
|
|
462
|
+
}
|
|
463
|
+
if (!enrichedBody.projectId && !enrichedBody.project_id) {
|
|
464
|
+
try {
|
|
465
|
+
const { getDefaultProjectId: getDefaultProjectId2 } = await Promise.resolve().then(() => (init_supabase(), supabase_exports));
|
|
466
|
+
const defaultProjectId = await getDefaultProjectId2();
|
|
467
|
+
if (defaultProjectId) {
|
|
468
|
+
enrichedBody.projectId = defaultProjectId;
|
|
469
|
+
enrichedBody.project_id = defaultProjectId;
|
|
470
|
+
}
|
|
471
|
+
} catch {
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
let url;
|
|
475
|
+
let method = options?.method ?? "POST";
|
|
476
|
+
let headers;
|
|
477
|
+
let requestBody;
|
|
478
|
+
if (!apiKey) {
|
|
479
|
+
clearTimeout(timer);
|
|
480
|
+
return {
|
|
481
|
+
data: null,
|
|
482
|
+
error: "Not authenticated. Run: npx @socialneuron/mcp-server login \u2014 Requires a paid plan (Starter+). See https://socialneuron.com/pricing"
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
url = new URL(`${supabaseUrl}/functions/v1/mcp-gateway`);
|
|
486
|
+
headers = {
|
|
487
|
+
Authorization: `Bearer ${apiKey}`,
|
|
488
|
+
"Content-Type": "application/json"
|
|
489
|
+
};
|
|
490
|
+
requestBody = {
|
|
491
|
+
functionName,
|
|
492
|
+
body: enrichedBody,
|
|
493
|
+
query: options?.query,
|
|
494
|
+
method: method.toUpperCase(),
|
|
495
|
+
timeoutMs
|
|
496
|
+
};
|
|
497
|
+
method = "POST";
|
|
498
|
+
try {
|
|
499
|
+
const response = await fetch(url.toString(), {
|
|
500
|
+
method,
|
|
501
|
+
headers,
|
|
502
|
+
body: method === "GET" ? void 0 : JSON.stringify(requestBody),
|
|
503
|
+
signal: controller.signal
|
|
504
|
+
});
|
|
505
|
+
clearTimeout(timer);
|
|
506
|
+
const responseText = await response.text();
|
|
507
|
+
if (!response.ok) {
|
|
508
|
+
const errorCode = safeGatewayError(responseText, response.status);
|
|
509
|
+
const failureData = safeFailureData(responseText);
|
|
510
|
+
if (response.status === 401) {
|
|
511
|
+
return {
|
|
512
|
+
data: failureData,
|
|
513
|
+
error: `Authentication failed (HTTP 401). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
if (response.status === 403) {
|
|
517
|
+
return {
|
|
518
|
+
data: failureData,
|
|
519
|
+
error: `Forbidden (HTTP 403): ${errorCode}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
if (response.status === 429) {
|
|
523
|
+
const retryAfter = response.headers.get("retry-after") || "60";
|
|
524
|
+
return {
|
|
525
|
+
data: failureData,
|
|
526
|
+
error: `Rate limit exceeded (HTTP 429). Wait ${retryAfter}s before retrying. Reduce request frequency or upgrade your plan.`
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
return { data: failureData, error: errorCode };
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
const data = JSON.parse(responseText);
|
|
533
|
+
return { data, error: null };
|
|
534
|
+
} catch {
|
|
535
|
+
return { data: { text: responseText }, error: null };
|
|
536
|
+
}
|
|
537
|
+
} catch (err) {
|
|
538
|
+
clearTimeout(timer);
|
|
539
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
540
|
+
return {
|
|
541
|
+
data: null,
|
|
542
|
+
error: `Edge Function '${functionName}' timed out after ${timeoutMs}ms`
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
return { data: null, error: "Network request failed. Please retry." };
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
var SAFE_GATEWAY_ERROR_CODES, SAFE_BILLING_STATUSES, SAFE_JOB_STATUSES;
|
|
549
|
+
var init_edge_function = __esm({
|
|
550
|
+
"src/lib/edge-function.ts"() {
|
|
551
|
+
"use strict";
|
|
552
|
+
init_supabase();
|
|
553
|
+
init_request_context();
|
|
554
|
+
SAFE_GATEWAY_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
555
|
+
"daily_limit_reached",
|
|
556
|
+
"insufficient_credits",
|
|
557
|
+
"project_scope_mismatch",
|
|
558
|
+
"schedule_conflict",
|
|
559
|
+
"post_not_found",
|
|
560
|
+
"post_not_reschedulable",
|
|
561
|
+
"post_in_progress",
|
|
562
|
+
"not_cancellable",
|
|
563
|
+
"publishing_in_progress",
|
|
564
|
+
"plan_upgrade_required",
|
|
565
|
+
"rate_limited",
|
|
566
|
+
"validation_error",
|
|
567
|
+
"permission_denied",
|
|
568
|
+
"not_found"
|
|
569
|
+
]);
|
|
570
|
+
SAFE_BILLING_STATUSES = /* @__PURE__ */ new Set([
|
|
571
|
+
"reserved",
|
|
572
|
+
"charged",
|
|
573
|
+
"refunded",
|
|
574
|
+
"failed_no_charge",
|
|
575
|
+
"refund_pending",
|
|
576
|
+
"not_charged"
|
|
577
|
+
]);
|
|
578
|
+
SAFE_JOB_STATUSES = /* @__PURE__ */ new Set([
|
|
579
|
+
"queued",
|
|
580
|
+
"pending",
|
|
581
|
+
"processing",
|
|
582
|
+
"completed",
|
|
583
|
+
"failed",
|
|
584
|
+
"cancelled",
|
|
585
|
+
"canceled"
|
|
586
|
+
]);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
387
590
|
// src/cli/credentials.ts
|
|
388
591
|
var credentials_exports = {};
|
|
389
592
|
__export(credentials_exports, {
|
|
@@ -410,6 +613,10 @@ import {
|
|
|
410
613
|
} from "node:fs";
|
|
411
614
|
import { homedir, platform } from "node:os";
|
|
412
615
|
import { join } from "node:path";
|
|
616
|
+
function loadNativeKeyring() {
|
|
617
|
+
nativeKeyringModule ??= import("@napi-rs/keyring").catch(() => null);
|
|
618
|
+
return nativeKeyringModule;
|
|
619
|
+
}
|
|
413
620
|
function assertSafeCredentialPaths() {
|
|
414
621
|
if (platform() === "win32") return;
|
|
415
622
|
const uid = process.getuid?.();
|
|
@@ -529,49 +736,90 @@ function writeCredentialsFile(data) {
|
|
|
529
736
|
}
|
|
530
737
|
hardenCredentialPermissions();
|
|
531
738
|
}
|
|
532
|
-
function
|
|
739
|
+
function isMacKeychainItemMissing(error) {
|
|
740
|
+
const commandError = error;
|
|
741
|
+
if (commandError?.status === 44) return true;
|
|
742
|
+
const detail = `${commandError?.stderr ?? ""}
|
|
743
|
+
${commandError?.message ?? ""}`;
|
|
744
|
+
return /\berrsecitemnotfound\b/i.test(detail) || /(?:^|:\s*)the specified item could not be found in the keychain\.\s*$/i.test(detail.trim());
|
|
745
|
+
}
|
|
746
|
+
async function inspectMacKeychain(service) {
|
|
747
|
+
const native = await loadNativeKeyring();
|
|
748
|
+
if (native) {
|
|
749
|
+
try {
|
|
750
|
+
const value = new native.Entry(service, KEYCHAIN_ACCOUNT).getPassword();
|
|
751
|
+
if (value) return { status: "found", value };
|
|
752
|
+
} catch {
|
|
753
|
+
}
|
|
754
|
+
}
|
|
533
755
|
try {
|
|
534
756
|
const result = execFileSync(
|
|
535
757
|
"security",
|
|
536
758
|
["find-generic-password", "-a", KEYCHAIN_ACCOUNT, "-s", service, "-w"],
|
|
537
759
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
538
760
|
);
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
761
|
+
const value = result.trim();
|
|
762
|
+
return value ? { status: "found", value } : { status: "missing" };
|
|
763
|
+
} catch (error) {
|
|
764
|
+
return isMacKeychainItemMissing(error) ? { status: "missing" } : { status: "unavailable" };
|
|
542
765
|
}
|
|
543
766
|
}
|
|
544
|
-
function
|
|
767
|
+
async function macKeychainRead(service) {
|
|
768
|
+
const result = await inspectMacKeychain(service);
|
|
769
|
+
return result.status === "found" ? result.value : null;
|
|
770
|
+
}
|
|
771
|
+
async function macKeychainWrite(service, value) {
|
|
772
|
+
const native = await loadNativeKeyring();
|
|
773
|
+
if (!native) return false;
|
|
545
774
|
try {
|
|
546
|
-
|
|
547
|
-
"security",
|
|
548
|
-
[
|
|
549
|
-
"add-generic-password",
|
|
550
|
-
"-a",
|
|
551
|
-
KEYCHAIN_ACCOUNT,
|
|
552
|
-
"-s",
|
|
553
|
-
service,
|
|
554
|
-
"-w",
|
|
555
|
-
value,
|
|
556
|
-
"-U"
|
|
557
|
-
// update if exists
|
|
558
|
-
],
|
|
559
|
-
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
560
|
-
);
|
|
775
|
+
new native.Entry(service, KEYCHAIN_ACCOUNT).setPassword(value);
|
|
561
776
|
return true;
|
|
562
777
|
} catch {
|
|
563
778
|
return false;
|
|
564
779
|
}
|
|
565
780
|
}
|
|
566
|
-
function macKeychainDelete(service) {
|
|
781
|
+
async function macKeychainDelete(service) {
|
|
782
|
+
const native = await loadNativeKeyring();
|
|
783
|
+
let nativeDeleted = false;
|
|
784
|
+
if (native) {
|
|
785
|
+
try {
|
|
786
|
+
nativeDeleted = new native.Entry(service, KEYCHAIN_ACCOUNT).deletePassword();
|
|
787
|
+
} catch {
|
|
788
|
+
}
|
|
789
|
+
}
|
|
567
790
|
try {
|
|
568
791
|
execFileSync("security", ["delete-generic-password", "-a", KEYCHAIN_ACCOUNT, "-s", service], {
|
|
569
792
|
stdio: ["pipe", "pipe", "pipe"]
|
|
570
793
|
});
|
|
571
|
-
return
|
|
572
|
-
} catch {
|
|
573
|
-
return
|
|
794
|
+
return "deleted";
|
|
795
|
+
} catch (error) {
|
|
796
|
+
if (isMacKeychainItemMissing(error)) return nativeDeleted ? "deleted" : "missing";
|
|
797
|
+
return "unavailable";
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
async function clearMacKeychain(service) {
|
|
801
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
802
|
+
const deleted = await macKeychainDelete(service);
|
|
803
|
+
if (deleted === "unavailable") break;
|
|
804
|
+
const remaining = await inspectMacKeychain(service);
|
|
805
|
+
if (remaining.status === "missing") return;
|
|
806
|
+
if (remaining.status === "unavailable") break;
|
|
807
|
+
}
|
|
808
|
+
throw new Error(
|
|
809
|
+
"Unable to verify removal of the existing Social Neuron Keychain credential. Unlock Keychain Access, remove the item, and retry."
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
async function verifyMacFileFallback(service) {
|
|
813
|
+
const existing = await inspectMacKeychain(service);
|
|
814
|
+
if (existing.status === "missing") return;
|
|
815
|
+
if (existing.status === "found") {
|
|
816
|
+
throw new Error(
|
|
817
|
+
"An existing Social Neuron Keychain credential could not be replaced. The existing value was retained; unlock or remove it in Keychain Access and retry."
|
|
818
|
+
);
|
|
574
819
|
}
|
|
820
|
+
throw new Error(
|
|
821
|
+
"Unable to verify absence of an existing Social Neuron Keychain credential. Unlock Keychain Access and retry."
|
|
822
|
+
);
|
|
575
823
|
}
|
|
576
824
|
function linuxSecretRead(key) {
|
|
577
825
|
try {
|
|
@@ -612,7 +860,7 @@ async function loadApiKey() {
|
|
|
612
860
|
if (envKey) return envKey;
|
|
613
861
|
const os = platform();
|
|
614
862
|
if (os === "darwin") {
|
|
615
|
-
const key = macKeychainRead(KEYCHAIN_SERVICE_API);
|
|
863
|
+
const key = await macKeychainRead(KEYCHAIN_SERVICE_API);
|
|
616
864
|
if (key) return key;
|
|
617
865
|
} else if (os === "linux") {
|
|
618
866
|
const key = linuxSecretRead("api-key");
|
|
@@ -629,13 +877,18 @@ async function loadApiKey() {
|
|
|
629
877
|
async function saveApiKey(key) {
|
|
630
878
|
const os = platform();
|
|
631
879
|
let saved = false;
|
|
880
|
+
let fallbackCredentials;
|
|
632
881
|
if (os === "darwin") {
|
|
633
|
-
saved = macKeychainWrite(KEYCHAIN_SERVICE_API, key);
|
|
882
|
+
saved = await macKeychainWrite(KEYCHAIN_SERVICE_API, key);
|
|
883
|
+
if (!saved) {
|
|
884
|
+
fallbackCredentials = readCredentialsFile();
|
|
885
|
+
await verifyMacFileFallback(KEYCHAIN_SERVICE_API);
|
|
886
|
+
}
|
|
634
887
|
} else if (os === "linux") {
|
|
635
888
|
saved = linuxSecretWrite("api-key", key);
|
|
636
889
|
}
|
|
637
890
|
if (!saved) {
|
|
638
|
-
const creds = readCredentialsFile();
|
|
891
|
+
const creds = fallbackCredentials ?? readCredentialsFile();
|
|
639
892
|
creds.apiKey = key;
|
|
640
893
|
writeCredentialsFile(creds);
|
|
641
894
|
if (os === "win32") {
|
|
@@ -658,8 +911,13 @@ async function saveApiKey(key) {
|
|
|
658
911
|
}
|
|
659
912
|
async function deleteApiKey() {
|
|
660
913
|
const os = platform();
|
|
914
|
+
let keychainError;
|
|
661
915
|
if (os === "darwin") {
|
|
662
|
-
|
|
916
|
+
try {
|
|
917
|
+
await clearMacKeychain(KEYCHAIN_SERVICE_API);
|
|
918
|
+
} catch (error) {
|
|
919
|
+
keychainError = error;
|
|
920
|
+
}
|
|
663
921
|
} else if (os === "linux") {
|
|
664
922
|
linuxSecretDelete("api-key");
|
|
665
923
|
}
|
|
@@ -675,13 +933,14 @@ async function deleteApiKey() {
|
|
|
675
933
|
writeCredentialsFile(creds);
|
|
676
934
|
}
|
|
677
935
|
}
|
|
936
|
+
if (keychainError) throw keychainError;
|
|
678
937
|
}
|
|
679
938
|
async function loadSupabaseUrl() {
|
|
680
939
|
const envUrl = process.env.SOCIALNEURON_SUPABASE_URL || process.env.SUPABASE_URL;
|
|
681
940
|
if (envUrl) return envUrl;
|
|
682
941
|
const os = platform();
|
|
683
942
|
if (os === "darwin") {
|
|
684
|
-
const url = macKeychainRead(KEYCHAIN_SERVICE_URL);
|
|
943
|
+
const url = await macKeychainRead(KEYCHAIN_SERVICE_URL);
|
|
685
944
|
if (url) return url;
|
|
686
945
|
} else if (os === "linux") {
|
|
687
946
|
const url = linuxSecretRead("supabase-url");
|
|
@@ -693,18 +952,23 @@ async function loadSupabaseUrl() {
|
|
|
693
952
|
async function saveSupabaseUrl(url) {
|
|
694
953
|
const os = platform();
|
|
695
954
|
let saved = false;
|
|
955
|
+
let fallbackCredentials;
|
|
696
956
|
if (os === "darwin") {
|
|
697
|
-
saved = macKeychainWrite(KEYCHAIN_SERVICE_URL, url);
|
|
957
|
+
saved = await macKeychainWrite(KEYCHAIN_SERVICE_URL, url);
|
|
958
|
+
if (!saved) {
|
|
959
|
+
fallbackCredentials = readCredentialsFile();
|
|
960
|
+
await verifyMacFileFallback(KEYCHAIN_SERVICE_URL);
|
|
961
|
+
}
|
|
698
962
|
} else if (os === "linux") {
|
|
699
963
|
saved = linuxSecretWrite("supabase-url", url);
|
|
700
964
|
}
|
|
701
965
|
if (!saved) {
|
|
702
|
-
const creds = readCredentialsFile();
|
|
966
|
+
const creds = fallbackCredentials ?? readCredentialsFile();
|
|
703
967
|
creds.supabaseUrl = url;
|
|
704
968
|
writeCredentialsFile(creds);
|
|
705
969
|
}
|
|
706
970
|
}
|
|
707
|
-
var KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE_API, KEYCHAIN_SERVICE_URL, CONFIG_DIR, CREDENTIALS_FILE;
|
|
971
|
+
var KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE_API, KEYCHAIN_SERVICE_URL, CONFIG_DIR, CREDENTIALS_FILE, nativeKeyringModule;
|
|
708
972
|
var init_credentials = __esm({
|
|
709
973
|
"src/cli/credentials.ts"() {
|
|
710
974
|
"use strict";
|
|
@@ -857,7 +1121,10 @@ __export(supabase_exports, {
|
|
|
857
1121
|
getSupabaseUrl: () => getSupabaseUrl,
|
|
858
1122
|
initializeAuth: () => initializeAuth,
|
|
859
1123
|
isTelemetryDisabled: () => isTelemetryDisabled,
|
|
860
|
-
|
|
1124
|
+
listAccessibleProjectsWithAccountStatus: () => listAccessibleProjectsWithAccountStatus,
|
|
1125
|
+
logMcpToolInvocation: () => logMcpToolInvocation,
|
|
1126
|
+
resolveProjectForConnectedAccountTool: () => resolveProjectForConnectedAccountTool,
|
|
1127
|
+
resolveProjectStrict: () => resolveProjectStrict
|
|
861
1128
|
});
|
|
862
1129
|
import { createClient } from "@supabase/supabase-js";
|
|
863
1130
|
import { randomUUID } from "node:crypto";
|
|
@@ -896,20 +1163,17 @@ async function getDefaultUserId() {
|
|
|
896
1163
|
if (authenticatedUserId) return authenticatedUserId;
|
|
897
1164
|
const envUserId = process.env.SOCIALNEURON_USER_ID;
|
|
898
1165
|
if (envUserId) return envUserId;
|
|
899
|
-
throw new Error(
|
|
1166
|
+
throw new Error(
|
|
1167
|
+
"No user ID available. Set SOCIALNEURON_USER_ID or authenticate via API key."
|
|
1168
|
+
);
|
|
900
1169
|
}
|
|
901
1170
|
async function getDefaultProjectId() {
|
|
902
1171
|
const requestProjectId = getRequestProjectId();
|
|
903
1172
|
if (requestProjectId) return requestProjectId;
|
|
904
1173
|
if (authenticatedProjectId) return authenticatedProjectId;
|
|
905
1174
|
const userId = await getDefaultUserId().catch(() => null);
|
|
906
|
-
if (userId) {
|
|
907
|
-
const cached = projectIdCache.get(userId);
|
|
908
|
-
if (cached) return cached;
|
|
909
|
-
}
|
|
910
1175
|
const envProjectId = process.env.SOCIALNEURON_PROJECT_ID;
|
|
911
1176
|
if (envProjectId) {
|
|
912
|
-
if (userId) projectIdCache.set(userId, envProjectId);
|
|
913
1177
|
return envProjectId;
|
|
914
1178
|
}
|
|
915
1179
|
if (!userId) return null;
|
|
@@ -918,15 +1182,126 @@ async function getDefaultProjectId() {
|
|
|
918
1182
|
const { data: memberships } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId);
|
|
919
1183
|
const orgIds = (memberships ?? []).map((m) => m.organization_id);
|
|
920
1184
|
if (orgIds.length === 0) return null;
|
|
921
|
-
const { data } = await supabase.from("projects").select("id").in("organization_id", orgIds).order("created_at", { ascending: false }).limit(
|
|
922
|
-
if (data?.
|
|
923
|
-
projectIdCache.set(userId, data.id);
|
|
924
|
-
return data.id;
|
|
925
|
-
}
|
|
1185
|
+
const { data } = await supabase.from("projects").select("id").in("organization_id", orgIds).order("created_at", { ascending: false }).limit(2);
|
|
1186
|
+
if (data?.length === 1) return data[0].id;
|
|
926
1187
|
} catch {
|
|
927
1188
|
}
|
|
928
1189
|
return null;
|
|
929
1190
|
}
|
|
1191
|
+
function normalizePlatformFilter(platform3) {
|
|
1192
|
+
if (!platform3) return null;
|
|
1193
|
+
const values = Array.isArray(platform3) ? platform3 : [platform3];
|
|
1194
|
+
const set = new Set(values.filter(Boolean).map((p) => p.toLowerCase()));
|
|
1195
|
+
return set.size > 0 ? set : null;
|
|
1196
|
+
}
|
|
1197
|
+
async function listAccessibleProjectsWithAccountStatusDirect(userId, platform3) {
|
|
1198
|
+
try {
|
|
1199
|
+
const supabase = getSupabaseClient();
|
|
1200
|
+
const { data: memberships } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId);
|
|
1201
|
+
const orgIds = (memberships ?? []).map((m) => m.organization_id);
|
|
1202
|
+
if (orgIds.length === 0) return [];
|
|
1203
|
+
const { data: projects } = await supabase.from("projects").select("id, name").in("organization_id", orgIds).order("created_at", { ascending: false });
|
|
1204
|
+
const projectRows = projects ?? [];
|
|
1205
|
+
if (projectRows.length === 0) return [];
|
|
1206
|
+
const projectIds = projectRows.map((p) => p.id);
|
|
1207
|
+
const { data: accounts } = await supabase.from("connected_accounts").select("project_id, status, platform").eq("user_id", userId).in("project_id", projectIds);
|
|
1208
|
+
const anyAccountByProject = /* @__PURE__ */ new Set();
|
|
1209
|
+
const platformsByProject = /* @__PURE__ */ new Map();
|
|
1210
|
+
for (const row of accounts ?? []) {
|
|
1211
|
+
if (row.status !== "active" && row.status !== "expires_soon") continue;
|
|
1212
|
+
if (!row.project_id) continue;
|
|
1213
|
+
anyAccountByProject.add(row.project_id);
|
|
1214
|
+
if (row.platform) {
|
|
1215
|
+
const set = platformsByProject.get(row.project_id) ?? /* @__PURE__ */ new Set();
|
|
1216
|
+
set.add(row.platform.toLowerCase());
|
|
1217
|
+
platformsByProject.set(row.project_id, set);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
const requestedPlatforms = normalizePlatformFilter(platform3);
|
|
1221
|
+
return projectRows.map((p) => {
|
|
1222
|
+
const platforms = Array.from(platformsByProject.get(p.id) ?? []);
|
|
1223
|
+
const hasConnectedAccounts = requestedPlatforms ? platforms.some((pl) => requestedPlatforms.has(pl)) : anyAccountByProject.has(p.id);
|
|
1224
|
+
return { id: p.id, name: p.name, hasConnectedAccounts, platforms };
|
|
1225
|
+
});
|
|
1226
|
+
} catch {
|
|
1227
|
+
return [];
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
async function listAccessibleProjectsWithAccountStatusViaEdgeFunction(userId, platform3) {
|
|
1231
|
+
try {
|
|
1232
|
+
const { callEdgeFunction: callEdgeFunction2 } = await Promise.resolve().then(() => (init_edge_function(), edge_function_exports));
|
|
1233
|
+
const { data, error } = await callEdgeFunction2(
|
|
1234
|
+
"mcp-data",
|
|
1235
|
+
{ action: "projects", userId, user_id: userId },
|
|
1236
|
+
{ timeoutMs: 1e4 }
|
|
1237
|
+
);
|
|
1238
|
+
if (error || !data?.success || !Array.isArray(data.projects)) return [];
|
|
1239
|
+
const requestedPlatforms = normalizePlatformFilter(platform3);
|
|
1240
|
+
return data.projects.map((p) => {
|
|
1241
|
+
const platforms = (p.platforms ?? []).map((pl) => pl.toLowerCase());
|
|
1242
|
+
const hasConnectedAccounts = requestedPlatforms ? platforms.some((pl) => requestedPlatforms.has(pl)) : Boolean(p.hasConnectedAccounts);
|
|
1243
|
+
return { id: p.id, name: p.name, hasConnectedAccounts, platforms };
|
|
1244
|
+
});
|
|
1245
|
+
} catch {
|
|
1246
|
+
return [];
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
async function listAccessibleProjectsWithAccountStatus(userId, platform3) {
|
|
1250
|
+
if (getServiceKeyOrNull()) {
|
|
1251
|
+
return listAccessibleProjectsWithAccountStatusDirect(userId, platform3);
|
|
1252
|
+
}
|
|
1253
|
+
return listAccessibleProjectsWithAccountStatusViaEdgeFunction(
|
|
1254
|
+
userId,
|
|
1255
|
+
platform3
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
async function resolveProjectForConnectedAccountTool(explicitProjectId, platform3) {
|
|
1259
|
+
if (explicitProjectId) return { projectId: explicitProjectId };
|
|
1260
|
+
const defaultProjectId = await getDefaultProjectId();
|
|
1261
|
+
if (defaultProjectId) return { projectId: defaultProjectId };
|
|
1262
|
+
const genericError = "project_id is required. Configure an explicit project or use an API key scoped to exactly one project.";
|
|
1263
|
+
const userId = await getDefaultUserId().catch(() => null);
|
|
1264
|
+
if (!userId) return { error: genericError };
|
|
1265
|
+
const projects = await listAccessibleProjectsWithAccountStatus(
|
|
1266
|
+
userId,
|
|
1267
|
+
platform3
|
|
1268
|
+
);
|
|
1269
|
+
if (projects.length === 0) return { error: genericError };
|
|
1270
|
+
const withAccounts = projects.filter((p) => p.hasConnectedAccounts);
|
|
1271
|
+
if (withAccounts.length === 1) {
|
|
1272
|
+
const chosen = withAccounts[0];
|
|
1273
|
+
const platformNote = platform3 ? ` for ${Array.isArray(platform3) ? platform3.join("/") : platform3}` : "";
|
|
1274
|
+
return {
|
|
1275
|
+
projectId: chosen.id,
|
|
1276
|
+
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}.`,
|
|
1277
|
+
projects
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
const projectList = projects.map(
|
|
1281
|
+
(p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
|
|
1282
|
+
).join("; ");
|
|
1283
|
+
return {
|
|
1284
|
+
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}.`,
|
|
1285
|
+
projects
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
async function resolveProjectStrict(explicitProjectId) {
|
|
1289
|
+
if (explicitProjectId) return { projectId: explicitProjectId };
|
|
1290
|
+
const defaultProjectId = await getDefaultProjectId();
|
|
1291
|
+
if (defaultProjectId) return { projectId: defaultProjectId };
|
|
1292
|
+
const genericError = "project_id is required. Configure an explicit project or use an API key scoped to exactly one project.";
|
|
1293
|
+
const userId = await getDefaultUserId().catch(() => null);
|
|
1294
|
+
if (!userId) return { error: genericError };
|
|
1295
|
+
const projects = await listAccessibleProjectsWithAccountStatus(userId);
|
|
1296
|
+
if (projects.length === 0) return { error: genericError };
|
|
1297
|
+
const projectList = projects.map(
|
|
1298
|
+
(p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
|
|
1299
|
+
).join("; ");
|
|
1300
|
+
return {
|
|
1301
|
+
error: `project_id is required \u2014 your account has ${projects.length} projects. Pass the exact project_id from this list: ${projectList}.`,
|
|
1302
|
+
projects
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
930
1305
|
async function initializeAuth() {
|
|
931
1306
|
const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
932
1307
|
const apiKey = await loadApiKey2();
|
|
@@ -952,8 +1327,11 @@ async function initializeAuth() {
|
|
|
952
1327
|
}
|
|
953
1328
|
if (authenticatedExpiresAt) {
|
|
954
1329
|
const expiresMs = new Date(authenticatedExpiresAt).getTime();
|
|
955
|
-
const daysLeft = Math.ceil(
|
|
956
|
-
|
|
1330
|
+
const daysLeft = Math.ceil(
|
|
1331
|
+
(expiresMs - Date.now()) / (1e3 * 60 * 60 * 24)
|
|
1332
|
+
);
|
|
1333
|
+
if (!_quietAuth)
|
|
1334
|
+
console.error("[MCP] Key expires: " + authenticatedExpiresAt);
|
|
957
1335
|
if (daysLeft <= 7) {
|
|
958
1336
|
console.error(
|
|
959
1337
|
`[MCP] Warning: API key expires in ${daysLeft} day(s). Run: npx @socialneuron/mcp-server login`
|
|
@@ -1032,7 +1410,7 @@ async function logMcpToolInvocation(args) {
|
|
|
1032
1410
|
Promise.resolve().then(() => (init_posthog(), posthog_exports)).then(({ captureToolEvent: captureToolEvent2 }) => captureToolEvent2(args)).catch(() => {
|
|
1033
1411
|
});
|
|
1034
1412
|
}
|
|
1035
|
-
var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, authenticatedProjectId, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY
|
|
1413
|
+
var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, authenticatedProjectId, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY;
|
|
1036
1414
|
var init_supabase = __esm({
|
|
1037
1415
|
"src/lib/supabase.ts"() {
|
|
1038
1416
|
"use strict";
|
|
@@ -1050,7 +1428,6 @@ var init_supabase = __esm({
|
|
|
1050
1428
|
MCP_RUN_ID = randomUUID();
|
|
1051
1429
|
CLOUD_SUPABASE_URL = "https://rhukkjscgzauutioyeei.supabase.co";
|
|
1052
1430
|
CLOUD_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJodWtranNjZ3phdXV0aW95ZWVpIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ4NjM4ODYsImV4cCI6MjA4MDQzOTg4Nn0.JVtrviGvN0HaSh0JFS5KNl5FAB5ffG5Y1IMZsQFUrNQ";
|
|
1053
|
-
projectIdCache = /* @__PURE__ */ new Map();
|
|
1054
1431
|
}
|
|
1055
1432
|
});
|
|
1056
1433
|
|
|
@@ -2032,253 +2409,50 @@ function scan(text, options) {
|
|
|
2032
2409
|
return {
|
|
2033
2410
|
passed: false,
|
|
2034
2411
|
risk_score: 1,
|
|
2035
|
-
flagged_patterns: ["excessive_length"],
|
|
2036
|
-
pii_redacted: false
|
|
2037
|
-
};
|
|
2038
|
-
}
|
|
2039
|
-
const zw = detectZeroWidth(text);
|
|
2040
|
-
if (zw.found) {
|
|
2041
|
-
flagged.add(zw.pattern);
|
|
2042
|
-
risk = Math.max(risk, 0.95);
|
|
2043
|
-
}
|
|
2044
|
-
const ipRaw = detectInstructionPhrase(text);
|
|
2045
|
-
if (ipRaw.found) {
|
|
2046
|
-
flagged.add(ipRaw.pattern);
|
|
2047
|
-
risk = Math.max(risk, 0.9);
|
|
2048
|
-
}
|
|
2049
|
-
const normalized = normalize(text);
|
|
2050
|
-
const ipNorm = detectInstructionPhrase(normalized);
|
|
2051
|
-
if (ipNorm.found) {
|
|
2052
|
-
flagged.add(ipNorm.pattern);
|
|
2053
|
-
risk = Math.max(risk, 0.9);
|
|
2054
|
-
}
|
|
2055
|
-
const flaggedArr = Array.from(flagged);
|
|
2056
|
-
if ((options.mode === "block" || options.mode === "sanitize") && flaggedArr.length > 0) {
|
|
2057
|
-
return { passed: false, risk_score: risk, flagged_patterns: flaggedArr, pii_redacted: false };
|
|
2058
|
-
}
|
|
2059
|
-
const pii = scrubPii(normalized, options.source);
|
|
2060
|
-
if (pii.redacted) {
|
|
2061
|
-
return {
|
|
2062
|
-
passed: true,
|
|
2063
|
-
risk_score: Math.max(risk, 0.3),
|
|
2064
|
-
flagged_patterns: [...flaggedArr, ...pii.patterns.map((p) => `pii_${p}`)],
|
|
2065
|
-
sanitized_text: pii.text,
|
|
2066
|
-
pii_redacted: true
|
|
2067
|
-
};
|
|
2068
|
-
}
|
|
2069
|
-
return { passed: true, risk_score: risk, flagged_patterns: flaggedArr, pii_redacted: false };
|
|
2070
|
-
}
|
|
2071
|
-
var init_scanner = __esm({
|
|
2072
|
-
"src/lib/agent-harness/scanner.ts"() {
|
|
2073
|
-
"use strict";
|
|
2074
|
-
init_constants2();
|
|
2075
|
-
init_normalize();
|
|
2076
|
-
init_zeroWidth();
|
|
2077
|
-
init_instructionPhrase();
|
|
2078
|
-
init_pii();
|
|
2079
|
-
}
|
|
2080
|
-
});
|
|
2081
|
-
|
|
2082
|
-
// src/lib/edge-function.ts
|
|
2083
|
-
var edge_function_exports = {};
|
|
2084
|
-
__export(edge_function_exports, {
|
|
2085
|
-
callEdgeFunction: () => callEdgeFunction
|
|
2086
|
-
});
|
|
2087
|
-
function safeGatewayError(responseText, status) {
|
|
2088
|
-
try {
|
|
2089
|
-
const parsed = JSON.parse(responseText);
|
|
2090
|
-
const nested = parsed.error && typeof parsed.error === "object" ? parsed.error : null;
|
|
2091
|
-
const candidates = [
|
|
2092
|
-
nested?.error_type,
|
|
2093
|
-
nested?.code,
|
|
2094
|
-
parsed.error_type,
|
|
2095
|
-
parsed.error_code,
|
|
2096
|
-
parsed.code,
|
|
2097
|
-
typeof parsed.error === "string" ? parsed.error : null
|
|
2098
|
-
];
|
|
2099
|
-
for (const value of candidates) {
|
|
2100
|
-
if (typeof value !== "string") continue;
|
|
2101
|
-
const normalized = value.trim().toLowerCase();
|
|
2102
|
-
if (SAFE_GATEWAY_ERROR_CODES.has(normalized)) return normalized;
|
|
2103
|
-
const embedded = [...SAFE_GATEWAY_ERROR_CODES].find((code) => normalized.includes(code));
|
|
2104
|
-
if (embedded) return embedded;
|
|
2105
|
-
}
|
|
2106
|
-
} catch {
|
|
2107
|
-
}
|
|
2108
|
-
return `Backend request failed (HTTP ${status}).`;
|
|
2109
|
-
}
|
|
2110
|
-
function safeFailureData(responseText) {
|
|
2111
|
-
try {
|
|
2112
|
-
const parsed = JSON.parse(responseText);
|
|
2113
|
-
const metric = (name) => {
|
|
2114
|
-
const value = parsed[name];
|
|
2115
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
2116
|
-
};
|
|
2117
|
-
const billingStatus = typeof parsed.billing_status === "string" && SAFE_BILLING_STATUSES.has(parsed.billing_status) ? parsed.billing_status : void 0;
|
|
2118
|
-
const failureReason = parsed.failure_reason === "generation_failed" || parsed.failure_reason === "authentication_failed" || parsed.failure_reason === "cancelled_by_user" ? parsed.failure_reason : void 0;
|
|
2119
|
-
const jobStatus = typeof parsed.status === "string" && SAFE_JOB_STATUSES.has(parsed.status) ? parsed.status : void 0;
|
|
2120
|
-
const safe = {
|
|
2121
|
-
...jobStatus ? { status: jobStatus } : {},
|
|
2122
|
-
...metric("credits_reserved") !== void 0 ? { credits_reserved: metric("credits_reserved") } : {},
|
|
2123
|
-
...metric("credits_charged") !== void 0 ? { credits_charged: metric("credits_charged") } : {},
|
|
2124
|
-
...metric("credits_refunded") !== void 0 ? { credits_refunded: metric("credits_refunded") } : {},
|
|
2125
|
-
...billingStatus ? { billing_status: billingStatus } : {},
|
|
2126
|
-
...failureReason ? { failure_reason: failureReason } : {}
|
|
2127
|
-
};
|
|
2128
|
-
return Object.keys(safe).length > 0 ? safe : null;
|
|
2129
|
-
} catch {
|
|
2130
|
-
return null;
|
|
2131
|
-
}
|
|
2132
|
-
}
|
|
2133
|
-
function getApiKeyOrNull() {
|
|
2134
|
-
const envKey = process.env.SOCIALNEURON_API_KEY;
|
|
2135
|
-
if (envKey && envKey.trim().length) return envKey.trim();
|
|
2136
|
-
const requestToken = getRequestToken();
|
|
2137
|
-
if (requestToken) return requestToken;
|
|
2138
|
-
return getAuthenticatedApiKey();
|
|
2139
|
-
}
|
|
2140
|
-
async function callEdgeFunction(functionName, body, options) {
|
|
2141
|
-
const supabaseUrl = getSupabaseUrl();
|
|
2142
|
-
const apiKey = getApiKeyOrNull();
|
|
2143
|
-
const controller = new AbortController();
|
|
2144
|
-
const timeoutMs = options?.timeoutMs ?? 6e4;
|
|
2145
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2146
|
-
const enrichedBody = { ...body };
|
|
2147
|
-
if (!enrichedBody.userId && !enrichedBody.user_id) {
|
|
2148
|
-
try {
|
|
2149
|
-
const defaultId = await getDefaultUserId();
|
|
2150
|
-
enrichedBody.userId = defaultId;
|
|
2151
|
-
enrichedBody.user_id = defaultId;
|
|
2152
|
-
} catch {
|
|
2153
|
-
}
|
|
2154
|
-
} else {
|
|
2155
|
-
if (enrichedBody.userId && !enrichedBody.user_id) enrichedBody.user_id = enrichedBody.userId;
|
|
2156
|
-
if (enrichedBody.user_id && !enrichedBody.userId) enrichedBody.userId = enrichedBody.user_id;
|
|
2157
|
-
}
|
|
2158
|
-
if (!enrichedBody.projectId && !enrichedBody.project_id) {
|
|
2159
|
-
try {
|
|
2160
|
-
const { getDefaultProjectId: getDefaultProjectId2 } = await Promise.resolve().then(() => (init_supabase(), supabase_exports));
|
|
2161
|
-
const defaultProjectId = await getDefaultProjectId2();
|
|
2162
|
-
if (defaultProjectId) {
|
|
2163
|
-
enrichedBody.projectId = defaultProjectId;
|
|
2164
|
-
enrichedBody.project_id = defaultProjectId;
|
|
2165
|
-
}
|
|
2166
|
-
} catch {
|
|
2167
|
-
}
|
|
2168
|
-
}
|
|
2169
|
-
let url;
|
|
2170
|
-
let method = options?.method ?? "POST";
|
|
2171
|
-
let headers;
|
|
2172
|
-
let requestBody;
|
|
2173
|
-
if (!apiKey) {
|
|
2174
|
-
clearTimeout(timer);
|
|
2175
|
-
return {
|
|
2176
|
-
data: null,
|
|
2177
|
-
error: "Not authenticated. Run: npx @socialneuron/mcp-server login \u2014 Requires a paid plan (Starter+). See https://socialneuron.com/pricing"
|
|
2178
|
-
};
|
|
2179
|
-
}
|
|
2180
|
-
url = new URL(`${supabaseUrl}/functions/v1/mcp-gateway`);
|
|
2181
|
-
headers = {
|
|
2182
|
-
Authorization: `Bearer ${apiKey}`,
|
|
2183
|
-
"Content-Type": "application/json"
|
|
2184
|
-
};
|
|
2185
|
-
requestBody = {
|
|
2186
|
-
functionName,
|
|
2187
|
-
body: enrichedBody,
|
|
2188
|
-
query: options?.query,
|
|
2189
|
-
method: method.toUpperCase(),
|
|
2190
|
-
timeoutMs
|
|
2191
|
-
};
|
|
2192
|
-
method = "POST";
|
|
2193
|
-
try {
|
|
2194
|
-
const response = await fetch(url.toString(), {
|
|
2195
|
-
method,
|
|
2196
|
-
headers,
|
|
2197
|
-
body: method === "GET" ? void 0 : JSON.stringify(requestBody),
|
|
2198
|
-
signal: controller.signal
|
|
2199
|
-
});
|
|
2200
|
-
clearTimeout(timer);
|
|
2201
|
-
const responseText = await response.text();
|
|
2202
|
-
if (!response.ok) {
|
|
2203
|
-
const errorCode = safeGatewayError(responseText, response.status);
|
|
2204
|
-
const failureData = safeFailureData(responseText);
|
|
2205
|
-
if (response.status === 401) {
|
|
2206
|
-
return {
|
|
2207
|
-
data: failureData,
|
|
2208
|
-
error: `Authentication failed (HTTP 401). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
|
|
2209
|
-
};
|
|
2210
|
-
}
|
|
2211
|
-
if (response.status === 403) {
|
|
2212
|
-
return {
|
|
2213
|
-
data: failureData,
|
|
2214
|
-
error: `Forbidden (HTTP 403): ${errorCode}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
|
|
2215
|
-
};
|
|
2216
|
-
}
|
|
2217
|
-
if (response.status === 429) {
|
|
2218
|
-
const retryAfter = response.headers.get("retry-after") || "60";
|
|
2219
|
-
return {
|
|
2220
|
-
data: failureData,
|
|
2221
|
-
error: `Rate limit exceeded (HTTP 429). Wait ${retryAfter}s before retrying. Reduce request frequency or upgrade your plan.`
|
|
2222
|
-
};
|
|
2223
|
-
}
|
|
2224
|
-
return { data: failureData, error: errorCode };
|
|
2225
|
-
}
|
|
2226
|
-
try {
|
|
2227
|
-
const data = JSON.parse(responseText);
|
|
2228
|
-
return { data, error: null };
|
|
2229
|
-
} catch {
|
|
2230
|
-
return { data: { text: responseText }, error: null };
|
|
2231
|
-
}
|
|
2232
|
-
} catch (err) {
|
|
2233
|
-
clearTimeout(timer);
|
|
2234
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
2235
|
-
return {
|
|
2236
|
-
data: null,
|
|
2237
|
-
error: `Edge Function '${functionName}' timed out after ${timeoutMs}ms`
|
|
2238
|
-
};
|
|
2239
|
-
}
|
|
2240
|
-
return { data: null, error: "Network request failed. Please retry." };
|
|
2412
|
+
flagged_patterns: ["excessive_length"],
|
|
2413
|
+
pii_redacted: false
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
const zw = detectZeroWidth(text);
|
|
2417
|
+
if (zw.found) {
|
|
2418
|
+
flagged.add(zw.pattern);
|
|
2419
|
+
risk = Math.max(risk, 0.95);
|
|
2420
|
+
}
|
|
2421
|
+
const ipRaw = detectInstructionPhrase(text);
|
|
2422
|
+
if (ipRaw.found) {
|
|
2423
|
+
flagged.add(ipRaw.pattern);
|
|
2424
|
+
risk = Math.max(risk, 0.9);
|
|
2425
|
+
}
|
|
2426
|
+
const normalized = normalize(text);
|
|
2427
|
+
const ipNorm = detectInstructionPhrase(normalized);
|
|
2428
|
+
if (ipNorm.found) {
|
|
2429
|
+
flagged.add(ipNorm.pattern);
|
|
2430
|
+
risk = Math.max(risk, 0.9);
|
|
2431
|
+
}
|
|
2432
|
+
const flaggedArr = Array.from(flagged);
|
|
2433
|
+
if ((options.mode === "block" || options.mode === "sanitize") && flaggedArr.length > 0) {
|
|
2434
|
+
return { passed: false, risk_score: risk, flagged_patterns: flaggedArr, pii_redacted: false };
|
|
2435
|
+
}
|
|
2436
|
+
const pii = scrubPii(normalized, options.source);
|
|
2437
|
+
if (pii.redacted) {
|
|
2438
|
+
return {
|
|
2439
|
+
passed: true,
|
|
2440
|
+
risk_score: Math.max(risk, 0.3),
|
|
2441
|
+
flagged_patterns: [...flaggedArr, ...pii.patterns.map((p) => `pii_${p}`)],
|
|
2442
|
+
sanitized_text: pii.text,
|
|
2443
|
+
pii_redacted: true
|
|
2444
|
+
};
|
|
2241
2445
|
}
|
|
2446
|
+
return { passed: true, risk_score: risk, flagged_patterns: flaggedArr, pii_redacted: false };
|
|
2242
2447
|
}
|
|
2243
|
-
var
|
|
2244
|
-
|
|
2245
|
-
"src/lib/edge-function.ts"() {
|
|
2448
|
+
var init_scanner = __esm({
|
|
2449
|
+
"src/lib/agent-harness/scanner.ts"() {
|
|
2246
2450
|
"use strict";
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
"project_scope_mismatch",
|
|
2253
|
-
"schedule_conflict",
|
|
2254
|
-
"post_not_found",
|
|
2255
|
-
"post_not_reschedulable",
|
|
2256
|
-
"post_in_progress",
|
|
2257
|
-
"not_cancellable",
|
|
2258
|
-
"publishing_in_progress",
|
|
2259
|
-
"plan_upgrade_required",
|
|
2260
|
-
"rate_limited",
|
|
2261
|
-
"validation_error",
|
|
2262
|
-
"permission_denied",
|
|
2263
|
-
"not_found"
|
|
2264
|
-
]);
|
|
2265
|
-
SAFE_BILLING_STATUSES = /* @__PURE__ */ new Set([
|
|
2266
|
-
"reserved",
|
|
2267
|
-
"charged",
|
|
2268
|
-
"refunded",
|
|
2269
|
-
"failed_no_charge",
|
|
2270
|
-
"refund_pending",
|
|
2271
|
-
"not_charged"
|
|
2272
|
-
]);
|
|
2273
|
-
SAFE_JOB_STATUSES = /* @__PURE__ */ new Set([
|
|
2274
|
-
"queued",
|
|
2275
|
-
"pending",
|
|
2276
|
-
"processing",
|
|
2277
|
-
"completed",
|
|
2278
|
-
"failed",
|
|
2279
|
-
"cancelled",
|
|
2280
|
-
"canceled"
|
|
2281
|
-
]);
|
|
2451
|
+
init_constants2();
|
|
2452
|
+
init_normalize();
|
|
2453
|
+
init_zeroWidth();
|
|
2454
|
+
init_instructionPhrase();
|
|
2455
|
+
init_pii();
|
|
2282
2456
|
}
|
|
2283
2457
|
});
|
|
2284
2458
|
|
|
@@ -2974,7 +3148,10 @@ function registerContentTools(server2) {
|
|
|
2974
3148
|
isError: true
|
|
2975
3149
|
};
|
|
2976
3150
|
}
|
|
2977
|
-
const rateLimit = checkRateLimit(
|
|
3151
|
+
const rateLimit = checkRateLimit(
|
|
3152
|
+
"generation",
|
|
3153
|
+
`generate_video:${userId}`
|
|
3154
|
+
);
|
|
2978
3155
|
if (!rateLimit.allowed) {
|
|
2979
3156
|
return {
|
|
2980
3157
|
content: [
|
|
@@ -3097,7 +3274,14 @@ function registerContentTools(server2) {
|
|
|
3097
3274
|
project_id: z2.string().optional().describe("Project ID to associate the generated image with."),
|
|
3098
3275
|
response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
3099
3276
|
},
|
|
3100
|
-
async ({
|
|
3277
|
+
async ({
|
|
3278
|
+
prompt: prompt2,
|
|
3279
|
+
model,
|
|
3280
|
+
aspect_ratio,
|
|
3281
|
+
image_url,
|
|
3282
|
+
project_id,
|
|
3283
|
+
response_format
|
|
3284
|
+
}) => {
|
|
3101
3285
|
const format = response_format ?? "text";
|
|
3102
3286
|
const userId = await getDefaultUserId();
|
|
3103
3287
|
const assetBudget = checkAssetBudget();
|
|
@@ -3115,7 +3299,10 @@ function registerContentTools(server2) {
|
|
|
3115
3299
|
isError: true
|
|
3116
3300
|
};
|
|
3117
3301
|
}
|
|
3118
|
-
const rateLimit = checkRateLimit(
|
|
3302
|
+
const rateLimit = checkRateLimit(
|
|
3303
|
+
"generation",
|
|
3304
|
+
`generate_image:${userId}`
|
|
3305
|
+
);
|
|
3119
3306
|
if (!rateLimit.allowed) {
|
|
3120
3307
|
return {
|
|
3121
3308
|
content: [
|
|
@@ -3246,6 +3433,15 @@ function registerContentTools(server2) {
|
|
|
3246
3433
|
isError: true
|
|
3247
3434
|
};
|
|
3248
3435
|
}
|
|
3436
|
+
let projectsDisclosure;
|
|
3437
|
+
const ownScopedProjectId = await getDefaultProjectId();
|
|
3438
|
+
if (!ownScopedProjectId) {
|
|
3439
|
+
const ownUserId = await getDefaultUserId().catch(() => null);
|
|
3440
|
+
if (ownUserId) {
|
|
3441
|
+
const list = await listAccessibleProjectsWithAccountStatus(ownUserId);
|
|
3442
|
+
if (list.length > 0) projectsDisclosure = list;
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3249
3445
|
if (job.external_id && (job.status === "pending" || job.status === "processing")) {
|
|
3250
3446
|
const { data: liveStatus } = await callEdgeFunction(
|
|
3251
3447
|
"kie-task-status",
|
|
@@ -3269,7 +3465,9 @@ function registerContentTools(server2) {
|
|
|
3269
3465
|
if (livePayload.error) {
|
|
3270
3466
|
lines2.push(`Error: ${livePayload.error}`);
|
|
3271
3467
|
}
|
|
3272
|
-
const fallbackDisclosure2 = buildFallbackDisclosureLine(
|
|
3468
|
+
const fallbackDisclosure2 = buildFallbackDisclosureLine(
|
|
3469
|
+
job.result_metadata
|
|
3470
|
+
);
|
|
3273
3471
|
if (fallbackDisclosure2) lines2.push(fallbackDisclosure2);
|
|
3274
3472
|
lines2.push(`Credits: ${job.credits_cost}`);
|
|
3275
3473
|
if (job.billing_status) {
|
|
@@ -3278,6 +3476,14 @@ function registerContentTools(server2) {
|
|
|
3278
3476
|
);
|
|
3279
3477
|
}
|
|
3280
3478
|
lines2.push(`Created: ${job.created_at}`);
|
|
3479
|
+
if (projectsDisclosure) {
|
|
3480
|
+
lines2.push(
|
|
3481
|
+
"",
|
|
3482
|
+
`Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
|
|
3483
|
+
(p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
|
|
3484
|
+
).join("; ")}`
|
|
3485
|
+
);
|
|
3486
|
+
}
|
|
3281
3487
|
if (format === "json") {
|
|
3282
3488
|
return {
|
|
3283
3489
|
content: [
|
|
@@ -3294,7 +3500,8 @@ function registerContentTools(server2) {
|
|
|
3294
3500
|
// `job` + `liveStatus`; do not duplicate them here.
|
|
3295
3501
|
asEnvelope2({
|
|
3296
3502
|
...liveStatus,
|
|
3297
|
-
...livePayload
|
|
3503
|
+
...livePayload,
|
|
3504
|
+
...projectsDisclosure ? { projects: projectsDisclosure } : {}
|
|
3298
3505
|
}),
|
|
3299
3506
|
null,
|
|
3300
3507
|
2
|
|
@@ -3337,7 +3544,9 @@ function registerContentTools(server2) {
|
|
|
3337
3544
|
if (job.error_message) {
|
|
3338
3545
|
lines.push(`Error: ${job.error_message}`);
|
|
3339
3546
|
}
|
|
3340
|
-
const fallbackDisclosure = buildFallbackDisclosureLine(
|
|
3547
|
+
const fallbackDisclosure = buildFallbackDisclosureLine(
|
|
3548
|
+
job.result_metadata
|
|
3549
|
+
);
|
|
3341
3550
|
if (fallbackDisclosure) lines.push(fallbackDisclosure);
|
|
3342
3551
|
lines.push(`Credits: ${job.credits_cost}`);
|
|
3343
3552
|
if (job.billing_status) {
|
|
@@ -3349,10 +3558,19 @@ function registerContentTools(server2) {
|
|
|
3349
3558
|
if (job.completed_at) {
|
|
3350
3559
|
lines.push(`Completed: ${job.completed_at}`);
|
|
3351
3560
|
}
|
|
3561
|
+
if (projectsDisclosure) {
|
|
3562
|
+
lines.push(
|
|
3563
|
+
"",
|
|
3564
|
+
`Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
|
|
3565
|
+
(p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
|
|
3566
|
+
).join("; ")}`
|
|
3567
|
+
);
|
|
3568
|
+
}
|
|
3352
3569
|
if (format === "json") {
|
|
3353
3570
|
const enriched = {
|
|
3354
3571
|
...job,
|
|
3355
|
-
...buildCheckStatusPayload(job)
|
|
3572
|
+
...buildCheckStatusPayload(job),
|
|
3573
|
+
...projectsDisclosure ? { projects: projectsDisclosure } : {}
|
|
3356
3574
|
};
|
|
3357
3575
|
return {
|
|
3358
3576
|
content: [
|
|
@@ -4229,6 +4447,122 @@ var init_quality = __esm({
|
|
|
4229
4447
|
}
|
|
4230
4448
|
});
|
|
4231
4449
|
|
|
4450
|
+
// src/lib/connected-account-routing.ts
|
|
4451
|
+
function canonicalPlatform(value) {
|
|
4452
|
+
const normalized = value.trim().toLowerCase();
|
|
4453
|
+
return normalized === "x" ? "twitter" : normalized;
|
|
4454
|
+
}
|
|
4455
|
+
function providerPlatform(value) {
|
|
4456
|
+
return PLATFORM_CASE_MAP[canonicalPlatform(value)] ?? value;
|
|
4457
|
+
}
|
|
4458
|
+
function isUsable(account) {
|
|
4459
|
+
const status = account.effective_status ?? account.status;
|
|
4460
|
+
return status === "active" || status === "expires_soon";
|
|
4461
|
+
}
|
|
4462
|
+
function normalizeRequestedIds(requested) {
|
|
4463
|
+
const ids = /* @__PURE__ */ new Map();
|
|
4464
|
+
for (const [platform3, accountId] of Object.entries(requested ?? {})) {
|
|
4465
|
+
const canonical = canonicalPlatform(platform3);
|
|
4466
|
+
const existing = ids.get(canonical);
|
|
4467
|
+
if (existing && existing !== accountId) {
|
|
4468
|
+
return {
|
|
4469
|
+
error: `Conflicting account IDs were supplied for ${platform3} and its platform alias.`
|
|
4470
|
+
};
|
|
4471
|
+
}
|
|
4472
|
+
ids.set(canonical, accountId);
|
|
4473
|
+
}
|
|
4474
|
+
return { ids };
|
|
4475
|
+
}
|
|
4476
|
+
async function resolveConnectedAccountRouting(input) {
|
|
4477
|
+
const normalizedRequested = normalizeRequestedIds(input.requestedAccountIds);
|
|
4478
|
+
if (normalizedRequested.error) return { error: normalizedRequested.error };
|
|
4479
|
+
const targetPlatforms = new Set(input.platforms.map(canonicalPlatform));
|
|
4480
|
+
for (const requestedPlatform of normalizedRequested.ids?.keys() ?? []) {
|
|
4481
|
+
if (!targetPlatforms.has(requestedPlatform)) {
|
|
4482
|
+
return {
|
|
4483
|
+
error: `An account ID was supplied for untargeted platform ${requestedPlatform}.`
|
|
4484
|
+
};
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
const { data, error } = await callEdgeFunction(
|
|
4488
|
+
"mcp-data",
|
|
4489
|
+
{
|
|
4490
|
+
action: "connected-accounts",
|
|
4491
|
+
projectId: input.projectId,
|
|
4492
|
+
project_id: input.projectId
|
|
4493
|
+
},
|
|
4494
|
+
{ timeoutMs: 1e4 }
|
|
4495
|
+
);
|
|
4496
|
+
if (error || !Array.isArray(data?.accounts)) {
|
|
4497
|
+
return {
|
|
4498
|
+
error: `Connected-account verification failed: ${error ?? data?.error ?? "no account inventory returned"}.`
|
|
4499
|
+
};
|
|
4500
|
+
}
|
|
4501
|
+
const connectedAccountIds = {};
|
|
4502
|
+
for (const platform3 of input.platforms) {
|
|
4503
|
+
const canonical = canonicalPlatform(platform3);
|
|
4504
|
+
const displayPlatform = providerPlatform(platform3);
|
|
4505
|
+
const platformAccounts = data.accounts.filter(
|
|
4506
|
+
(account) => canonicalPlatform(account.platform) === canonical && account.project_id === input.projectId && isUsable(account)
|
|
4507
|
+
);
|
|
4508
|
+
const requestedId = normalizedRequested.ids?.get(canonical);
|
|
4509
|
+
let selected;
|
|
4510
|
+
if (requestedId) {
|
|
4511
|
+
selected = data.accounts.find((account) => account.id === requestedId);
|
|
4512
|
+
if (!selected) {
|
|
4513
|
+
return {
|
|
4514
|
+
error: `${displayPlatform}: account ${requestedId} is not available for project_id ${input.projectId}.`
|
|
4515
|
+
};
|
|
4516
|
+
}
|
|
4517
|
+
if (canonicalPlatform(selected.platform) !== canonical) {
|
|
4518
|
+
return {
|
|
4519
|
+
error: `${displayPlatform}: account ${requestedId} belongs to ${selected.platform}.`
|
|
4520
|
+
};
|
|
4521
|
+
}
|
|
4522
|
+
if (selected.project_id !== input.projectId) {
|
|
4523
|
+
return {
|
|
4524
|
+
error: `${displayPlatform}: account ${requestedId} is not bound to project_id ${input.projectId}.`
|
|
4525
|
+
};
|
|
4526
|
+
}
|
|
4527
|
+
if (!isUsable(selected)) {
|
|
4528
|
+
return {
|
|
4529
|
+
error: `${displayPlatform}: account ${requestedId} is ${selected.effective_status ?? selected.status}.`
|
|
4530
|
+
};
|
|
4531
|
+
}
|
|
4532
|
+
} else if (platformAccounts.length === 1) {
|
|
4533
|
+
selected = platformAccounts[0];
|
|
4534
|
+
} else if (platformAccounts.length === 0) {
|
|
4535
|
+
return {
|
|
4536
|
+
error: `${displayPlatform}: no active account is bound to project_id ${input.projectId}.`
|
|
4537
|
+
};
|
|
4538
|
+
} else {
|
|
4539
|
+
return {
|
|
4540
|
+
error: `${displayPlatform}: multiple active accounts are bound to project_id ${input.projectId}; pass the exact account ID returned by list_connected_accounts.`
|
|
4541
|
+
};
|
|
4542
|
+
}
|
|
4543
|
+
connectedAccountIds[displayPlatform] = selected.id;
|
|
4544
|
+
}
|
|
4545
|
+
return { connectedAccountIds };
|
|
4546
|
+
}
|
|
4547
|
+
var PLATFORM_CASE_MAP;
|
|
4548
|
+
var init_connected_account_routing = __esm({
|
|
4549
|
+
"src/lib/connected-account-routing.ts"() {
|
|
4550
|
+
"use strict";
|
|
4551
|
+
init_edge_function();
|
|
4552
|
+
PLATFORM_CASE_MAP = {
|
|
4553
|
+
youtube: "YouTube",
|
|
4554
|
+
tiktok: "TikTok",
|
|
4555
|
+
instagram: "Instagram",
|
|
4556
|
+
twitter: "Twitter",
|
|
4557
|
+
x: "Twitter",
|
|
4558
|
+
linkedin: "LinkedIn",
|
|
4559
|
+
facebook: "Facebook",
|
|
4560
|
+
threads: "Threads",
|
|
4561
|
+
bluesky: "Bluesky"
|
|
4562
|
+
};
|
|
4563
|
+
}
|
|
4564
|
+
});
|
|
4565
|
+
|
|
4232
4566
|
// src/tools/distribution.ts
|
|
4233
4567
|
import { z as z3 } from "zod";
|
|
4234
4568
|
import { createHash } from "node:crypto";
|
|
@@ -4341,21 +4675,6 @@ async function validatePublishMediaUrl(url) {
|
|
|
4341
4675
|
function accountEffectiveStatus(account) {
|
|
4342
4676
|
return account.effective_status || account.status;
|
|
4343
4677
|
}
|
|
4344
|
-
function isUsableAccount(account) {
|
|
4345
|
-
const status = accountEffectiveStatus(account);
|
|
4346
|
-
return status === "active" || status === "expires_soon";
|
|
4347
|
-
}
|
|
4348
|
-
function formatAccountChoice(account) {
|
|
4349
|
-
const name = account.username ? `@${account.username}` : "unnamed";
|
|
4350
|
-
const project = account.project_id ? `project_id=${account.project_id}` : "project_id=unassigned";
|
|
4351
|
-
const refresh = account.has_refresh_token ? "OAuth 2.0 refresh" : "no refresh token";
|
|
4352
|
-
return `${account.id} (${name}, ${project}, ${accountEffectiveStatus(account)}, ${refresh})`;
|
|
4353
|
-
}
|
|
4354
|
-
function requestedAccountIdForPlatform(accountId, accountIds, platform3) {
|
|
4355
|
-
if (accountId) return accountId;
|
|
4356
|
-
if (!accountIds) return void 0;
|
|
4357
|
-
return accountIds[platform3] || accountIds[platform3.toLowerCase()];
|
|
4358
|
-
}
|
|
4359
4678
|
async function rehostExternalUrl(mediaUrl, projectId) {
|
|
4360
4679
|
const ssrf = await validateUrlForSSRF(mediaUrl);
|
|
4361
4680
|
if (!ssrf.isValid) {
|
|
@@ -4491,17 +4810,17 @@ function registerDistributionTools(server2) {
|
|
|
4491
4810
|
schedule_at: z3.string().optional().describe(
|
|
4492
4811
|
'ISO 8601 UTC datetime for scheduled posting (e.g. "2026-03-20T14:00:00Z"). Omit to post immediately. Must be in the future.'
|
|
4493
4812
|
),
|
|
4494
|
-
project_id: z3.string().optional().describe(
|
|
4813
|
+
project_id: z3.string().uuid().optional().describe(
|
|
4495
4814
|
"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."
|
|
4496
4815
|
),
|
|
4497
4816
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
|
|
4498
4817
|
attribution: z3.boolean().optional().describe(
|
|
4499
4818
|
'If true, appends "Created with Social Neuron" to the caption. Default: false.'
|
|
4500
4819
|
),
|
|
4501
|
-
account_id: z3.string().optional().describe(
|
|
4502
|
-
"Connected account ID to post from.
|
|
4820
|
+
account_id: z3.string().uuid().optional().describe(
|
|
4821
|
+
"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."
|
|
4503
4822
|
),
|
|
4504
|
-
account_ids: z3.record(z3.string(), z3.string()).optional().describe(
|
|
4823
|
+
account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
|
|
4505
4824
|
'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.'
|
|
4506
4825
|
),
|
|
4507
4826
|
auto_rehost: z3.boolean().optional().describe(
|
|
@@ -4545,6 +4864,45 @@ function registerDistributionTools(server2) {
|
|
|
4545
4864
|
isError: true
|
|
4546
4865
|
};
|
|
4547
4866
|
}
|
|
4867
|
+
const projectResolution = await resolveProjectForConnectedAccountTool(
|
|
4868
|
+
project_id,
|
|
4869
|
+
platforms
|
|
4870
|
+
);
|
|
4871
|
+
if (!projectResolution.projectId) {
|
|
4872
|
+
return {
|
|
4873
|
+
content: [
|
|
4874
|
+
{
|
|
4875
|
+
type: "text",
|
|
4876
|
+
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."
|
|
4877
|
+
}
|
|
4878
|
+
],
|
|
4879
|
+
isError: true
|
|
4880
|
+
};
|
|
4881
|
+
}
|
|
4882
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
4883
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
4884
|
+
if (account_id && account_ids) {
|
|
4885
|
+
return {
|
|
4886
|
+
content: [
|
|
4887
|
+
{
|
|
4888
|
+
type: "text",
|
|
4889
|
+
text: "Pass either account_id or account_ids, not both."
|
|
4890
|
+
}
|
|
4891
|
+
],
|
|
4892
|
+
isError: true
|
|
4893
|
+
};
|
|
4894
|
+
}
|
|
4895
|
+
if (account_id && platforms.length !== 1) {
|
|
4896
|
+
return {
|
|
4897
|
+
content: [
|
|
4898
|
+
{
|
|
4899
|
+
type: "text",
|
|
4900
|
+
text: "account_id is valid only for a single target platform. Use account_ids for multi-platform publishing."
|
|
4901
|
+
}
|
|
4902
|
+
],
|
|
4903
|
+
isError: true
|
|
4904
|
+
};
|
|
4905
|
+
}
|
|
4548
4906
|
const userId = await getDefaultUserId();
|
|
4549
4907
|
const rateLimit = checkRateLimit("posting", `schedule_post:${userId}`);
|
|
4550
4908
|
if (!rateLimit.allowed) {
|
|
@@ -4649,11 +5007,16 @@ function registerDistributionTools(server2) {
|
|
|
4649
5007
|
}
|
|
4650
5008
|
const resolvedJobs = resolved;
|
|
4651
5009
|
resolvedMediaUrls = resolvedJobs.map((item) => item.url);
|
|
4652
|
-
resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map(
|
|
5010
|
+
resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map(
|
|
5011
|
+
(item) => item.trustedR2
|
|
5012
|
+
);
|
|
4653
5013
|
}
|
|
4654
5014
|
const shouldRehost = auto_rehost !== false;
|
|
4655
5015
|
if (shouldRehost && resolvedMediaUrl && !resolvedMediaUrlIsTrustedR2) {
|
|
4656
|
-
const rehost = await rehostExternalUrl(
|
|
5016
|
+
const rehost = await rehostExternalUrl(
|
|
5017
|
+
resolvedMediaUrl,
|
|
5018
|
+
resolvedProjectId
|
|
5019
|
+
);
|
|
4657
5020
|
if ("error" in rehost) {
|
|
4658
5021
|
return {
|
|
4659
5022
|
content: [
|
|
@@ -4671,7 +5034,7 @@ function registerDistributionTools(server2) {
|
|
|
4671
5034
|
if (shouldRehost && resolvedMediaUrls && resolvedMediaUrls.length > 0) {
|
|
4672
5035
|
const rehosted = await Promise.all(
|
|
4673
5036
|
resolvedMediaUrls.map(
|
|
4674
|
-
(u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u,
|
|
5037
|
+
(u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, resolvedProjectId)
|
|
4675
5038
|
)
|
|
4676
5039
|
);
|
|
4677
5040
|
const failIdx = rehosted.findIndex((r) => "error" in r);
|
|
@@ -4738,7 +5101,7 @@ function registerDistributionTools(server2) {
|
|
|
4738
5101
|
}
|
|
4739
5102
|
}
|
|
4740
5103
|
const normalizedPlatforms = platforms.map(
|
|
4741
|
-
(p) =>
|
|
5104
|
+
(p) => PLATFORM_CASE_MAP2[p.toLowerCase()] || p
|
|
4742
5105
|
);
|
|
4743
5106
|
const blockedPlatforms = normalizedPlatforms.filter(
|
|
4744
5107
|
(p) => MCP_NOT_LIVE_FOR_POSTING.has(p)
|
|
@@ -4754,92 +5117,27 @@ function registerDistributionTools(server2) {
|
|
|
4754
5117
|
isError: true
|
|
4755
5118
|
};
|
|
4756
5119
|
}
|
|
4757
|
-
|
|
4758
|
-
"mcp-data",
|
|
4759
|
-
{
|
|
4760
|
-
action: "connected-accounts",
|
|
4761
|
-
...project_id ? { projectId: project_id, project_id } : {}
|
|
4762
|
-
},
|
|
4763
|
-
{ timeoutMs: 1e4 }
|
|
4764
|
-
);
|
|
4765
|
-
if (accountsData?.accounts) {
|
|
4766
|
-
const accounts = accountsData.accounts;
|
|
4767
|
-
const issues = [];
|
|
4768
|
-
for (const platform3 of normalizedPlatforms) {
|
|
4769
|
-
const platformAccounts = accounts.filter(
|
|
4770
|
-
(a) => a.platform.toLowerCase() === platform3.toLowerCase() && isUsableAccount(a)
|
|
4771
|
-
);
|
|
4772
|
-
const requestedAccountId = requestedAccountIdForPlatform(
|
|
4773
|
-
account_id,
|
|
4774
|
-
account_ids,
|
|
4775
|
-
platform3
|
|
4776
|
-
);
|
|
4777
|
-
if (requestedAccountId) {
|
|
4778
|
-
const selected = accounts.find((a) => a.id === requestedAccountId);
|
|
4779
|
-
if (!selected) {
|
|
4780
|
-
issues.push(
|
|
4781
|
-
`${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.`
|
|
4782
|
-
);
|
|
4783
|
-
} else if (selected.platform.toLowerCase() !== platform3.toLowerCase()) {
|
|
4784
|
-
issues.push(
|
|
4785
|
-
`${platform3}: Account "${requestedAccountId}" belongs to ${selected.platform}, not ${platform3}.`
|
|
4786
|
-
);
|
|
4787
|
-
} else if (!isUsableAccount(selected)) {
|
|
4788
|
-
issues.push(
|
|
4789
|
-
`${platform3}: Account "${selected.username || selected.id}" is ${accountEffectiveStatus(selected)}. Reconnect at socialneuron.com/settings/connections.`
|
|
4790
|
-
);
|
|
4791
|
-
} else if (project_id && selected.project_id && selected.project_id !== project_id) {
|
|
4792
|
-
issues.push(
|
|
4793
|
-
`${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.`
|
|
4794
|
-
);
|
|
4795
|
-
}
|
|
4796
|
-
continue;
|
|
4797
|
-
}
|
|
4798
|
-
if (platformAccounts.length === 0) {
|
|
4799
|
-
issues.push(
|
|
4800
|
-
`${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.`
|
|
4801
|
-
);
|
|
4802
|
-
} else if (platformAccounts.length > 1) {
|
|
4803
|
-
const accountList = platformAccounts.map((a) => ` - ${formatAccountChoice(a)}`).join("\n");
|
|
4804
|
-
issues.push(
|
|
4805
|
-
`${platform3}: Multiple accounts found. Specify account_id or account_ids to choose:
|
|
4806
|
-
${accountList}`
|
|
4807
|
-
);
|
|
4808
|
-
} else if (platformAccounts.length === 1) {
|
|
4809
|
-
const acct = platformAccounts[0];
|
|
4810
|
-
if (acct.expires_at && new Date(acct.expires_at) < /* @__PURE__ */ new Date() && !acct.has_refresh_token) {
|
|
4811
|
-
issues.push(
|
|
4812
|
-
`${platform3}: Account "${acct.username || acct.id}" has expired OAuth and no refresh token. Reconnect at socialneuron.com/settings/connections.`
|
|
4813
|
-
);
|
|
4814
|
-
}
|
|
4815
|
-
}
|
|
4816
|
-
}
|
|
4817
|
-
if (issues.length > 0) {
|
|
4818
|
-
return {
|
|
4819
|
-
content: [
|
|
4820
|
-
{
|
|
4821
|
-
type: "text",
|
|
4822
|
-
text: `Cannot post \u2014 account issues found:
|
|
4823
|
-
|
|
4824
|
-
${issues.join("\n\n")}`
|
|
4825
|
-
}
|
|
4826
|
-
],
|
|
4827
|
-
isError: true
|
|
4828
|
-
};
|
|
4829
|
-
}
|
|
4830
|
-
}
|
|
4831
|
-
let connectedAccountIds;
|
|
5120
|
+
let requestedAccountIds;
|
|
4832
5121
|
if (account_id) {
|
|
4833
|
-
|
|
4834
|
-
for (const p of normalizedPlatforms) {
|
|
4835
|
-
connectedAccountIds[p] = account_id;
|
|
4836
|
-
}
|
|
5122
|
+
requestedAccountIds = { [normalizedPlatforms[0]]: account_id };
|
|
4837
5123
|
} else if (account_ids) {
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
5124
|
+
requestedAccountIds = account_ids;
|
|
5125
|
+
}
|
|
5126
|
+
const routing = await resolveConnectedAccountRouting({
|
|
5127
|
+
projectId: resolvedProjectId,
|
|
5128
|
+
platforms: normalizedPlatforms,
|
|
5129
|
+
requestedAccountIds
|
|
5130
|
+
});
|
|
5131
|
+
if (routing.error || !routing.connectedAccountIds) {
|
|
5132
|
+
return {
|
|
5133
|
+
content: [
|
|
5134
|
+
{
|
|
5135
|
+
type: "text",
|
|
5136
|
+
text: `Cannot post \u2014 ${routing.error ?? "exact connected-account routing could not be established."}`
|
|
5137
|
+
}
|
|
5138
|
+
],
|
|
5139
|
+
isError: true
|
|
5140
|
+
};
|
|
4843
5141
|
}
|
|
4844
5142
|
let finalCaption = caption;
|
|
4845
5143
|
if (attribution && finalCaption) {
|
|
@@ -4893,8 +5191,9 @@ Created with Social Neuron`;
|
|
|
4893
5191
|
title,
|
|
4894
5192
|
hashtags,
|
|
4895
5193
|
scheduledAt: schedule_at,
|
|
4896
|
-
projectId:
|
|
4897
|
-
|
|
5194
|
+
projectId: resolvedProjectId,
|
|
5195
|
+
project_id: resolvedProjectId,
|
|
5196
|
+
connectedAccountIds: routing.connectedAccountIds,
|
|
4898
5197
|
...normalizedPlatformMetadata ? {
|
|
4899
5198
|
platformMetadata: convertPlatformMetadata(
|
|
4900
5199
|
normalizedPlatformMetadata
|
|
@@ -4929,10 +5228,14 @@ Created with Social Neuron`;
|
|
|
4929
5228
|
isError: true
|
|
4930
5229
|
};
|
|
4931
5230
|
}
|
|
5231
|
+
const responseData = projectAutoResolvedNote ? { ...data, project_auto_resolved: projectAutoResolvedNote } : data;
|
|
4932
5232
|
const lines = [
|
|
4933
5233
|
data.success ? "Post scheduled successfully." : "Post scheduling had errors.",
|
|
4934
5234
|
`Scheduled for: ${data.scheduledAt}`
|
|
4935
5235
|
];
|
|
5236
|
+
if (projectAutoResolvedNote) {
|
|
5237
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
5238
|
+
}
|
|
4936
5239
|
if (tiktokAutoInboxApplied) {
|
|
4937
5240
|
lines.push(
|
|
4938
5241
|
"",
|
|
@@ -4950,7 +5253,7 @@ Created with Social Neuron`;
|
|
|
4950
5253
|
}
|
|
4951
5254
|
}
|
|
4952
5255
|
if (format === "json") {
|
|
4953
|
-
const structuredContent = asEnvelope3(
|
|
5256
|
+
const structuredContent = asEnvelope3(responseData);
|
|
4954
5257
|
return {
|
|
4955
5258
|
structuredContent,
|
|
4956
5259
|
content: [
|
|
@@ -4963,7 +5266,7 @@ Created with Social Neuron`;
|
|
|
4963
5266
|
};
|
|
4964
5267
|
}
|
|
4965
5268
|
return {
|
|
4966
|
-
structuredContent: asEnvelope3(
|
|
5269
|
+
structuredContent: asEnvelope3(responseData),
|
|
4967
5270
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
4968
5271
|
isError: !data.success
|
|
4969
5272
|
};
|
|
@@ -4977,7 +5280,9 @@ Created with Social Neuron`;
|
|
|
4977
5280
|
project_id: z3.string().uuid().optional().describe(
|
|
4978
5281
|
"Brand/project ID that owns the post. Defaults to the authenticated key's project or the account default."
|
|
4979
5282
|
),
|
|
4980
|
-
scheduled_at: z3.string().datetime({ offset: true }).describe(
|
|
5283
|
+
scheduled_at: z3.string().datetime({ offset: true }).describe(
|
|
5284
|
+
"New future publish time as an ISO 8601 datetime with timezone."
|
|
5285
|
+
),
|
|
4981
5286
|
expected_scheduled_at: z3.string().datetime({ offset: true }).optional().describe(
|
|
4982
5287
|
"Optional current schedule timestamp. If it changed since you read it, the update is rejected instead of silently overwriting it."
|
|
4983
5288
|
),
|
|
@@ -5033,7 +5338,11 @@ Created with Social Neuron`;
|
|
|
5033
5338
|
projectId: resolvedProjectId,
|
|
5034
5339
|
project_id: resolvedProjectId,
|
|
5035
5340
|
scheduled_at: next.toISOString(),
|
|
5036
|
-
...expected_scheduled_at ? {
|
|
5341
|
+
...expected_scheduled_at ? {
|
|
5342
|
+
expected_scheduled_at: new Date(
|
|
5343
|
+
expected_scheduled_at
|
|
5344
|
+
).toISOString()
|
|
5345
|
+
} : {}
|
|
5037
5346
|
});
|
|
5038
5347
|
if (error || !result?.success) {
|
|
5039
5348
|
const code = result?.error ?? error ?? "reschedule_failed";
|
|
@@ -5066,17 +5375,34 @@ Created with Social Neuron`;
|
|
|
5066
5375
|
"list_connected_accounts",
|
|
5067
5376
|
"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.",
|
|
5068
5377
|
{
|
|
5069
|
-
project_id: z3.string().optional().describe(
|
|
5378
|
+
project_id: z3.string().uuid().optional().describe(
|
|
5070
5379
|
"Brand/project ID to scope connected accounts. Use the same project_id when calling schedule_post."
|
|
5071
5380
|
),
|
|
5072
|
-
include_all: z3.boolean().optional().describe(
|
|
5381
|
+
include_all: z3.boolean().optional().describe(
|
|
5382
|
+
"If true, include expired or inactive accounts as well as usable accounts."
|
|
5383
|
+
),
|
|
5073
5384
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
5074
5385
|
},
|
|
5075
5386
|
async ({ project_id, include_all, response_format }) => {
|
|
5076
5387
|
const format = response_format ?? "text";
|
|
5388
|
+
const projectResolution = await resolveProjectForConnectedAccountTool(project_id);
|
|
5389
|
+
if (!projectResolution.projectId) {
|
|
5390
|
+
return {
|
|
5391
|
+
content: [
|
|
5392
|
+
{
|
|
5393
|
+
type: "text",
|
|
5394
|
+
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."
|
|
5395
|
+
}
|
|
5396
|
+
],
|
|
5397
|
+
isError: true
|
|
5398
|
+
};
|
|
5399
|
+
}
|
|
5400
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
5401
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
5077
5402
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
5078
5403
|
action: "connected-accounts",
|
|
5079
|
-
|
|
5404
|
+
projectId: resolvedProjectId,
|
|
5405
|
+
project_id: resolvedProjectId,
|
|
5080
5406
|
...include_all ? { includeAll: true } : {}
|
|
5081
5407
|
});
|
|
5082
5408
|
if (efError || !result?.success) {
|
|
@@ -5090,10 +5416,27 @@ Created with Social Neuron`;
|
|
|
5090
5416
|
isError: true
|
|
5091
5417
|
};
|
|
5092
5418
|
}
|
|
5093
|
-
const
|
|
5419
|
+
const parsedAccounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
|
|
5420
|
+
if (parsedAccounts.some(
|
|
5421
|
+
(account) => account.project_id !== resolvedProjectId
|
|
5422
|
+
)) {
|
|
5423
|
+
return {
|
|
5424
|
+
content: [
|
|
5425
|
+
{
|
|
5426
|
+
type: "text",
|
|
5427
|
+
text: "Connected-account project attestation failed. No account inventory was returned."
|
|
5428
|
+
}
|
|
5429
|
+
],
|
|
5430
|
+
isError: true
|
|
5431
|
+
};
|
|
5432
|
+
}
|
|
5433
|
+
const accounts = parsedAccounts;
|
|
5094
5434
|
if (accounts.length === 0) {
|
|
5095
5435
|
if (format === "json") {
|
|
5096
|
-
const structuredContent = asEnvelope3({
|
|
5436
|
+
const structuredContent = asEnvelope3({
|
|
5437
|
+
accounts: [],
|
|
5438
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
5439
|
+
});
|
|
5097
5440
|
return {
|
|
5098
5441
|
structuredContent,
|
|
5099
5442
|
content: [
|
|
@@ -5108,13 +5451,15 @@ Created with Social Neuron`;
|
|
|
5108
5451
|
content: [
|
|
5109
5452
|
{
|
|
5110
5453
|
type: "text",
|
|
5111
|
-
text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections."
|
|
5454
|
+
text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections." + (projectAutoResolvedNote ? `
|
|
5455
|
+
|
|
5456
|
+
Note: ${projectAutoResolvedNote}` : "")
|
|
5112
5457
|
}
|
|
5113
5458
|
]
|
|
5114
5459
|
};
|
|
5115
5460
|
}
|
|
5116
5461
|
const lines = [
|
|
5117
|
-
`${accounts.length} connected account(s)
|
|
5462
|
+
`${accounts.length} connected account(s) for project ${resolvedProjectId}:`,
|
|
5118
5463
|
""
|
|
5119
5464
|
];
|
|
5120
5465
|
for (const account of accounts) {
|
|
@@ -5126,8 +5471,14 @@ Created with Social Neuron`;
|
|
|
5126
5471
|
` ${platformLower}: ${name} | id=${account.id} | ${project} | status=${status} (connected ${account.created_at.split("T")[0]})`
|
|
5127
5472
|
);
|
|
5128
5473
|
}
|
|
5474
|
+
if (projectAutoResolvedNote) {
|
|
5475
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
5476
|
+
}
|
|
5129
5477
|
if (format === "json") {
|
|
5130
|
-
const structuredContent = asEnvelope3({
|
|
5478
|
+
const structuredContent = asEnvelope3({
|
|
5479
|
+
accounts,
|
|
5480
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
5481
|
+
});
|
|
5131
5482
|
return {
|
|
5132
5483
|
structuredContent,
|
|
5133
5484
|
content: [
|
|
@@ -5322,7 +5673,10 @@ Created with Social Neuron`;
|
|
|
5322
5673
|
if (!Number.isFinite(startDate.getTime())) {
|
|
5323
5674
|
return {
|
|
5324
5675
|
content: [
|
|
5325
|
-
{
|
|
5676
|
+
{
|
|
5677
|
+
type: "text",
|
|
5678
|
+
text: "start_after must be a valid ISO datetime."
|
|
5679
|
+
}
|
|
5326
5680
|
],
|
|
5327
5681
|
isError: true
|
|
5328
5682
|
};
|
|
@@ -5423,11 +5777,14 @@ Created with Social Neuron`;
|
|
|
5423
5777
|
"Schedule all posts in a content plan. Optionally auto-assigns time slots and runs quality checks before scheduling. Supports dry-run mode.",
|
|
5424
5778
|
{
|
|
5425
5779
|
plan: z3.object({
|
|
5780
|
+
project_id: z3.string().uuid().optional(),
|
|
5781
|
+
account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe("Exact connected-account ID per platform."),
|
|
5426
5782
|
posts: z3.array(
|
|
5427
5783
|
z3.object({
|
|
5428
5784
|
id: z3.string(),
|
|
5429
5785
|
caption: z3.string(),
|
|
5430
5786
|
platform: z3.string(),
|
|
5787
|
+
connected_account_id: z3.string().uuid().optional(),
|
|
5431
5788
|
title: z3.string().optional(),
|
|
5432
5789
|
media_url: z3.string().optional(),
|
|
5433
5790
|
schedule_at: z3.string().optional(),
|
|
@@ -5435,6 +5792,12 @@ Created with Social Neuron`;
|
|
|
5435
5792
|
})
|
|
5436
5793
|
)
|
|
5437
5794
|
}).passthrough().optional(),
|
|
5795
|
+
project_id: z3.string().uuid().optional().describe(
|
|
5796
|
+
"Exact brand/project ID. Defaults only when the authenticated user has one project."
|
|
5797
|
+
),
|
|
5798
|
+
account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
|
|
5799
|
+
"Exact connected-account ID per platform for every post in the plan."
|
|
5800
|
+
),
|
|
5438
5801
|
plan_id: z3.string().uuid().optional().describe("Persisted content plan ID from content_plans table"),
|
|
5439
5802
|
auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
|
|
5440
5803
|
dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
|
|
@@ -5451,6 +5814,8 @@ Created with Social Neuron`;
|
|
|
5451
5814
|
async ({
|
|
5452
5815
|
plan,
|
|
5453
5816
|
plan_id,
|
|
5817
|
+
project_id,
|
|
5818
|
+
account_ids,
|
|
5454
5819
|
auto_slot,
|
|
5455
5820
|
dry_run,
|
|
5456
5821
|
response_format,
|
|
@@ -5460,9 +5825,11 @@ Created with Social Neuron`;
|
|
|
5460
5825
|
idempotency_seed
|
|
5461
5826
|
}) => {
|
|
5462
5827
|
try {
|
|
5828
|
+
const effectiveBatchSize = batch_size ?? 4;
|
|
5463
5829
|
let workingPlan = plan;
|
|
5464
5830
|
let effectivePlanId = plan_id;
|
|
5465
|
-
let effectiveProjectId;
|
|
5831
|
+
let effectiveProjectId = project_id;
|
|
5832
|
+
let projectAutoResolvedNote;
|
|
5466
5833
|
let approvalSummary;
|
|
5467
5834
|
if (!workingPlan && plan_id) {
|
|
5468
5835
|
const { data: planResult, error: planError } = await callEdgeFunction("mcp-data", { action: "get-content-plan", plan_id });
|
|
@@ -5509,7 +5876,18 @@ Created with Social Neuron`;
|
|
|
5509
5876
|
posts: postsFromPayload
|
|
5510
5877
|
};
|
|
5511
5878
|
effectivePlanId = stored.id;
|
|
5512
|
-
effectiveProjectId
|
|
5879
|
+
if (effectiveProjectId && stored.project_id && effectiveProjectId !== stored.project_id) {
|
|
5880
|
+
return {
|
|
5881
|
+
content: [
|
|
5882
|
+
{
|
|
5883
|
+
type: "text",
|
|
5884
|
+
text: `project_id ${effectiveProjectId} does not own plan ${plan_id}.`
|
|
5885
|
+
}
|
|
5886
|
+
],
|
|
5887
|
+
isError: true
|
|
5888
|
+
};
|
|
5889
|
+
}
|
|
5890
|
+
effectiveProjectId = stored.project_id ?? effectiveProjectId;
|
|
5513
5891
|
}
|
|
5514
5892
|
if (!workingPlan) {
|
|
5515
5893
|
return {
|
|
@@ -5522,10 +5900,35 @@ Created with Social Neuron`;
|
|
|
5522
5900
|
isError: true
|
|
5523
5901
|
};
|
|
5524
5902
|
}
|
|
5903
|
+
const planProjectId = workingPlan.project_id;
|
|
5904
|
+
if (effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0 && effectiveProjectId !== planProjectId) {
|
|
5905
|
+
return {
|
|
5906
|
+
content: [
|
|
5907
|
+
{
|
|
5908
|
+
type: "text",
|
|
5909
|
+
text: `Conflicting project_id values were supplied for the plan (${planProjectId}) and request (${effectiveProjectId}).`
|
|
5910
|
+
}
|
|
5911
|
+
],
|
|
5912
|
+
isError: true
|
|
5913
|
+
};
|
|
5914
|
+
}
|
|
5915
|
+
if (!effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0) {
|
|
5916
|
+
effectiveProjectId = planProjectId;
|
|
5917
|
+
}
|
|
5525
5918
|
if (!effectiveProjectId) {
|
|
5526
|
-
const
|
|
5527
|
-
|
|
5528
|
-
|
|
5919
|
+
const projectResolution = await resolveProjectForConnectedAccountTool();
|
|
5920
|
+
effectiveProjectId = projectResolution.projectId;
|
|
5921
|
+
projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
5922
|
+
if (!effectiveProjectId) {
|
|
5923
|
+
return {
|
|
5924
|
+
content: [
|
|
5925
|
+
{
|
|
5926
|
+
type: "text",
|
|
5927
|
+
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."
|
|
5928
|
+
}
|
|
5929
|
+
],
|
|
5930
|
+
isError: true
|
|
5931
|
+
};
|
|
5529
5932
|
}
|
|
5530
5933
|
}
|
|
5531
5934
|
if (effectivePlanId) {
|
|
@@ -5744,6 +6147,61 @@ Created with Social Neuron`;
|
|
|
5744
6147
|
isError: false
|
|
5745
6148
|
};
|
|
5746
6149
|
}
|
|
6150
|
+
const embeddedAccountIds = workingPlan.account_ids ?? {};
|
|
6151
|
+
for (const [platform3, accountId] of Object.entries(account_ids ?? {})) {
|
|
6152
|
+
if (embeddedAccountIds[platform3] && embeddedAccountIds[platform3] !== accountId) {
|
|
6153
|
+
return {
|
|
6154
|
+
content: [
|
|
6155
|
+
{
|
|
6156
|
+
type: "text",
|
|
6157
|
+
text: `Conflicting account_ids values were supplied for ${platform3}.`
|
|
6158
|
+
}
|
|
6159
|
+
],
|
|
6160
|
+
isError: true
|
|
6161
|
+
};
|
|
6162
|
+
}
|
|
6163
|
+
}
|
|
6164
|
+
const requestedPlanAccountIds = {
|
|
6165
|
+
...embeddedAccountIds,
|
|
6166
|
+
...account_ids ?? {}
|
|
6167
|
+
};
|
|
6168
|
+
for (const post of workingPlan.posts) {
|
|
6169
|
+
if (!post.connected_account_id) continue;
|
|
6170
|
+
const key = post.platform.toLowerCase() === "x" ? "twitter" : post.platform.toLowerCase();
|
|
6171
|
+
const existing = requestedPlanAccountIds[key];
|
|
6172
|
+
if (existing && existing !== post.connected_account_id) {
|
|
6173
|
+
return {
|
|
6174
|
+
content: [
|
|
6175
|
+
{
|
|
6176
|
+
type: "text",
|
|
6177
|
+
text: `Plan contains conflicting connected_account_id values for ${post.platform}. Use separate plans when scheduling the same platform through different accounts.`
|
|
6178
|
+
}
|
|
6179
|
+
],
|
|
6180
|
+
isError: true
|
|
6181
|
+
};
|
|
6182
|
+
}
|
|
6183
|
+
requestedPlanAccountIds[key] = post.connected_account_id;
|
|
6184
|
+
}
|
|
6185
|
+
const planPlatforms = Array.from(
|
|
6186
|
+
new Set(workingPlan.posts.map((post) => post.platform))
|
|
6187
|
+
);
|
|
6188
|
+
const planRouting = await resolveConnectedAccountRouting({
|
|
6189
|
+
projectId: effectiveProjectId,
|
|
6190
|
+
platforms: planPlatforms,
|
|
6191
|
+
requestedAccountIds: requestedPlanAccountIds
|
|
6192
|
+
});
|
|
6193
|
+
if (planRouting.error || !planRouting.connectedAccountIds) {
|
|
6194
|
+
return {
|
|
6195
|
+
content: [
|
|
6196
|
+
{
|
|
6197
|
+
type: "text",
|
|
6198
|
+
text: `Cannot schedule plan \u2014 ${planRouting.error ?? "exact connected-account routing could not be established."}`
|
|
6199
|
+
}
|
|
6200
|
+
],
|
|
6201
|
+
isError: true
|
|
6202
|
+
};
|
|
6203
|
+
}
|
|
6204
|
+
const verifiedPlanAccountIds = planRouting.connectedAccountIds;
|
|
5747
6205
|
let scheduled = 0;
|
|
5748
6206
|
let failed = 0;
|
|
5749
6207
|
const results = [];
|
|
@@ -5772,7 +6230,7 @@ Created with Social Neuron`;
|
|
|
5772
6230
|
retryable: false
|
|
5773
6231
|
};
|
|
5774
6232
|
}
|
|
5775
|
-
const normalizedPlatform =
|
|
6233
|
+
const normalizedPlatform = PLATFORM_CASE_MAP2[post.platform.toLowerCase()] ?? post.platform;
|
|
5776
6234
|
const idempotencyKey = buildIdempotencyKey(post);
|
|
5777
6235
|
const { data, error } = await callEdgeFunction(
|
|
5778
6236
|
"schedule-post",
|
|
@@ -5783,6 +6241,13 @@ Created with Social Neuron`;
|
|
|
5783
6241
|
mediaUrl: post.media_url,
|
|
5784
6242
|
scheduledAt: post.schedule_at,
|
|
5785
6243
|
hashtags: post.hashtags,
|
|
6244
|
+
...effectiveProjectId ? {
|
|
6245
|
+
projectId: effectiveProjectId,
|
|
6246
|
+
project_id: effectiveProjectId
|
|
6247
|
+
} : {},
|
|
6248
|
+
connectedAccountIds: {
|
|
6249
|
+
[normalizedPlatform]: verifiedPlanAccountIds[normalizedPlatform]
|
|
6250
|
+
},
|
|
5786
6251
|
...effectivePlanId ? { planId: effectivePlanId } : {},
|
|
5787
6252
|
idempotencyKey
|
|
5788
6253
|
},
|
|
@@ -5841,7 +6306,7 @@ Created with Social Neuron`;
|
|
|
5841
6306
|
const platformBatches = Array.from(grouped.entries()).map(
|
|
5842
6307
|
async ([platform3, platformPosts]) => {
|
|
5843
6308
|
const platformResults = [];
|
|
5844
|
-
const batches = chunk(platformPosts,
|
|
6309
|
+
const batches = chunk(platformPosts, effectiveBatchSize);
|
|
5845
6310
|
for (const batch of batches) {
|
|
5846
6311
|
const settled = await Promise.allSettled(
|
|
5847
6312
|
batch.map((post) => scheduleOne(post))
|
|
@@ -5902,7 +6367,8 @@ Created with Social Neuron`;
|
|
|
5902
6367
|
total_posts: workingPlan.posts.length,
|
|
5903
6368
|
scheduled,
|
|
5904
6369
|
failed
|
|
5905
|
-
}
|
|
6370
|
+
},
|
|
6371
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
5906
6372
|
}),
|
|
5907
6373
|
null,
|
|
5908
6374
|
2
|
|
@@ -5926,6 +6392,9 @@ Created with Social Neuron`;
|
|
|
5926
6392
|
lines.push(
|
|
5927
6393
|
`Scheduled: ${scheduled}/${workingPlan.posts.length} | Failed: ${failed}/${workingPlan.posts.length}`
|
|
5928
6394
|
);
|
|
6395
|
+
if (projectAutoResolvedNote) {
|
|
6396
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
6397
|
+
}
|
|
5929
6398
|
return {
|
|
5930
6399
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
5931
6400
|
isError: failed > 0
|
|
@@ -5945,7 +6414,7 @@ Created with Social Neuron`;
|
|
|
5945
6414
|
}
|
|
5946
6415
|
);
|
|
5947
6416
|
}
|
|
5948
|
-
var
|
|
6417
|
+
var PLATFORM_CASE_MAP2, TIKTOK_AUDIT_APPROVED, MCP_NOT_LIVE_FOR_POSTING, VIDEO_FILE_EXTENSIONS, IMAGE_FILE_EXTENSIONS;
|
|
5949
6418
|
var init_distribution = __esm({
|
|
5950
6419
|
"src/tools/distribution.ts"() {
|
|
5951
6420
|
"use strict";
|
|
@@ -5956,11 +6425,17 @@ var init_distribution = __esm({
|
|
|
5956
6425
|
init_supabase();
|
|
5957
6426
|
init_quality();
|
|
5958
6427
|
init_version();
|
|
5959
|
-
|
|
6428
|
+
init_connected_account_routing();
|
|
6429
|
+
PLATFORM_CASE_MAP2 = {
|
|
5960
6430
|
youtube: "YouTube",
|
|
5961
6431
|
tiktok: "TikTok",
|
|
5962
6432
|
instagram: "Instagram",
|
|
5963
6433
|
twitter: "Twitter",
|
|
6434
|
+
// 'x' is the platform's current branding but connected_account_routing.ts
|
|
6435
|
+
// (and the DB convention) still key on 'Twitter' — keep both aliases
|
|
6436
|
+
// resolving to the same case so schedule_content_plan's platform:'x' posts
|
|
6437
|
+
// don't fall through to an undefined binding (F8, 2026-07-15).
|
|
6438
|
+
x: "Twitter",
|
|
5964
6439
|
linkedin: "LinkedIn",
|
|
5965
6440
|
facebook: "Facebook",
|
|
5966
6441
|
threads: "Threads",
|
|
@@ -6502,15 +6977,35 @@ function registerAnalyticsTools(server2) {
|
|
|
6502
6977
|
content_id: z5.string().uuid().optional().describe(
|
|
6503
6978
|
"Filter to a specific content_history ID to see performance of one piece of content."
|
|
6504
6979
|
),
|
|
6505
|
-
project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
6980
|
+
project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
|
|
6506
6981
|
limit: z5.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
|
|
6507
6982
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6508
6983
|
},
|
|
6509
|
-
async ({
|
|
6984
|
+
async ({
|
|
6985
|
+
platform: platform3,
|
|
6986
|
+
days,
|
|
6987
|
+
content_id,
|
|
6988
|
+
project_id,
|
|
6989
|
+
limit,
|
|
6990
|
+
response_format
|
|
6991
|
+
}) => {
|
|
6510
6992
|
const format = response_format ?? "text";
|
|
6511
6993
|
const lookbackDays = days ?? 30;
|
|
6512
6994
|
const maxPosts = limit ?? 20;
|
|
6513
|
-
const
|
|
6995
|
+
const projectResolution = await resolveProjectStrict(project_id);
|
|
6996
|
+
if (!projectResolution.projectId) {
|
|
6997
|
+
return {
|
|
6998
|
+
content: [
|
|
6999
|
+
{
|
|
7000
|
+
type: "text",
|
|
7001
|
+
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."
|
|
7002
|
+
}
|
|
7003
|
+
],
|
|
7004
|
+
isError: true
|
|
7005
|
+
};
|
|
7006
|
+
}
|
|
7007
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
7008
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
6514
7009
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
6515
7010
|
action: "analytics",
|
|
6516
7011
|
platform: platform3,
|
|
@@ -6521,11 +7016,17 @@ function registerAnalyticsTools(server2) {
|
|
|
6521
7016
|
limit: Math.min(maxPosts * 5, 100),
|
|
6522
7017
|
latestOnly: true,
|
|
6523
7018
|
contentId: content_id,
|
|
6524
|
-
|
|
7019
|
+
projectId: resolvedProjectId,
|
|
7020
|
+
project_id: resolvedProjectId
|
|
6525
7021
|
});
|
|
6526
7022
|
if (efError) {
|
|
6527
7023
|
return {
|
|
6528
|
-
content: [
|
|
7024
|
+
content: [
|
|
7025
|
+
{
|
|
7026
|
+
type: "text",
|
|
7027
|
+
text: `Failed to fetch analytics: ${efError}`
|
|
7028
|
+
}
|
|
7029
|
+
],
|
|
6529
7030
|
isError: true
|
|
6530
7031
|
};
|
|
6531
7032
|
}
|
|
@@ -6545,7 +7046,8 @@ function registerAnalyticsTools(server2) {
|
|
|
6545
7046
|
totalViews: 0,
|
|
6546
7047
|
totalEngagement: 0,
|
|
6547
7048
|
postCount: 0,
|
|
6548
|
-
posts: []
|
|
7049
|
+
posts: [],
|
|
7050
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
6549
7051
|
});
|
|
6550
7052
|
return {
|
|
6551
7053
|
structuredContent,
|
|
@@ -6561,7 +7063,9 @@ function registerAnalyticsTools(server2) {
|
|
|
6561
7063
|
content: [
|
|
6562
7064
|
{
|
|
6563
7065
|
type: "text",
|
|
6564
|
-
text: `No analytics data found for the last ${lookbackDays} days${platform3 ? ` on ${platform3}` : ""}.`
|
|
7066
|
+
text: `No analytics data found for the last ${lookbackDays} days${platform3 ? ` on ${platform3}` : ""}.` + (projectAutoResolvedNote ? `
|
|
7067
|
+
|
|
7068
|
+
Note: ${projectAutoResolvedNote}` : "")
|
|
6565
7069
|
}
|
|
6566
7070
|
]
|
|
6567
7071
|
};
|
|
@@ -6593,20 +7097,27 @@ function registerAnalyticsTools(server2) {
|
|
|
6593
7097
|
postCount: posts.length,
|
|
6594
7098
|
posts
|
|
6595
7099
|
};
|
|
6596
|
-
return formatAnalytics(
|
|
7100
|
+
return formatAnalytics(
|
|
7101
|
+
summary,
|
|
7102
|
+
lookbackDays,
|
|
7103
|
+
format,
|
|
7104
|
+
projectAutoResolvedNote
|
|
7105
|
+
);
|
|
6597
7106
|
}
|
|
6598
7107
|
);
|
|
6599
7108
|
server2.tool(
|
|
6600
7109
|
"refresh_platform_analytics",
|
|
6601
7110
|
"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.",
|
|
6602
7111
|
{
|
|
6603
|
-
project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
7112
|
+
project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
|
|
6604
7113
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6605
7114
|
},
|
|
6606
7115
|
async ({ project_id, response_format }) => {
|
|
6607
7116
|
const format = response_format ?? "text";
|
|
6608
7117
|
const userId = await getDefaultUserId();
|
|
6609
|
-
const
|
|
7118
|
+
const projectResolution = await resolveProjectStrict(project_id);
|
|
7119
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
7120
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
6610
7121
|
const rateLimit = checkRateLimit(
|
|
6611
7122
|
"posting",
|
|
6612
7123
|
`refresh_platform_analytics:${userId}:${resolvedProjectId ?? "default"}`
|
|
@@ -6622,25 +7133,48 @@ function registerAnalyticsTools(server2) {
|
|
|
6622
7133
|
isError: true
|
|
6623
7134
|
};
|
|
6624
7135
|
}
|
|
7136
|
+
if (!resolvedProjectId) {
|
|
7137
|
+
return {
|
|
7138
|
+
content: [
|
|
7139
|
+
{
|
|
7140
|
+
type: "text",
|
|
7141
|
+
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."
|
|
7142
|
+
}
|
|
7143
|
+
],
|
|
7144
|
+
isError: true
|
|
7145
|
+
};
|
|
7146
|
+
}
|
|
6625
7147
|
const { data, error } = await callEdgeFunction("fetch-analytics", {
|
|
6626
7148
|
userId,
|
|
6627
|
-
|
|
7149
|
+
projectId: resolvedProjectId,
|
|
7150
|
+
project_id: resolvedProjectId
|
|
6628
7151
|
});
|
|
6629
7152
|
if (error) {
|
|
6630
7153
|
return {
|
|
6631
|
-
content: [
|
|
7154
|
+
content: [
|
|
7155
|
+
{
|
|
7156
|
+
type: "text",
|
|
7157
|
+
text: `Error refreshing analytics: ${error}`
|
|
7158
|
+
}
|
|
7159
|
+
],
|
|
6632
7160
|
isError: true
|
|
6633
7161
|
};
|
|
6634
7162
|
}
|
|
6635
7163
|
const result = data;
|
|
6636
7164
|
if (!result.success) {
|
|
6637
7165
|
return {
|
|
6638
|
-
content: [
|
|
7166
|
+
content: [
|
|
7167
|
+
{ type: "text", text: "Analytics refresh failed." }
|
|
7168
|
+
],
|
|
6639
7169
|
isError: true
|
|
6640
7170
|
};
|
|
6641
7171
|
}
|
|
6642
|
-
const queued = (result.results ?? []).filter(
|
|
6643
|
-
|
|
7172
|
+
const queued = (result.results ?? []).filter(
|
|
7173
|
+
(r) => r.status === "queued"
|
|
7174
|
+
).length;
|
|
7175
|
+
const errored = (result.results ?? []).filter(
|
|
7176
|
+
(r) => r.status === "error"
|
|
7177
|
+
).length;
|
|
6644
7178
|
const lines = [
|
|
6645
7179
|
`Analytics refresh triggered successfully.`,
|
|
6646
7180
|
` Posts processed: ${result.postsProcessed}`,
|
|
@@ -6649,13 +7183,17 @@ function registerAnalyticsTools(server2) {
|
|
|
6649
7183
|
if (errored > 0) {
|
|
6650
7184
|
lines.push(` Errors: ${errored}`);
|
|
6651
7185
|
}
|
|
7186
|
+
if (projectAutoResolvedNote) {
|
|
7187
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
7188
|
+
}
|
|
6652
7189
|
if (format === "json") {
|
|
6653
7190
|
const structuredContent = asEnvelope4({
|
|
6654
7191
|
success: true,
|
|
6655
7192
|
postsProcessed: result.postsProcessed,
|
|
6656
7193
|
queued,
|
|
6657
7194
|
errored,
|
|
6658
|
-
projectId: resolvedProjectId ?? null
|
|
7195
|
+
projectId: resolvedProjectId ?? null,
|
|
7196
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
6659
7197
|
});
|
|
6660
7198
|
return {
|
|
6661
7199
|
structuredContent,
|
|
@@ -6671,12 +7209,21 @@ function registerAnalyticsTools(server2) {
|
|
|
6671
7209
|
}
|
|
6672
7210
|
);
|
|
6673
7211
|
}
|
|
6674
|
-
function formatAnalytics(summary, days, format) {
|
|
6675
|
-
const structuredContent = asEnvelope4({
|
|
7212
|
+
function formatAnalytics(summary, days, format, projectAutoResolvedNote) {
|
|
7213
|
+
const structuredContent = asEnvelope4({
|
|
7214
|
+
...summary,
|
|
7215
|
+
days,
|
|
7216
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
7217
|
+
});
|
|
6676
7218
|
if (format === "json") {
|
|
6677
7219
|
return {
|
|
6678
7220
|
structuredContent,
|
|
6679
|
-
content: [
|
|
7221
|
+
content: [
|
|
7222
|
+
{
|
|
7223
|
+
type: "text",
|
|
7224
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
7225
|
+
}
|
|
7226
|
+
]
|
|
6680
7227
|
};
|
|
6681
7228
|
}
|
|
6682
7229
|
const lines = [
|
|
@@ -6700,6 +7247,9 @@ function formatAnalytics(summary, days, format) {
|
|
|
6700
7247
|
lines.push(line);
|
|
6701
7248
|
}
|
|
6702
7249
|
}
|
|
7250
|
+
if (projectAutoResolvedNote) {
|
|
7251
|
+
lines.push("", `Note: ${projectAutoResolvedNote}`);
|
|
7252
|
+
}
|
|
6703
7253
|
return {
|
|
6704
7254
|
structuredContent,
|
|
6705
7255
|
content: [{ type: "text", text: lines.join("\n") }]
|
|
@@ -8129,15 +8679,64 @@ function registerYouTubeAnalyticsTools(server2) {
|
|
|
8129
8679
|
start_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Start date in YYYY-MM-DD format."),
|
|
8130
8680
|
end_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("End date in YYYY-MM-DD format."),
|
|
8131
8681
|
video_id: z10.string().optional().describe('YouTube video ID. Required when action is "video".'),
|
|
8132
|
-
max_results: z10.number().min(1).max(50).optional().describe(
|
|
8682
|
+
max_results: z10.number().min(1).max(50).optional().describe(
|
|
8683
|
+
'Max videos to return for "topVideos" action. Defaults to 10.'
|
|
8684
|
+
),
|
|
8685
|
+
connected_account_id: z10.string().uuid().optional().describe(
|
|
8686
|
+
"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."
|
|
8687
|
+
),
|
|
8688
|
+
project_id: z10.string().uuid().optional().describe(
|
|
8689
|
+
"Exact brand/project ID. Defaults only when the authenticated user has one project."
|
|
8690
|
+
),
|
|
8133
8691
|
response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8134
8692
|
},
|
|
8135
|
-
async ({
|
|
8693
|
+
async ({
|
|
8694
|
+
action,
|
|
8695
|
+
start_date,
|
|
8696
|
+
end_date,
|
|
8697
|
+
video_id,
|
|
8698
|
+
max_results,
|
|
8699
|
+
connected_account_id,
|
|
8700
|
+
project_id,
|
|
8701
|
+
response_format
|
|
8702
|
+
}) => {
|
|
8136
8703
|
const format = response_format ?? "text";
|
|
8137
8704
|
if (action === "video" && !video_id) {
|
|
8138
8705
|
return {
|
|
8139
8706
|
content: [
|
|
8140
|
-
{
|
|
8707
|
+
{
|
|
8708
|
+
type: "text",
|
|
8709
|
+
text: 'Error: video_id is required when action is "video".'
|
|
8710
|
+
}
|
|
8711
|
+
],
|
|
8712
|
+
isError: true
|
|
8713
|
+
};
|
|
8714
|
+
}
|
|
8715
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
8716
|
+
if (!resolvedProjectId) {
|
|
8717
|
+
return {
|
|
8718
|
+
content: [
|
|
8719
|
+
{
|
|
8720
|
+
type: "text",
|
|
8721
|
+
text: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
|
|
8722
|
+
}
|
|
8723
|
+
],
|
|
8724
|
+
isError: true
|
|
8725
|
+
};
|
|
8726
|
+
}
|
|
8727
|
+
const routing = await resolveConnectedAccountRouting({
|
|
8728
|
+
projectId: resolvedProjectId,
|
|
8729
|
+
platforms: ["youtube"],
|
|
8730
|
+
requestedAccountIds: connected_account_id ? { youtube: connected_account_id } : void 0
|
|
8731
|
+
});
|
|
8732
|
+
const resolvedAccountId = routing.connectedAccountIds?.YouTube;
|
|
8733
|
+
if (routing.error || !resolvedAccountId) {
|
|
8734
|
+
return {
|
|
8735
|
+
content: [
|
|
8736
|
+
{
|
|
8737
|
+
type: "text",
|
|
8738
|
+
text: routing.error ?? "YouTube: exact connected-account routing could not be established."
|
|
8739
|
+
}
|
|
8141
8740
|
],
|
|
8142
8741
|
isError: true
|
|
8143
8742
|
};
|
|
@@ -8147,11 +8746,19 @@ function registerYouTubeAnalyticsTools(server2) {
|
|
|
8147
8746
|
startDate: start_date,
|
|
8148
8747
|
endDate: end_date,
|
|
8149
8748
|
videoId: video_id,
|
|
8150
|
-
maxResults: max_results ?? 10
|
|
8749
|
+
maxResults: max_results ?? 10,
|
|
8750
|
+
projectId: resolvedProjectId,
|
|
8751
|
+
project_id: resolvedProjectId,
|
|
8752
|
+
connectedAccountId: resolvedAccountId
|
|
8151
8753
|
});
|
|
8152
8754
|
if (error) {
|
|
8153
8755
|
return {
|
|
8154
|
-
content: [
|
|
8756
|
+
content: [
|
|
8757
|
+
{
|
|
8758
|
+
type: "text",
|
|
8759
|
+
text: `YouTube Analytics error: ${error}`
|
|
8760
|
+
}
|
|
8761
|
+
],
|
|
8155
8762
|
isError: true
|
|
8156
8763
|
};
|
|
8157
8764
|
}
|
|
@@ -8164,7 +8771,12 @@ function registerYouTubeAnalyticsTools(server2) {
|
|
|
8164
8771
|
{
|
|
8165
8772
|
type: "text",
|
|
8166
8773
|
text: JSON.stringify(
|
|
8167
|
-
asEnvelope7({
|
|
8774
|
+
asEnvelope7({
|
|
8775
|
+
action,
|
|
8776
|
+
startDate: start_date,
|
|
8777
|
+
endDate: end_date,
|
|
8778
|
+
analytics: a
|
|
8779
|
+
}),
|
|
8168
8780
|
null,
|
|
8169
8781
|
2
|
|
8170
8782
|
)
|
|
@@ -8191,7 +8803,10 @@ function registerYouTubeAnalyticsTools(server2) {
|
|
|
8191
8803
|
if (days.length === 0) {
|
|
8192
8804
|
return {
|
|
8193
8805
|
content: [
|
|
8194
|
-
{
|
|
8806
|
+
{
|
|
8807
|
+
type: "text",
|
|
8808
|
+
text: "No daily analytics data found for this period."
|
|
8809
|
+
}
|
|
8195
8810
|
]
|
|
8196
8811
|
};
|
|
8197
8812
|
}
|
|
@@ -8214,7 +8829,10 @@ function registerYouTubeAnalyticsTools(server2) {
|
|
|
8214
8829
|
]
|
|
8215
8830
|
};
|
|
8216
8831
|
}
|
|
8217
|
-
const lines = [
|
|
8832
|
+
const lines = [
|
|
8833
|
+
`YouTube Daily Analytics (${start_date} to ${end_date}):`,
|
|
8834
|
+
""
|
|
8835
|
+
];
|
|
8218
8836
|
for (const d of days) {
|
|
8219
8837
|
lines.push(
|
|
8220
8838
|
` ${d.date}: ${d.views.toLocaleString()} views, ${d.watchTimeMinutes.toLocaleString()} min watch, +${d.subscribersGained} subs, ${d.likes} likes, ${d.comments} comments`
|
|
@@ -8261,7 +8879,12 @@ function registerYouTubeAnalyticsTools(server2) {
|
|
|
8261
8879
|
const videos = result.topVideos ?? [];
|
|
8262
8880
|
if (videos.length === 0) {
|
|
8263
8881
|
return {
|
|
8264
|
-
content: [
|
|
8882
|
+
content: [
|
|
8883
|
+
{
|
|
8884
|
+
type: "text",
|
|
8885
|
+
text: "No top videos found for this period."
|
|
8886
|
+
}
|
|
8887
|
+
]
|
|
8265
8888
|
};
|
|
8266
8889
|
}
|
|
8267
8890
|
if (format === "json") {
|
|
@@ -8283,7 +8906,10 @@ function registerYouTubeAnalyticsTools(server2) {
|
|
|
8283
8906
|
]
|
|
8284
8907
|
};
|
|
8285
8908
|
}
|
|
8286
|
-
const lines = [
|
|
8909
|
+
const lines = [
|
|
8910
|
+
`Top ${videos.length} YouTube Videos (${start_date} to ${end_date}):`,
|
|
8911
|
+
""
|
|
8912
|
+
];
|
|
8287
8913
|
for (let i = 0; i < videos.length; i++) {
|
|
8288
8914
|
const v = videos[i];
|
|
8289
8915
|
lines.push(
|
|
@@ -8296,11 +8922,18 @@ function registerYouTubeAnalyticsTools(server2) {
|
|
|
8296
8922
|
}
|
|
8297
8923
|
if (format === "json") {
|
|
8298
8924
|
return {
|
|
8299
|
-
content: [
|
|
8925
|
+
content: [
|
|
8926
|
+
{
|
|
8927
|
+
type: "text",
|
|
8928
|
+
text: JSON.stringify(asEnvelope7(result), null, 2)
|
|
8929
|
+
}
|
|
8930
|
+
]
|
|
8300
8931
|
};
|
|
8301
8932
|
}
|
|
8302
8933
|
return {
|
|
8303
|
-
content: [
|
|
8934
|
+
content: [
|
|
8935
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
8936
|
+
]
|
|
8304
8937
|
};
|
|
8305
8938
|
}
|
|
8306
8939
|
);
|
|
@@ -8309,6 +8942,8 @@ var init_youtube_analytics = __esm({
|
|
|
8309
8942
|
"src/tools/youtube-analytics.ts"() {
|
|
8310
8943
|
"use strict";
|
|
8311
8944
|
init_edge_function();
|
|
8945
|
+
init_supabase();
|
|
8946
|
+
init_connected_account_routing();
|
|
8312
8947
|
init_version();
|
|
8313
8948
|
}
|
|
8314
8949
|
});
|
|
@@ -8324,6 +8959,29 @@ function asEnvelope8(data) {
|
|
|
8324
8959
|
data
|
|
8325
8960
|
};
|
|
8326
8961
|
}
|
|
8962
|
+
async function exactYouTubeRoute(projectId, connectedAccountId) {
|
|
8963
|
+
const resolvedProjectId = projectId ?? await getDefaultProjectId() ?? void 0;
|
|
8964
|
+
if (!resolvedProjectId) {
|
|
8965
|
+
return {
|
|
8966
|
+
error: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
|
|
8967
|
+
};
|
|
8968
|
+
}
|
|
8969
|
+
const routing = await resolveConnectedAccountRouting({
|
|
8970
|
+
projectId: resolvedProjectId,
|
|
8971
|
+
platforms: ["youtube"],
|
|
8972
|
+
requestedAccountIds: connectedAccountId ? { youtube: connectedAccountId } : void 0
|
|
8973
|
+
});
|
|
8974
|
+
const resolvedAccountId = routing.connectedAccountIds?.YouTube;
|
|
8975
|
+
if (routing.error || !resolvedAccountId) {
|
|
8976
|
+
return {
|
|
8977
|
+
error: routing.error ?? "YouTube: exact connected-account routing could not be established."
|
|
8978
|
+
};
|
|
8979
|
+
}
|
|
8980
|
+
return {
|
|
8981
|
+
projectId: resolvedProjectId,
|
|
8982
|
+
connectedAccountId: resolvedAccountId
|
|
8983
|
+
};
|
|
8984
|
+
}
|
|
8327
8985
|
function registerCommentsTools(server2) {
|
|
8328
8986
|
server2.tool(
|
|
8329
8987
|
"list_comments",
|
|
@@ -8336,19 +8994,42 @@ function registerCommentsTools(server2) {
|
|
|
8336
8994
|
page_token: z11.string().optional().describe(
|
|
8337
8995
|
"Pagination cursor from previous list_comments response nextPageToken field. Omit for first page of results."
|
|
8338
8996
|
),
|
|
8997
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
8998
|
+
"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."
|
|
8999
|
+
),
|
|
9000
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
8339
9001
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8340
9002
|
},
|
|
8341
|
-
async ({
|
|
9003
|
+
async ({
|
|
9004
|
+
video_id,
|
|
9005
|
+
max_results,
|
|
9006
|
+
page_token,
|
|
9007
|
+
connected_account_id,
|
|
9008
|
+
project_id,
|
|
9009
|
+
response_format
|
|
9010
|
+
}) => {
|
|
8342
9011
|
const format = response_format ?? "text";
|
|
9012
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
9013
|
+
if ("error" in route) {
|
|
9014
|
+
return {
|
|
9015
|
+
content: [{ type: "text", text: route.error }],
|
|
9016
|
+
isError: true
|
|
9017
|
+
};
|
|
9018
|
+
}
|
|
8343
9019
|
const { data, error } = await callEdgeFunction("youtube-comments", {
|
|
8344
9020
|
action: "list",
|
|
8345
9021
|
videoId: video_id,
|
|
8346
9022
|
maxResults: max_results ?? 50,
|
|
8347
|
-
pageToken: page_token
|
|
9023
|
+
pageToken: page_token,
|
|
9024
|
+
projectId: route.projectId,
|
|
9025
|
+
project_id: route.projectId,
|
|
9026
|
+
connectedAccountId: route.connectedAccountId
|
|
8348
9027
|
});
|
|
8349
9028
|
if (error) {
|
|
8350
9029
|
return {
|
|
8351
|
-
content: [
|
|
9030
|
+
content: [
|
|
9031
|
+
{ type: "text", text: `Error listing comments: ${error}` }
|
|
9032
|
+
],
|
|
8352
9033
|
isError: true
|
|
8353
9034
|
};
|
|
8354
9035
|
}
|
|
@@ -8360,7 +9041,10 @@ function registerCommentsTools(server2) {
|
|
|
8360
9041
|
{
|
|
8361
9042
|
type: "text",
|
|
8362
9043
|
text: JSON.stringify(
|
|
8363
|
-
asEnvelope8({
|
|
9044
|
+
asEnvelope8({
|
|
9045
|
+
comments,
|
|
9046
|
+
nextPageToken: result.nextPageToken ?? null
|
|
9047
|
+
}),
|
|
8364
9048
|
null,
|
|
8365
9049
|
2
|
|
8366
9050
|
)
|
|
@@ -8397,12 +9081,31 @@ function registerCommentsTools(server2) {
|
|
|
8397
9081
|
"reply_to_comment",
|
|
8398
9082
|
"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.",
|
|
8399
9083
|
{
|
|
8400
|
-
parent_id: z11.string().describe(
|
|
9084
|
+
parent_id: z11.string().describe(
|
|
9085
|
+
"The ID of the parent comment to reply to (from list_comments)."
|
|
9086
|
+
),
|
|
8401
9087
|
text: z11.string().min(1).describe("The reply text."),
|
|
9088
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
9089
|
+
"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."
|
|
9090
|
+
),
|
|
9091
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
8402
9092
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8403
9093
|
},
|
|
8404
|
-
async ({
|
|
9094
|
+
async ({
|
|
9095
|
+
parent_id,
|
|
9096
|
+
text,
|
|
9097
|
+
connected_account_id,
|
|
9098
|
+
project_id,
|
|
9099
|
+
response_format
|
|
9100
|
+
}) => {
|
|
8405
9101
|
const format = response_format ?? "text";
|
|
9102
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
9103
|
+
if ("error" in route) {
|
|
9104
|
+
return {
|
|
9105
|
+
content: [{ type: "text", text: route.error }],
|
|
9106
|
+
isError: true
|
|
9107
|
+
};
|
|
9108
|
+
}
|
|
8406
9109
|
const userId = await getDefaultUserId();
|
|
8407
9110
|
const rateLimit = checkRateLimit("posting", `reply_to_comment:${userId}`);
|
|
8408
9111
|
if (!rateLimit.allowed) {
|
|
@@ -8419,18 +9122,31 @@ function registerCommentsTools(server2) {
|
|
|
8419
9122
|
const { data, error } = await callEdgeFunction("youtube-comments", {
|
|
8420
9123
|
action: "reply",
|
|
8421
9124
|
parentId: parent_id,
|
|
8422
|
-
text
|
|
9125
|
+
text,
|
|
9126
|
+
projectId: route.projectId,
|
|
9127
|
+
project_id: route.projectId,
|
|
9128
|
+
connectedAccountId: route.connectedAccountId
|
|
8423
9129
|
});
|
|
8424
9130
|
if (error) {
|
|
8425
9131
|
return {
|
|
8426
|
-
content: [
|
|
9132
|
+
content: [
|
|
9133
|
+
{
|
|
9134
|
+
type: "text",
|
|
9135
|
+
text: `Error replying to comment: ${error}`
|
|
9136
|
+
}
|
|
9137
|
+
],
|
|
8427
9138
|
isError: true
|
|
8428
9139
|
};
|
|
8429
9140
|
}
|
|
8430
9141
|
const result = data;
|
|
8431
9142
|
if (format === "json") {
|
|
8432
9143
|
return {
|
|
8433
|
-
content: [
|
|
9144
|
+
content: [
|
|
9145
|
+
{
|
|
9146
|
+
type: "text",
|
|
9147
|
+
text: JSON.stringify(asEnvelope8(result), null, 2)
|
|
9148
|
+
}
|
|
9149
|
+
]
|
|
8434
9150
|
};
|
|
8435
9151
|
}
|
|
8436
9152
|
return {
|
|
@@ -8451,10 +9167,27 @@ function registerCommentsTools(server2) {
|
|
|
8451
9167
|
{
|
|
8452
9168
|
video_id: z11.string().describe("The YouTube video ID to comment on."),
|
|
8453
9169
|
text: z11.string().min(1).describe("The comment text."),
|
|
9170
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
9171
|
+
"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."
|
|
9172
|
+
),
|
|
9173
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
8454
9174
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8455
9175
|
},
|
|
8456
|
-
async ({
|
|
9176
|
+
async ({
|
|
9177
|
+
video_id,
|
|
9178
|
+
text,
|
|
9179
|
+
connected_account_id,
|
|
9180
|
+
project_id,
|
|
9181
|
+
response_format
|
|
9182
|
+
}) => {
|
|
8457
9183
|
const format = response_format ?? "text";
|
|
9184
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
9185
|
+
if ("error" in route) {
|
|
9186
|
+
return {
|
|
9187
|
+
content: [{ type: "text", text: route.error }],
|
|
9188
|
+
isError: true
|
|
9189
|
+
};
|
|
9190
|
+
}
|
|
8458
9191
|
const userId = await getDefaultUserId();
|
|
8459
9192
|
const rateLimit = checkRateLimit("posting", `post_comment:${userId}`);
|
|
8460
9193
|
if (!rateLimit.allowed) {
|
|
@@ -8471,18 +9204,28 @@ function registerCommentsTools(server2) {
|
|
|
8471
9204
|
const { data, error } = await callEdgeFunction("youtube-comments", {
|
|
8472
9205
|
action: "post",
|
|
8473
9206
|
videoId: video_id,
|
|
8474
|
-
text
|
|
9207
|
+
text,
|
|
9208
|
+
projectId: route.projectId,
|
|
9209
|
+
project_id: route.projectId,
|
|
9210
|
+
connectedAccountId: route.connectedAccountId
|
|
8475
9211
|
});
|
|
8476
9212
|
if (error) {
|
|
8477
9213
|
return {
|
|
8478
|
-
content: [
|
|
9214
|
+
content: [
|
|
9215
|
+
{ type: "text", text: `Error posting comment: ${error}` }
|
|
9216
|
+
],
|
|
8479
9217
|
isError: true
|
|
8480
9218
|
};
|
|
8481
9219
|
}
|
|
8482
9220
|
const result = data;
|
|
8483
9221
|
if (format === "json") {
|
|
8484
9222
|
return {
|
|
8485
|
-
content: [
|
|
9223
|
+
content: [
|
|
9224
|
+
{
|
|
9225
|
+
type: "text",
|
|
9226
|
+
text: JSON.stringify(asEnvelope8(result), null, 2)
|
|
9227
|
+
}
|
|
9228
|
+
]
|
|
8486
9229
|
};
|
|
8487
9230
|
}
|
|
8488
9231
|
return {
|
|
@@ -8503,10 +9246,27 @@ function registerCommentsTools(server2) {
|
|
|
8503
9246
|
{
|
|
8504
9247
|
comment_id: z11.string().describe("The comment ID to moderate."),
|
|
8505
9248
|
moderation_status: z11.enum(["published", "rejected"]).describe('"published" to approve, "rejected" to hide.'),
|
|
9249
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
9250
|
+
"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."
|
|
9251
|
+
),
|
|
9252
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
8506
9253
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8507
9254
|
},
|
|
8508
|
-
async ({
|
|
9255
|
+
async ({
|
|
9256
|
+
comment_id,
|
|
9257
|
+
moderation_status,
|
|
9258
|
+
connected_account_id,
|
|
9259
|
+
project_id,
|
|
9260
|
+
response_format
|
|
9261
|
+
}) => {
|
|
8509
9262
|
const format = response_format ?? "text";
|
|
9263
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
9264
|
+
if ("error" in route) {
|
|
9265
|
+
return {
|
|
9266
|
+
content: [{ type: "text", text: route.error }],
|
|
9267
|
+
isError: true
|
|
9268
|
+
};
|
|
9269
|
+
}
|
|
8510
9270
|
const userId = await getDefaultUserId();
|
|
8511
9271
|
const rateLimit = checkRateLimit("posting", `moderate_comment:${userId}`);
|
|
8512
9272
|
if (!rateLimit.allowed) {
|
|
@@ -8523,11 +9283,19 @@ function registerCommentsTools(server2) {
|
|
|
8523
9283
|
const { error } = await callEdgeFunction("youtube-comments", {
|
|
8524
9284
|
action: "moderate",
|
|
8525
9285
|
commentId: comment_id,
|
|
8526
|
-
moderationStatus: moderation_status
|
|
9286
|
+
moderationStatus: moderation_status,
|
|
9287
|
+
projectId: route.projectId,
|
|
9288
|
+
project_id: route.projectId,
|
|
9289
|
+
connectedAccountId: route.connectedAccountId
|
|
8527
9290
|
});
|
|
8528
9291
|
if (error) {
|
|
8529
9292
|
return {
|
|
8530
|
-
content: [
|
|
9293
|
+
content: [
|
|
9294
|
+
{
|
|
9295
|
+
type: "text",
|
|
9296
|
+
text: `Error moderating comment: ${error}`
|
|
9297
|
+
}
|
|
9298
|
+
],
|
|
8531
9299
|
isError: true
|
|
8532
9300
|
};
|
|
8533
9301
|
}
|
|
@@ -8564,10 +9332,26 @@ function registerCommentsTools(server2) {
|
|
|
8564
9332
|
"Delete a YouTube comment. Only works for comments owned by the authenticated channel.",
|
|
8565
9333
|
{
|
|
8566
9334
|
comment_id: z11.string().describe("The comment ID to delete."),
|
|
9335
|
+
connected_account_id: z11.string().uuid().optional().describe(
|
|
9336
|
+
"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."
|
|
9337
|
+
),
|
|
9338
|
+
project_id: PROJECT_ID_SCHEMA,
|
|
8567
9339
|
response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8568
9340
|
},
|
|
8569
|
-
async ({
|
|
9341
|
+
async ({
|
|
9342
|
+
comment_id,
|
|
9343
|
+
connected_account_id,
|
|
9344
|
+
project_id,
|
|
9345
|
+
response_format
|
|
9346
|
+
}) => {
|
|
8570
9347
|
const format = response_format ?? "text";
|
|
9348
|
+
const route = await exactYouTubeRoute(project_id, connected_account_id);
|
|
9349
|
+
if ("error" in route) {
|
|
9350
|
+
return {
|
|
9351
|
+
content: [{ type: "text", text: route.error }],
|
|
9352
|
+
isError: true
|
|
9353
|
+
};
|
|
9354
|
+
}
|
|
8571
9355
|
const userId = await getDefaultUserId();
|
|
8572
9356
|
const rateLimit = checkRateLimit("posting", `delete_comment:${userId}`);
|
|
8573
9357
|
if (!rateLimit.allowed) {
|
|
@@ -8583,11 +9367,16 @@ function registerCommentsTools(server2) {
|
|
|
8583
9367
|
}
|
|
8584
9368
|
const { error } = await callEdgeFunction("youtube-comments", {
|
|
8585
9369
|
action: "delete",
|
|
8586
|
-
commentId: comment_id
|
|
9370
|
+
commentId: comment_id,
|
|
9371
|
+
projectId: route.projectId,
|
|
9372
|
+
project_id: route.projectId,
|
|
9373
|
+
connectedAccountId: route.connectedAccountId
|
|
8587
9374
|
});
|
|
8588
9375
|
if (error) {
|
|
8589
9376
|
return {
|
|
8590
|
-
content: [
|
|
9377
|
+
content: [
|
|
9378
|
+
{ type: "text", text: `Error deleting comment: ${error}` }
|
|
9379
|
+
],
|
|
8591
9380
|
isError: true
|
|
8592
9381
|
};
|
|
8593
9382
|
}
|
|
@@ -8596,24 +9385,38 @@ function registerCommentsTools(server2) {
|
|
|
8596
9385
|
content: [
|
|
8597
9386
|
{
|
|
8598
9387
|
type: "text",
|
|
8599
|
-
text: JSON.stringify(
|
|
9388
|
+
text: JSON.stringify(
|
|
9389
|
+
asEnvelope8({ success: true, commentId: comment_id }),
|
|
9390
|
+
null,
|
|
9391
|
+
2
|
|
9392
|
+
)
|
|
8600
9393
|
}
|
|
8601
9394
|
]
|
|
8602
9395
|
};
|
|
8603
9396
|
}
|
|
8604
9397
|
return {
|
|
8605
|
-
content: [
|
|
9398
|
+
content: [
|
|
9399
|
+
{
|
|
9400
|
+
type: "text",
|
|
9401
|
+
text: `Comment ${comment_id} deleted successfully.`
|
|
9402
|
+
}
|
|
9403
|
+
]
|
|
8606
9404
|
};
|
|
8607
9405
|
}
|
|
8608
9406
|
);
|
|
8609
9407
|
}
|
|
9408
|
+
var PROJECT_ID_SCHEMA;
|
|
8610
9409
|
var init_comments = __esm({
|
|
8611
9410
|
"src/tools/comments.ts"() {
|
|
8612
9411
|
"use strict";
|
|
8613
9412
|
init_edge_function();
|
|
8614
9413
|
init_rate_limit();
|
|
8615
9414
|
init_supabase();
|
|
9415
|
+
init_connected_account_routing();
|
|
8616
9416
|
init_version();
|
|
9417
|
+
PROJECT_ID_SCHEMA = z11.string().uuid().optional().describe(
|
|
9418
|
+
"Exact brand/project ID. Defaults only when the authenticated user has one project."
|
|
9419
|
+
);
|
|
8617
9420
|
}
|
|
8618
9421
|
});
|
|
8619
9422
|
|
|
@@ -11606,7 +12409,10 @@ var init_discovery = __esm({
|
|
|
11606
12409
|
import { z as z24 } from "zod";
|
|
11607
12410
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
11608
12411
|
function asEnvelope20(data) {
|
|
11609
|
-
return {
|
|
12412
|
+
return {
|
|
12413
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
12414
|
+
data
|
|
12415
|
+
};
|
|
11610
12416
|
}
|
|
11611
12417
|
function registerPipelineTools(server2) {
|
|
11612
12418
|
server2.tool(
|
|
@@ -11629,7 +12435,10 @@ function registerPipelineTools(server2) {
|
|
|
11629
12435
|
action: "pipeline-readiness",
|
|
11630
12436
|
platforms,
|
|
11631
12437
|
estimated_posts,
|
|
11632
|
-
...resolvedProjectId ? {
|
|
12438
|
+
...resolvedProjectId ? {
|
|
12439
|
+
projectId: resolvedProjectId,
|
|
12440
|
+
project_id: resolvedProjectId
|
|
12441
|
+
} : {}
|
|
11633
12442
|
},
|
|
11634
12443
|
{ timeoutMs: 15e3 }
|
|
11635
12444
|
);
|
|
@@ -11647,16 +12456,24 @@ function registerPipelineTools(server2) {
|
|
|
11647
12456
|
const blockers = [];
|
|
11648
12457
|
const warnings = [];
|
|
11649
12458
|
if (!isUnlimited && credits < estimatedCost) {
|
|
11650
|
-
blockers.push(
|
|
12459
|
+
blockers.push(
|
|
12460
|
+
`Insufficient credits: ${credits} available, ~${estimatedCost} needed`
|
|
12461
|
+
);
|
|
11651
12462
|
}
|
|
11652
12463
|
if (missingPlatforms.length > 0) {
|
|
11653
|
-
blockers.push(
|
|
12464
|
+
blockers.push(
|
|
12465
|
+
`Missing connected accounts: ${missingPlatforms.join(", ")}`
|
|
12466
|
+
);
|
|
11654
12467
|
}
|
|
11655
12468
|
if (!hasBrand) {
|
|
11656
|
-
warnings.push(
|
|
12469
|
+
warnings.push(
|
|
12470
|
+
"No brand profile found. Content will use generic voice."
|
|
12471
|
+
);
|
|
11657
12472
|
}
|
|
11658
12473
|
if (pendingApprovals > 0) {
|
|
11659
|
-
warnings.push(
|
|
12474
|
+
warnings.push(
|
|
12475
|
+
`${pendingApprovals} pending approval(s) from previous runs.`
|
|
12476
|
+
);
|
|
11660
12477
|
}
|
|
11661
12478
|
if (!insightsFresh) {
|
|
11662
12479
|
warnings.push(
|
|
@@ -11671,7 +12488,10 @@ function registerPipelineTools(server2) {
|
|
|
11671
12488
|
estimated_cost: estimatedCost,
|
|
11672
12489
|
sufficient: credits >= estimatedCost
|
|
11673
12490
|
},
|
|
11674
|
-
connected_accounts: {
|
|
12491
|
+
connected_accounts: {
|
|
12492
|
+
platforms: connectedPlatforms,
|
|
12493
|
+
missing: missingPlatforms
|
|
12494
|
+
},
|
|
11675
12495
|
brand_profile: { exists: hasBrand },
|
|
11676
12496
|
pending_approvals: { count: pendingApprovals },
|
|
11677
12497
|
insights_available: {
|
|
@@ -11685,11 +12505,18 @@ function registerPipelineTools(server2) {
|
|
|
11685
12505
|
};
|
|
11686
12506
|
if (format === "json") {
|
|
11687
12507
|
return {
|
|
11688
|
-
content: [
|
|
12508
|
+
content: [
|
|
12509
|
+
{
|
|
12510
|
+
type: "text",
|
|
12511
|
+
text: JSON.stringify(asEnvelope20(result), null, 2)
|
|
12512
|
+
}
|
|
12513
|
+
]
|
|
11689
12514
|
};
|
|
11690
12515
|
}
|
|
11691
12516
|
const lines = [];
|
|
11692
|
-
lines.push(
|
|
12517
|
+
lines.push(
|
|
12518
|
+
`Pipeline Readiness: ${result.ready ? "READY" : "NOT READY"}`
|
|
12519
|
+
);
|
|
11693
12520
|
lines.push("=".repeat(40));
|
|
11694
12521
|
lines.push(
|
|
11695
12522
|
`Credits: ${credits} available, ~${estimatedCost} needed \u2014 ${credits >= estimatedCost ? "OK" : "BLOCKED"}`
|
|
@@ -11697,7 +12524,9 @@ function registerPipelineTools(server2) {
|
|
|
11697
12524
|
lines.push(
|
|
11698
12525
|
`Accounts: ${connectedPlatforms.length} connected${missingPlatforms.length > 0 ? ` (missing: ${missingPlatforms.join(", ")})` : " \u2014 OK"}`
|
|
11699
12526
|
);
|
|
11700
|
-
lines.push(
|
|
12527
|
+
lines.push(
|
|
12528
|
+
`Brand: ${hasBrand ? "OK" : "Missing (will use generic voice)"}`
|
|
12529
|
+
);
|
|
11701
12530
|
lines.push(`Pending Approvals: ${pendingApprovals}`);
|
|
11702
12531
|
lines.push(
|
|
11703
12532
|
`Insights: ${insightsFresh ? "Fresh" : insightAge === null ? "None available" : `${insightAge} days old`}`
|
|
@@ -11716,7 +12545,12 @@ function registerPipelineTools(server2) {
|
|
|
11716
12545
|
} catch (err) {
|
|
11717
12546
|
const message = sanitizeError(err);
|
|
11718
12547
|
return {
|
|
11719
|
-
content: [
|
|
12548
|
+
content: [
|
|
12549
|
+
{
|
|
12550
|
+
type: "text",
|
|
12551
|
+
text: `Readiness check failed: ${message}`
|
|
12552
|
+
}
|
|
12553
|
+
],
|
|
11720
12554
|
isError: true
|
|
11721
12555
|
};
|
|
11722
12556
|
}
|
|
@@ -11730,6 +12564,9 @@ function registerPipelineTools(server2) {
|
|
|
11730
12564
|
topic: z24.string().optional().describe("Content topic (required if no source_url)"),
|
|
11731
12565
|
source_url: z24.string().optional().describe("URL to extract content from"),
|
|
11732
12566
|
platforms: z24.array(PLATFORM_ENUM2).min(1).describe("Target platforms"),
|
|
12567
|
+
account_ids: z24.record(z24.string(), z24.string().uuid()).optional().describe(
|
|
12568
|
+
"Exact connected-account ID per target platform. Required when a project has multiple accounts on one platform."
|
|
12569
|
+
),
|
|
11733
12570
|
days: z24.number().min(1).max(7).default(5).describe("Days to plan"),
|
|
11734
12571
|
posts_per_day: z24.number().min(1).max(3).default(1).describe("Posts per platform per day"),
|
|
11735
12572
|
approval_mode: z24.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe(
|
|
@@ -11751,6 +12588,7 @@ function registerPipelineTools(server2) {
|
|
|
11751
12588
|
topic,
|
|
11752
12589
|
source_url,
|
|
11753
12590
|
platforms,
|
|
12591
|
+
account_ids,
|
|
11754
12592
|
days,
|
|
11755
12593
|
posts_per_day,
|
|
11756
12594
|
approval_mode,
|
|
@@ -11768,7 +12606,12 @@ function registerPipelineTools(server2) {
|
|
|
11768
12606
|
let creditsUsed = 0;
|
|
11769
12607
|
if (!topic && !source_url) {
|
|
11770
12608
|
return {
|
|
11771
|
-
content: [
|
|
12609
|
+
content: [
|
|
12610
|
+
{
|
|
12611
|
+
type: "text",
|
|
12612
|
+
text: "Either topic or source_url is required."
|
|
12613
|
+
}
|
|
12614
|
+
],
|
|
11772
12615
|
isError: true
|
|
11773
12616
|
};
|
|
11774
12617
|
}
|
|
@@ -11798,6 +12641,35 @@ function registerPipelineTools(server2) {
|
|
|
11798
12641
|
}
|
|
11799
12642
|
try {
|
|
11800
12643
|
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
12644
|
+
if (schedulingRequested && !resolvedProjectId) {
|
|
12645
|
+
return {
|
|
12646
|
+
content: [
|
|
12647
|
+
{
|
|
12648
|
+
type: "text",
|
|
12649
|
+
text: "A project_id is required to schedule pipeline output. Configure an explicit project or use an API key scoped to exactly one project."
|
|
12650
|
+
}
|
|
12651
|
+
],
|
|
12652
|
+
isError: true
|
|
12653
|
+
};
|
|
12654
|
+
}
|
|
12655
|
+
if (schedulingRequested) {
|
|
12656
|
+
const preflightRouting = await resolveConnectedAccountRouting({
|
|
12657
|
+
projectId: resolvedProjectId,
|
|
12658
|
+
platforms,
|
|
12659
|
+
requestedAccountIds: account_ids
|
|
12660
|
+
});
|
|
12661
|
+
if (preflightRouting.error || !preflightRouting.connectedAccountIds) {
|
|
12662
|
+
return {
|
|
12663
|
+
content: [
|
|
12664
|
+
{
|
|
12665
|
+
type: "text",
|
|
12666
|
+
text: `Cannot run publishing pipeline \u2014 ${preflightRouting.error ?? "exact connected-account routing could not be established."} (checked before any credits were spent.)`
|
|
12667
|
+
}
|
|
12668
|
+
],
|
|
12669
|
+
isError: true
|
|
12670
|
+
};
|
|
12671
|
+
}
|
|
12672
|
+
}
|
|
11801
12673
|
const estimatedCost = BASE_PLAN_CREDITS + (source_url ? SOURCE_EXTRACTION_CREDITS : 0);
|
|
11802
12674
|
const { data: budgetData } = await callEdgeFunction(
|
|
11803
12675
|
"mcp-data",
|
|
@@ -11851,7 +12723,13 @@ function registerPipelineTools(server2) {
|
|
|
11851
12723
|
"social-neuron-ai",
|
|
11852
12724
|
{
|
|
11853
12725
|
type: "generation",
|
|
11854
|
-
prompt: buildPlanPrompt(
|
|
12726
|
+
prompt: buildPlanPrompt(
|
|
12727
|
+
resolvedTopic,
|
|
12728
|
+
platforms,
|
|
12729
|
+
days,
|
|
12730
|
+
posts_per_day,
|
|
12731
|
+
source_url
|
|
12732
|
+
),
|
|
11855
12733
|
model: "gemini-2.5-flash",
|
|
11856
12734
|
responseFormat: "json",
|
|
11857
12735
|
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
@@ -11859,7 +12737,10 @@ function registerPipelineTools(server2) {
|
|
|
11859
12737
|
{ timeoutMs: 6e4 }
|
|
11860
12738
|
);
|
|
11861
12739
|
if (planError || !planData) {
|
|
11862
|
-
errors.push({
|
|
12740
|
+
errors.push({
|
|
12741
|
+
stage: "planning",
|
|
12742
|
+
message: planError ?? "No AI response"
|
|
12743
|
+
});
|
|
11863
12744
|
await callEdgeFunction(
|
|
11864
12745
|
"mcp-data",
|
|
11865
12746
|
{
|
|
@@ -11876,7 +12757,10 @@ function registerPipelineTools(server2) {
|
|
|
11876
12757
|
);
|
|
11877
12758
|
return {
|
|
11878
12759
|
content: [
|
|
11879
|
-
{
|
|
12760
|
+
{
|
|
12761
|
+
type: "text",
|
|
12762
|
+
text: `Planning failed: ${planError ?? "No AI response"}`
|
|
12763
|
+
}
|
|
11880
12764
|
],
|
|
11881
12765
|
isError: true
|
|
11882
12766
|
};
|
|
@@ -11906,20 +12790,22 @@ function registerPipelineTools(server2) {
|
|
|
11906
12790
|
const postsArray = extractJsonArray(rawText);
|
|
11907
12791
|
const requestedPlatformSet = new Set(platforms);
|
|
11908
12792
|
const maxPosts = platforms.length * days * posts_per_day;
|
|
11909
|
-
const parsedPosts = (postsArray ?? []).map(
|
|
11910
|
-
|
|
11911
|
-
|
|
11912
|
-
|
|
11913
|
-
|
|
11914
|
-
|
|
11915
|
-
|
|
11916
|
-
|
|
11917
|
-
|
|
11918
|
-
|
|
11919
|
-
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
12793
|
+
const parsedPosts = (postsArray ?? []).map(
|
|
12794
|
+
(p) => ({
|
|
12795
|
+
id: String(p.id ?? randomUUID3().slice(0, 8)),
|
|
12796
|
+
day: Number(p.day ?? 1),
|
|
12797
|
+
date: String(p.date ?? ""),
|
|
12798
|
+
platform: String(p.platform ?? ""),
|
|
12799
|
+
content_type: p.content_type ?? "caption",
|
|
12800
|
+
caption: String(p.caption ?? ""),
|
|
12801
|
+
title: p.title ? String(p.title) : void 0,
|
|
12802
|
+
hashtags: Array.isArray(p.hashtags) ? p.hashtags.map(String) : void 0,
|
|
12803
|
+
hook: String(p.hook ?? ""),
|
|
12804
|
+
angle: String(p.angle ?? ""),
|
|
12805
|
+
visual_direction: p.visual_direction ? String(p.visual_direction) : void 0,
|
|
12806
|
+
media_type: p.media_type ? String(p.media_type) : void 0
|
|
12807
|
+
})
|
|
12808
|
+
);
|
|
11923
12809
|
const platformFilteredPosts = parsedPosts.filter(
|
|
11924
12810
|
(post) => requestedPlatformSet.has(post.platform)
|
|
11925
12811
|
);
|
|
@@ -12002,7 +12888,10 @@ function registerPipelineTools(server2) {
|
|
|
12002
12888
|
estimated_credits: estimatedCost,
|
|
12003
12889
|
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
12004
12890
|
},
|
|
12005
|
-
...resolvedProjectId ? {
|
|
12891
|
+
...resolvedProjectId ? {
|
|
12892
|
+
projectId: resolvedProjectId,
|
|
12893
|
+
project_id: resolvedProjectId
|
|
12894
|
+
} : {}
|
|
12006
12895
|
},
|
|
12007
12896
|
{ timeoutMs: 1e4 }
|
|
12008
12897
|
);
|
|
@@ -12055,6 +12944,22 @@ function registerPipelineTools(server2) {
|
|
|
12055
12944
|
}
|
|
12056
12945
|
}
|
|
12057
12946
|
let postsScheduled = 0;
|
|
12947
|
+
const pipelineRouting = schedulingRequested && postsApproved > 0 ? await resolveConnectedAccountRouting({
|
|
12948
|
+
projectId: resolvedProjectId,
|
|
12949
|
+
platforms,
|
|
12950
|
+
requestedAccountIds: account_ids
|
|
12951
|
+
}) : void 0;
|
|
12952
|
+
if (pipelineRouting?.error || schedulingRequested && postsApproved > 0 && !pipelineRouting?.connectedAccountIds) {
|
|
12953
|
+
return {
|
|
12954
|
+
content: [
|
|
12955
|
+
{
|
|
12956
|
+
type: "text",
|
|
12957
|
+
text: `Cannot run publishing pipeline \u2014 ${pipelineRouting?.error ?? "exact connected-account routing could not be established."}`
|
|
12958
|
+
}
|
|
12959
|
+
],
|
|
12960
|
+
isError: true
|
|
12961
|
+
};
|
|
12962
|
+
}
|
|
12058
12963
|
if (!dry_run && !skipSet.has("schedule") && postsApproved > 0) {
|
|
12059
12964
|
const approvedPosts = posts.filter((p) => p.status === "approved");
|
|
12060
12965
|
const scheduleBase = /* @__PURE__ */ new Date();
|
|
@@ -12062,10 +12967,27 @@ function registerPipelineTools(server2) {
|
|
|
12062
12967
|
const scheduleBaseMs = scheduleBase.getTime();
|
|
12063
12968
|
for (const post of approvedPosts) {
|
|
12064
12969
|
if (creditsUsed >= creditLimit) {
|
|
12065
|
-
errors.push({
|
|
12970
|
+
errors.push({
|
|
12971
|
+
stage: "schedule",
|
|
12972
|
+
message: "Credit limit reached"
|
|
12973
|
+
});
|
|
12066
12974
|
break;
|
|
12067
12975
|
}
|
|
12068
|
-
const scheduledAt = post.schedule_at ?? new Date(
|
|
12976
|
+
const scheduledAt = post.schedule_at ?? new Date(
|
|
12977
|
+
scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5
|
|
12978
|
+
).toISOString();
|
|
12979
|
+
const route = Object.entries(
|
|
12980
|
+
pipelineRouting.connectedAccountIds
|
|
12981
|
+
).find(
|
|
12982
|
+
([platform3]) => platform3.toLowerCase() === post.platform.toLowerCase()
|
|
12983
|
+
);
|
|
12984
|
+
if (!route) {
|
|
12985
|
+
errors.push({
|
|
12986
|
+
stage: "schedule",
|
|
12987
|
+
message: `No verified connected account route for ${post.platform}`
|
|
12988
|
+
});
|
|
12989
|
+
continue;
|
|
12990
|
+
}
|
|
12069
12991
|
try {
|
|
12070
12992
|
const { error: schedError } = await callEdgeFunction(
|
|
12071
12993
|
"schedule-post",
|
|
@@ -12078,7 +13000,11 @@ function registerPipelineTools(server2) {
|
|
|
12078
13000
|
scheduledAt,
|
|
12079
13001
|
planId,
|
|
12080
13002
|
idempotencyKey: `pipeline-${planId}-${post.id}`,
|
|
12081
|
-
...resolvedProjectId ? {
|
|
13003
|
+
...resolvedProjectId ? {
|
|
13004
|
+
projectId: resolvedProjectId,
|
|
13005
|
+
project_id: resolvedProjectId
|
|
13006
|
+
} : {},
|
|
13007
|
+
connectedAccountIds: { [route[0]]: route[1] }
|
|
12082
13008
|
},
|
|
12083
13009
|
{ timeoutMs: 15e3 }
|
|
12084
13010
|
);
|
|
@@ -12144,12 +13070,17 @@ function registerPipelineTools(server2) {
|
|
|
12144
13070
|
if (response_format === "json") {
|
|
12145
13071
|
return {
|
|
12146
13072
|
content: [
|
|
12147
|
-
{
|
|
13073
|
+
{
|
|
13074
|
+
type: "text",
|
|
13075
|
+
text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
|
|
13076
|
+
}
|
|
12148
13077
|
]
|
|
12149
13078
|
};
|
|
12150
13079
|
}
|
|
12151
13080
|
const lines = [];
|
|
12152
|
-
lines.push(
|
|
13081
|
+
lines.push(
|
|
13082
|
+
`Pipeline ${pipelineId.slice(0, 8)}... ${finalStatus.toUpperCase()}`
|
|
13083
|
+
);
|
|
12153
13084
|
lines.push("=".repeat(40));
|
|
12154
13085
|
lines.push(`Posts generated: ${posts.length}`);
|
|
12155
13086
|
lines.push(`Posts approved: ${postsApproved}`);
|
|
@@ -12188,7 +13119,9 @@ function registerPipelineTools(server2) {
|
|
|
12188
13119
|
} catch {
|
|
12189
13120
|
}
|
|
12190
13121
|
return {
|
|
12191
|
-
content: [
|
|
13122
|
+
content: [
|
|
13123
|
+
{ type: "text", text: `Pipeline failed: ${message}` }
|
|
13124
|
+
],
|
|
12192
13125
|
isError: true
|
|
12193
13126
|
};
|
|
12194
13127
|
}
|
|
@@ -12225,7 +13158,12 @@ function registerPipelineTools(server2) {
|
|
|
12225
13158
|
}
|
|
12226
13159
|
if (format === "json") {
|
|
12227
13160
|
return {
|
|
12228
|
-
content: [
|
|
13161
|
+
content: [
|
|
13162
|
+
{
|
|
13163
|
+
type: "text",
|
|
13164
|
+
text: JSON.stringify(asEnvelope20(data), null, 2)
|
|
13165
|
+
}
|
|
13166
|
+
]
|
|
12229
13167
|
};
|
|
12230
13168
|
}
|
|
12231
13169
|
const lines = [];
|
|
@@ -12265,10 +13203,19 @@ function registerPipelineTools(server2) {
|
|
|
12265
13203
|
},
|
|
12266
13204
|
async ({ plan_id, quality_threshold, response_format }) => {
|
|
12267
13205
|
try {
|
|
12268
|
-
const { data: loadResult, error: loadError } = await callEdgeFunction(
|
|
13206
|
+
const { data: loadResult, error: loadError } = await callEdgeFunction(
|
|
13207
|
+
"mcp-data",
|
|
13208
|
+
{ action: "auto-approve-plan", plan_id },
|
|
13209
|
+
{ timeoutMs: 1e4 }
|
|
13210
|
+
);
|
|
12269
13211
|
if (loadError) {
|
|
12270
13212
|
return {
|
|
12271
|
-
content: [
|
|
13213
|
+
content: [
|
|
13214
|
+
{
|
|
13215
|
+
type: "text",
|
|
13216
|
+
text: `Failed to load plan: ${loadError}`
|
|
13217
|
+
}
|
|
13218
|
+
],
|
|
12272
13219
|
isError: true
|
|
12273
13220
|
};
|
|
12274
13221
|
}
|
|
@@ -12276,7 +13223,10 @@ function registerPipelineTools(server2) {
|
|
|
12276
13223
|
if (!stored?.plan_payload) {
|
|
12277
13224
|
return {
|
|
12278
13225
|
content: [
|
|
12279
|
-
{
|
|
13226
|
+
{
|
|
13227
|
+
type: "text",
|
|
13228
|
+
text: `No content plan found for plan_id=${plan_id}`
|
|
13229
|
+
}
|
|
12280
13230
|
],
|
|
12281
13231
|
isError: true
|
|
12282
13232
|
};
|
|
@@ -12303,7 +13253,11 @@ function registerPipelineTools(server2) {
|
|
|
12303
13253
|
blockers: []
|
|
12304
13254
|
};
|
|
12305
13255
|
autoApproved++;
|
|
12306
|
-
details.push({
|
|
13256
|
+
details.push({
|
|
13257
|
+
post_id: post.id,
|
|
13258
|
+
action: "approved",
|
|
13259
|
+
score: quality.total
|
|
13260
|
+
});
|
|
12307
13261
|
} else if (quality.total >= quality_threshold - 5) {
|
|
12308
13262
|
post.status = "needs_edit";
|
|
12309
13263
|
post.quality = {
|
|
@@ -12313,7 +13267,11 @@ function registerPipelineTools(server2) {
|
|
|
12313
13267
|
blockers: quality.blockers
|
|
12314
13268
|
};
|
|
12315
13269
|
flagged++;
|
|
12316
|
-
details.push({
|
|
13270
|
+
details.push({
|
|
13271
|
+
post_id: post.id,
|
|
13272
|
+
action: "flagged",
|
|
13273
|
+
score: quality.total
|
|
13274
|
+
});
|
|
12317
13275
|
} else {
|
|
12318
13276
|
post.status = "rejected";
|
|
12319
13277
|
post.quality = {
|
|
@@ -12323,7 +13281,11 @@ function registerPipelineTools(server2) {
|
|
|
12323
13281
|
blockers: quality.blockers
|
|
12324
13282
|
};
|
|
12325
13283
|
rejected++;
|
|
12326
|
-
details.push({
|
|
13284
|
+
details.push({
|
|
13285
|
+
post_id: post.id,
|
|
13286
|
+
action: "rejected",
|
|
13287
|
+
score: quality.total
|
|
13288
|
+
});
|
|
12327
13289
|
}
|
|
12328
13290
|
}
|
|
12329
13291
|
const newStatus = flagged === 0 && rejected === 0 ? "approved" : "in_review";
|
|
@@ -12359,7 +13321,10 @@ function registerPipelineTools(server2) {
|
|
|
12359
13321
|
if (response_format === "json") {
|
|
12360
13322
|
return {
|
|
12361
13323
|
content: [
|
|
12362
|
-
{
|
|
13324
|
+
{
|
|
13325
|
+
type: "text",
|
|
13326
|
+
text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
|
|
13327
|
+
}
|
|
12363
13328
|
]
|
|
12364
13329
|
};
|
|
12365
13330
|
}
|
|
@@ -12380,7 +13345,9 @@ function registerPipelineTools(server2) {
|
|
|
12380
13345
|
} catch (err) {
|
|
12381
13346
|
const message = sanitizeError(err);
|
|
12382
13347
|
return {
|
|
12383
|
-
content: [
|
|
13348
|
+
content: [
|
|
13349
|
+
{ type: "text", text: `Auto-approve failed: ${message}` }
|
|
13350
|
+
],
|
|
12384
13351
|
isError: true
|
|
12385
13352
|
};
|
|
12386
13353
|
}
|
|
@@ -12414,6 +13381,7 @@ var init_pipeline = __esm({
|
|
|
12414
13381
|
init_quality();
|
|
12415
13382
|
init_version();
|
|
12416
13383
|
init_parse_utils();
|
|
13384
|
+
init_connected_account_routing();
|
|
12417
13385
|
PLATFORM_ENUM2 = z24.enum([
|
|
12418
13386
|
"youtube",
|
|
12419
13387
|
"tiktok",
|
|
@@ -15078,28 +16046,33 @@ var init_analytics_pulse = __esm({
|
|
|
15078
16046
|
|
|
15079
16047
|
// src/tools/connections.ts
|
|
15080
16048
|
import { z as z33 } from "zod";
|
|
15081
|
-
function
|
|
16049
|
+
function findActiveAccounts(accounts, platform3, projectId) {
|
|
15082
16050
|
const target = platform3.toLowerCase();
|
|
15083
|
-
return accounts.
|
|
16051
|
+
return accounts.filter((a) => {
|
|
15084
16052
|
const effectiveStatus = a.effective_status || a.status;
|
|
15085
|
-
return a.platform.toLowerCase() === target && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
|
|
15086
|
-
})
|
|
16053
|
+
return a.platform.toLowerCase() === target && a.project_id === projectId && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
|
|
16054
|
+
});
|
|
15087
16055
|
}
|
|
15088
16056
|
function registerConnectionTools(server2) {
|
|
15089
16057
|
server2.tool(
|
|
15090
16058
|
"start_platform_connection",
|
|
15091
16059
|
"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.",
|
|
15092
16060
|
{
|
|
15093
|
-
platform: z33.enum(PLATFORM_ENUM4).describe(
|
|
15094
|
-
|
|
15095
|
-
|
|
16061
|
+
platform: z33.enum(PLATFORM_ENUM4).describe(
|
|
16062
|
+
"Platform to connect. Lower-case: instagram, tiktok, youtube, etc."
|
|
16063
|
+
),
|
|
16064
|
+
project_id: z33.string().uuid().optional().describe(
|
|
16065
|
+
"Brand/project ID to bind the new social account to. Required when the account has multiple brands."
|
|
15096
16066
|
),
|
|
15097
16067
|
response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
15098
16068
|
},
|
|
15099
16069
|
async ({ platform: platform3, project_id, response_format }) => {
|
|
15100
16070
|
const format = response_format ?? "text";
|
|
15101
16071
|
const userId = await getDefaultUserId();
|
|
15102
|
-
const rl = checkRateLimit(
|
|
16072
|
+
const rl = checkRateLimit(
|
|
16073
|
+
"posting",
|
|
16074
|
+
`start_platform_connection:${userId}`
|
|
16075
|
+
);
|
|
15103
16076
|
if (!rl.allowed) {
|
|
15104
16077
|
return {
|
|
15105
16078
|
content: [
|
|
@@ -15111,12 +16084,27 @@ function registerConnectionTools(server2) {
|
|
|
15111
16084
|
isError: true
|
|
15112
16085
|
};
|
|
15113
16086
|
}
|
|
16087
|
+
const projectResolution = await resolveProjectStrict(project_id);
|
|
16088
|
+
if (!projectResolution.projectId) {
|
|
16089
|
+
return {
|
|
16090
|
+
content: [
|
|
16091
|
+
{
|
|
16092
|
+
type: "text",
|
|
16093
|
+
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."
|
|
16094
|
+
}
|
|
16095
|
+
],
|
|
16096
|
+
isError: true
|
|
16097
|
+
};
|
|
16098
|
+
}
|
|
16099
|
+
const resolvedProjectId = projectResolution.projectId;
|
|
16100
|
+
const projectAutoResolvedNote = projectResolution.autoResolvedNote;
|
|
15114
16101
|
const { data, error } = await callEdgeFunction(
|
|
15115
16102
|
"mcp-data",
|
|
15116
16103
|
{
|
|
15117
16104
|
action: "mint-connection-nonce",
|
|
15118
16105
|
platform: platform3,
|
|
15119
|
-
|
|
16106
|
+
projectId: resolvedProjectId,
|
|
16107
|
+
project_id: resolvedProjectId
|
|
15120
16108
|
},
|
|
15121
16109
|
{ timeoutMs: 1e4 }
|
|
15122
16110
|
);
|
|
@@ -15132,6 +16120,17 @@ function registerConnectionTools(server2) {
|
|
|
15132
16120
|
isError: true
|
|
15133
16121
|
};
|
|
15134
16122
|
}
|
|
16123
|
+
if (data.project_id !== resolvedProjectId) {
|
|
16124
|
+
return {
|
|
16125
|
+
content: [
|
|
16126
|
+
{
|
|
16127
|
+
type: "text",
|
|
16128
|
+
text: "Connection link project attestation failed. No OAuth link was returned."
|
|
16129
|
+
}
|
|
16130
|
+
],
|
|
16131
|
+
isError: true
|
|
16132
|
+
};
|
|
16133
|
+
}
|
|
15135
16134
|
if (format === "json") {
|
|
15136
16135
|
return {
|
|
15137
16136
|
content: [
|
|
@@ -15140,10 +16139,11 @@ function registerConnectionTools(server2) {
|
|
|
15140
16139
|
text: JSON.stringify(
|
|
15141
16140
|
{
|
|
15142
16141
|
platform: data.platform,
|
|
15143
|
-
project_id:
|
|
16142
|
+
project_id: resolvedProjectId,
|
|
15144
16143
|
deep_link: data.deep_link,
|
|
15145
16144
|
expires_at: data.expires_at,
|
|
15146
|
-
next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection."
|
|
16145
|
+
next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection.",
|
|
16146
|
+
...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
|
|
15147
16147
|
},
|
|
15148
16148
|
null,
|
|
15149
16149
|
2
|
|
@@ -15159,7 +16159,7 @@ function registerConnectionTools(server2) {
|
|
|
15159
16159
|
type: "text",
|
|
15160
16160
|
text: [
|
|
15161
16161
|
`${data.platform} connection ready.`,
|
|
15162
|
-
|
|
16162
|
+
`Brand/project: ${resolvedProjectId}`,
|
|
15163
16163
|
"",
|
|
15164
16164
|
'Ask the user to open this link in a browser and click "Connect" on the platform:',
|
|
15165
16165
|
` ${data.deep_link}`,
|
|
@@ -15167,7 +16167,8 @@ function registerConnectionTools(server2) {
|
|
|
15167
16167
|
`Link expires at: ${data.expires_at} (~2 minutes).`,
|
|
15168
16168
|
"",
|
|
15169
16169
|
"After they approve, call `wait_for_connection` with the same platform to confirm.",
|
|
15170
|
-
"This is a one-time browser setup \u2014 not another OAuth flow inside Claude."
|
|
16170
|
+
"This is a one-time browser setup \u2014 not another OAuth flow inside Claude.",
|
|
16171
|
+
...projectAutoResolvedNote ? ["", `Note: ${projectAutoResolvedNote}`] : []
|
|
15171
16172
|
].join("\n")
|
|
15172
16173
|
}
|
|
15173
16174
|
],
|
|
@@ -15180,14 +16181,20 @@ function registerConnectionTools(server2) {
|
|
|
15180
16181
|
"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.",
|
|
15181
16182
|
{
|
|
15182
16183
|
platform: z33.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
|
|
15183
|
-
project_id: z33.string().optional().describe(
|
|
16184
|
+
project_id: z33.string().uuid().optional().describe(
|
|
15184
16185
|
"Brand/project ID to scope the connection poll. Use the same project_id passed to start_platform_connection."
|
|
15185
16186
|
),
|
|
15186
16187
|
timeout_s: z33.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
|
|
15187
16188
|
poll_interval_s: z33.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
|
|
15188
16189
|
response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
15189
16190
|
},
|
|
15190
|
-
async ({
|
|
16191
|
+
async ({
|
|
16192
|
+
platform: platform3,
|
|
16193
|
+
project_id,
|
|
16194
|
+
timeout_s,
|
|
16195
|
+
poll_interval_s,
|
|
16196
|
+
response_format
|
|
16197
|
+
}) => {
|
|
15191
16198
|
const format = response_format ?? "text";
|
|
15192
16199
|
const startedAt = Date.now();
|
|
15193
16200
|
const timeoutMs = (timeout_s ?? 120) * 1e3;
|
|
@@ -15206,6 +16213,18 @@ function registerConnectionTools(server2) {
|
|
|
15206
16213
|
isError: true
|
|
15207
16214
|
};
|
|
15208
16215
|
}
|
|
16216
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
16217
|
+
if (!resolvedProjectId) {
|
|
16218
|
+
return {
|
|
16219
|
+
content: [
|
|
16220
|
+
{
|
|
16221
|
+
type: "text",
|
|
16222
|
+
text: "A project_id is required to wait for a connection. Use the same project_id used to start the OAuth flow."
|
|
16223
|
+
}
|
|
16224
|
+
],
|
|
16225
|
+
isError: true
|
|
16226
|
+
};
|
|
16227
|
+
}
|
|
15209
16228
|
const activeWaits = inFlightWaitsByUser.get(userId) ?? 0;
|
|
15210
16229
|
if (activeWaits >= MAX_CONCURRENT_WAITS_PER_USER) {
|
|
15211
16230
|
return {
|
|
@@ -15227,12 +16246,62 @@ function registerConnectionTools(server2) {
|
|
|
15227
16246
|
"mcp-data",
|
|
15228
16247
|
{
|
|
15229
16248
|
action: "connected-accounts",
|
|
15230
|
-
|
|
16249
|
+
projectId: resolvedProjectId,
|
|
16250
|
+
project_id: resolvedProjectId
|
|
15231
16251
|
},
|
|
15232
16252
|
{ timeoutMs: 1e4 }
|
|
15233
16253
|
);
|
|
15234
16254
|
if (!error && data?.success) {
|
|
15235
|
-
const
|
|
16255
|
+
const foundAccounts = findActiveAccounts(
|
|
16256
|
+
data.accounts ?? [],
|
|
16257
|
+
platform3,
|
|
16258
|
+
resolvedProjectId
|
|
16259
|
+
);
|
|
16260
|
+
if (foundAccounts.length > 1) {
|
|
16261
|
+
if (format === "json") {
|
|
16262
|
+
return {
|
|
16263
|
+
content: [
|
|
16264
|
+
{
|
|
16265
|
+
type: "text",
|
|
16266
|
+
text: JSON.stringify(
|
|
16267
|
+
{
|
|
16268
|
+
connected: true,
|
|
16269
|
+
platform: platform3,
|
|
16270
|
+
project_id: resolvedProjectId,
|
|
16271
|
+
accounts: foundAccounts.map((a) => ({
|
|
16272
|
+
id: a.id,
|
|
16273
|
+
username: a.username,
|
|
16274
|
+
connected_at: a.created_at
|
|
16275
|
+
})),
|
|
16276
|
+
attempts,
|
|
16277
|
+
message: `${platform3} has ${foundAccounts.length} active accounts for this project. Pass the exact account_id to schedule_post.`
|
|
16278
|
+
},
|
|
16279
|
+
null,
|
|
16280
|
+
2
|
|
16281
|
+
)
|
|
16282
|
+
}
|
|
16283
|
+
],
|
|
16284
|
+
isError: false
|
|
16285
|
+
};
|
|
16286
|
+
}
|
|
16287
|
+
return {
|
|
16288
|
+
content: [
|
|
16289
|
+
{
|
|
16290
|
+
type: "text",
|
|
16291
|
+
text: [
|
|
16292
|
+
`${platform3} is connected \u2014 ${foundAccounts.length} active accounts found for project ${resolvedProjectId}:`,
|
|
16293
|
+
...foundAccounts.map(
|
|
16294
|
+
(a) => ` ${a.username || "(unnamed)"} (id=${a.id})`
|
|
16295
|
+
),
|
|
16296
|
+
"",
|
|
16297
|
+
"Call schedule_post with the exact account_id (or account_ids) for the one you mean."
|
|
16298
|
+
].join("\n")
|
|
16299
|
+
}
|
|
16300
|
+
],
|
|
16301
|
+
isError: false
|
|
16302
|
+
};
|
|
16303
|
+
}
|
|
16304
|
+
const found = foundAccounts[0];
|
|
15236
16305
|
if (found) {
|
|
15237
16306
|
if (format === "json") {
|
|
15238
16307
|
return {
|
|
@@ -15243,7 +16312,7 @@ function registerConnectionTools(server2) {
|
|
|
15243
16312
|
{
|
|
15244
16313
|
connected: true,
|
|
15245
16314
|
platform: found.platform,
|
|
15246
|
-
project_id:
|
|
16315
|
+
project_id: resolvedProjectId,
|
|
15247
16316
|
account_id: found.id,
|
|
15248
16317
|
username: found.username,
|
|
15249
16318
|
connected_at: found.created_at,
|
|
@@ -15263,7 +16332,7 @@ function registerConnectionTools(server2) {
|
|
|
15263
16332
|
type: "text",
|
|
15264
16333
|
text: [
|
|
15265
16334
|
`${found.platform} is connected.`,
|
|
15266
|
-
|
|
16335
|
+
`Brand/project: ${resolvedProjectId}`,
|
|
15267
16336
|
`Account: ${found.username || "(unnamed)"} (id=${found.id})`,
|
|
15268
16337
|
`Detected after ${attempts} poll(s) in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s.`,
|
|
15269
16338
|
"Ready to call `schedule_post`."
|
|
@@ -15276,7 +16345,9 @@ function registerConnectionTools(server2) {
|
|
|
15276
16345
|
}
|
|
15277
16346
|
const remaining = deadline - Date.now();
|
|
15278
16347
|
if (remaining <= 0) break;
|
|
15279
|
-
await new Promise(
|
|
16348
|
+
await new Promise(
|
|
16349
|
+
(resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining))
|
|
16350
|
+
);
|
|
15280
16351
|
}
|
|
15281
16352
|
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.`;
|
|
15282
16353
|
if (format === "json") {
|
|
@@ -15288,7 +16359,7 @@ function registerConnectionTools(server2) {
|
|
|
15288
16359
|
{
|
|
15289
16360
|
connected: false,
|
|
15290
16361
|
platform: platform3,
|
|
15291
|
-
project_id:
|
|
16362
|
+
project_id: resolvedProjectId,
|
|
15292
16363
|
attempts,
|
|
15293
16364
|
timed_out: true,
|
|
15294
16365
|
message
|