@socialneuron/mcp-server 1.8.2 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.8.2";
22
+ MCP_VERSION = "1.9.1";
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,50 +736,91 @@ function writeCredentialsFile(data) {
529
736
  }
530
737
  hardenCredentialPermissions();
531
738
  }
532
- function macKeychainRead(service) {
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
- return result.trim() || null;
540
- } catch {
541
- return null;
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 macKeychainWrite(service, value) {
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
- execFileSync(
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 true;
572
- } catch {
573
- return false;
794
+ return "deleted";
795
+ } catch (error) {
796
+ if (isMacKeychainItemMissing(error)) return nativeDeleted ? "deleted" : "missing";
797
+ return "unavailable";
574
798
  }
575
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
+ );
819
+ }
820
+ throw new Error(
821
+ "Unable to verify absence of an existing Social Neuron Keychain credential. Unlock Keychain Access and retry."
822
+ );
823
+ }
576
824
  function linuxSecretRead(key) {
577
825
  try {
578
826
  const result = execFileSync(
@@ -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
- macKeychainDelete(KEYCHAIN_SERVICE_API);
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
- logMcpToolInvocation: () => logMcpToolInvocation
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("No user ID available. Set SOCIALNEURON_USER_ID or authenticate via API key.");
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(1).maybeSingle();
922
- if (data?.id) {
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((expiresMs - Date.now()) / (1e3 * 60 * 60 * 24));
956
- if (!_quietAuth) console.error("[MCP] Key expires: " + authenticatedExpiresAt);
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, projectIdCache;
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
 
@@ -2016,269 +2393,66 @@ var COMPILED;
2016
2393
  var init_pii = __esm({
2017
2394
  "src/lib/agent-harness/detectors/pii.ts"() {
2018
2395
  "use strict";
2019
- init_constants2();
2020
- COMPILED = Object.entries(CONSTANTS.PII_PATTERNS).map(
2021
- ([name, pattern]) => ({ name, re: new RegExp(pattern, "gi") })
2022
- );
2023
- }
2024
- });
2025
-
2026
- // src/lib/agent-harness/scanner.ts
2027
- function scan(text, options) {
2028
- const flagged = /* @__PURE__ */ new Set();
2029
- let risk = 0;
2030
- const maxLength = options.source === "mcp_tool_output" ? CONSTANTS.MAX_OUTPUT_LENGTH : CONSTANTS.MAX_LENGTH;
2031
- if (text.length > maxLength) {
2032
- return {
2033
- passed: false,
2034
- 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
- }
2396
+ init_constants2();
2397
+ COMPILED = Object.entries(CONSTANTS.PII_PATTERNS).map(
2398
+ ([name, pattern]) => ({ name, re: new RegExp(pattern, "gi") })
2399
+ );
2168
2400
  }
2169
- let url;
2170
- let method = options?.method ?? "POST";
2171
- let headers;
2172
- let requestBody;
2173
- if (!apiKey) {
2174
- clearTimeout(timer);
2401
+ });
2402
+
2403
+ // src/lib/agent-harness/scanner.ts
2404
+ function scan(text, options) {
2405
+ const flagged = /* @__PURE__ */ new Set();
2406
+ let risk = 0;
2407
+ const maxLength = options.source === "mcp_tool_output" ? CONSTANTS.MAX_OUTPUT_LENGTH : CONSTANTS.MAX_LENGTH;
2408
+ if (text.length > maxLength) {
2175
2409
  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"
2410
+ passed: false,
2411
+ risk_score: 1,
2412
+ flagged_patterns: ["excessive_length"],
2413
+ pii_redacted: false
2178
2414
  };
2179
2415
  }
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." };
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 SAFE_GATEWAY_ERROR_CODES, SAFE_BILLING_STATUSES, SAFE_JOB_STATUSES;
2244
- var init_edge_function = __esm({
2245
- "src/lib/edge-function.ts"() {
2448
+ var init_scanner = __esm({
2449
+ "src/lib/agent-harness/scanner.ts"() {
2246
2450
  "use strict";
2247
- init_supabase();
2248
- init_request_context();
2249
- SAFE_GATEWAY_ERROR_CODES = /* @__PURE__ */ new Set([
2250
- "daily_limit_reached",
2251
- "insufficient_credits",
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
 
@@ -2770,6 +2944,92 @@ var init_ideation = __esm({
2770
2944
  }
2771
2945
  });
2772
2946
 
2947
+ // src/lib/sanitize-error.ts
2948
+ function redactSensitiveIdentifiers(message) {
2949
+ return message.replace(EMAIL_PATTERN, "[redacted-email]").replace(UUID_PATTERN, "[redacted-id]");
2950
+ }
2951
+ function sanitizeError(error) {
2952
+ const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
2953
+ for (const [pattern, userMessage] of ERROR_PATTERNS) {
2954
+ if (pattern.test(msg)) {
2955
+ return userMessage;
2956
+ }
2957
+ }
2958
+ return "An unexpected error occurred. Please try again.";
2959
+ }
2960
+ var ERROR_PATTERNS, UUID_PATTERN, EMAIL_PATTERN;
2961
+ var init_sanitize_error = __esm({
2962
+ "src/lib/sanitize-error.ts"() {
2963
+ "use strict";
2964
+ ERROR_PATTERNS = [
2965
+ // Postgres / PostgREST
2966
+ [
2967
+ /PGRST301|permission denied/i,
2968
+ "Access denied. Check your account permissions."
2969
+ ],
2970
+ [
2971
+ /42P01|does not exist/i,
2972
+ "Service temporarily unavailable. Please try again."
2973
+ ],
2974
+ [
2975
+ /23505|unique.*constraint|duplicate key/i,
2976
+ "A duplicate record already exists."
2977
+ ],
2978
+ [/23503|foreign key/i, "Referenced record not found."],
2979
+ // Gemini / Google AI
2980
+ [
2981
+ /google.*api.*key|googleapis\.com.*40[13]/i,
2982
+ "Content generation failed. Please try again."
2983
+ ],
2984
+ [
2985
+ /RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
2986
+ "AI service rate limit reached. Please wait and retry."
2987
+ ],
2988
+ [
2989
+ /SAFETY|prompt.*blocked|content.*filter/i,
2990
+ "Content was blocked by the AI safety filter. Try rephrasing."
2991
+ ],
2992
+ [
2993
+ /gemini.*error|generativelanguage/i,
2994
+ "Content generation failed. Please try again."
2995
+ ],
2996
+ // Kie.ai
2997
+ [/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
2998
+ // Stripe
2999
+ [
3000
+ /stripe.*api|sk_live_|sk_test_/i,
3001
+ "Payment processing error. Please try again."
3002
+ ],
3003
+ // Network / fetch
3004
+ [
3005
+ /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
3006
+ "External service unavailable. Please try again."
3007
+ ],
3008
+ [
3009
+ /fetch failed|network error|abort.*timeout/i,
3010
+ "Network request failed. Please try again."
3011
+ ],
3012
+ [/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
3013
+ // Supabase Edge Function internals
3014
+ [
3015
+ /FunctionsHttpError|non-2xx status/i,
3016
+ "Backend service error. Please try again."
3017
+ ],
3018
+ [
3019
+ /JWT|token.*expired|token.*invalid/i,
3020
+ "Authentication expired. Please re-authenticate."
3021
+ ],
3022
+ // Generic sensitive patterns (API keys, URLs with secrets)
3023
+ [
3024
+ /[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i,
3025
+ "An internal error occurred. Please try again."
3026
+ ]
3027
+ ];
3028
+ UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
3029
+ EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
3030
+ }
3031
+ });
3032
+
2773
3033
  // src/lib/checkStatusShape.ts
2774
3034
  function buildCheckStatusPayload(job, liveStatus) {
2775
3035
  const status = liveStatus?.status ?? job.status;
@@ -2865,9 +3125,22 @@ function checkCreditBudget(estimatedCost) {
2865
3125
  }
2866
3126
  const used = getCreditsUsed();
2867
3127
  if (used + estimatedCost > MAX_CREDITS_PER_RUN) {
3128
+ const message = `Per-run credit cap reached \u2014 this is a local spend guard, not a server rate limit, so retrying will not help. The SOCIALNEURON_MAX_CREDITS_PER_RUN environment variable is set to ${MAX_CREDITS_PER_RUN} credits. This run has already spent ${used} credits and this call is estimated at ~${estimatedCost} more (${used} + ${estimatedCost} = ${used + estimatedCost} > ${MAX_CREDITS_PER_RUN}). To proceed, raise or unset SOCIALNEURON_MAX_CREDITS_PER_RUN.`;
2868
3129
  return {
2869
3130
  ok: false,
2870
- message: `Credit budget exceeded for this MCP run. Used=${used}, next~=${estimatedCost}, limit=${MAX_CREDITS_PER_RUN}.`
3131
+ message,
3132
+ error: toolError("billing_error", message, {
3133
+ recover_with: [
3134
+ "Raise SOCIALNEURON_MAX_CREDITS_PER_RUN to a higher credit ceiling.",
3135
+ "Unset SOCIALNEURON_MAX_CREDITS_PER_RUN to remove the per-run cap."
3136
+ ],
3137
+ details: {
3138
+ env_var: "SOCIALNEURON_MAX_CREDITS_PER_RUN",
3139
+ credits_used_this_run: used,
3140
+ estimated_call_cost: estimatedCost,
3141
+ max_credits_per_run: MAX_CREDITS_PER_RUN
3142
+ }
3143
+ })
2871
3144
  };
2872
3145
  }
2873
3146
  return { ok: true };
@@ -2890,8 +3163,15 @@ var init_budget = __esm({
2890
3163
  "src/lib/budget.ts"() {
2891
3164
  "use strict";
2892
3165
  init_request_context();
2893
- MAX_CREDITS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
2894
- MAX_ASSETS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0));
3166
+ init_tool_error();
3167
+ MAX_CREDITS_PER_RUN = Math.max(
3168
+ 0,
3169
+ Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0)
3170
+ );
3171
+ MAX_ASSETS_PER_RUN = Math.max(
3172
+ 0,
3173
+ Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0)
3174
+ );
2895
3175
  _globalCreditsUsed = 0;
2896
3176
  _globalAssetsGenerated = 0;
2897
3177
  }
@@ -2918,7 +3198,7 @@ function asEnvelope2(data) {
2918
3198
  function registerContentTools(server2) {
2919
3199
  server2.tool(
2920
3200
  "generate_video",
2921
- "Start an async AI video generation job \u2014 returns a job_id immediately. Poll with check_status every 10-30s until complete. Base credit costs (reference config; dynamic models scale with duration/audio/resolution): seedance-2-fast 264 (8s, best quality/credit) \xB7 kling-3 100 (5s no-audio) \xB7 grok-imagine 30 (6s, cheapest) \xB7 veo3-fast 65 (8s) \xB7 kling-3-pro 135 (5s no-audio) \xB7 seedance-2 328 (8s 720p) \xB7 veo3-quality 1000 (8s) \xB7 wan-2.6 105 (5s) \xB7 gemini-omni-video 126 (multi-reference composites) \xB7 hailuo-02-standard 180 / seedance-1.5-pro 150 / kling 170 (legacy/budget fallbacks). Costs are estimated pre-check and reconciled post-response from the actual charge. audio adds ~1.5x on kling-3/kling-3-pro and ~2.65x on kling (enable_audio defaults to FALSE). Check get_credit_balance first for expensive generations.",
3201
+ "Start an async AI video generation job \u2014 returns a job_id immediately. Poll with check_status every 10-30s until complete. Base credit costs (reference config; dynamic models scale with duration/audio/resolution): seedance-2-fast 264 (8s, best quality/credit) \xB7 kling-3 100 (5s no-audio) \xB7 grok-imagine 30 (6s, cheapest) \xB7 veo3-fast 65 (8s) \xB7 kling-3-pro 135 (5s no-audio) \xB7 seedance-2 328 (8s 720p) \xB7 veo3-quality 1000 (8s) \xB7 wan-2.6 105 (5s) \xB7 gemini-omni-video 126 (multi-reference composites) \xB7 hailuo-02-standard 180 / seedance-1.5-pro 150 / kling 170 (legacy/budget fallbacks). Costs are estimated pre-check and reconciled post-response from the actual charge. audio adds ~1.5x on kling-3/kling-3-pro and ~2.65x on kling; enable_audio defaults to FALSE EXCEPT the seedance-2 family (seedance-2-fast, seedance-2) where audio is ON by default and its cost is already included. Check get_credit_balance first for expensive generations.",
2922
3202
  {
2923
3203
  prompt: z2.string().max(2500).describe(
2924
3204
  'Video prompt \u2014 be specific about visual style, camera movement, lighting, and mood. Example: "Aerial drone shot of coastal cliffs at golden hour, slow dolly forward, cinematic 24fps, warm color grading." Vague prompts produce generic results.'
@@ -2933,7 +3213,7 @@ function registerContentTools(server2) {
2933
3213
  "Video aspect ratio. 16:9 for YouTube/landscape, 9:16 for TikTok/Reels/Shorts, 1:1 for Instagram feed/square. Defaults to 16:9."
2934
3214
  ),
2935
3215
  enable_audio: z2.boolean().optional().describe(
2936
- "Enable native audio generation. DEFAULTS TO FALSE (cost control). Cost multiplier when true: kling 2.6 ~2.65x (17->45 cr/sec), kling-3 1.5x (20->30 cr/sec), kling-3-pro ~1.5x (27->40 cr/sec). seedance-2/-fast generate audio natively regardless. 5+ languages."
3216
+ "Enable native audio generation. For most models this DEFAULTS TO FALSE (cost control). EXCEPTION: the seedance-2 family (seedance-2-fast, seedance-2) DEFAULTS TO TRUE \u2014 these generate audio natively and their credit cost already includes it, so audio is on unless you explicitly pass false. Cost multiplier when true on other models: kling 2.6 ~2.65x (17->45 cr/sec), kling-3 1.5x (20->30 cr/sec), kling-3-pro ~1.5x (27->40 cr/sec). 5+ languages."
2937
3217
  ),
2938
3218
  image_url: z2.string().optional().describe(
2939
3219
  "Start frame image URL for image-to-video (Kling 3.0 frame control)."
@@ -2969,12 +3249,12 @@ function registerContentTools(server2) {
2969
3249
  const estimatedCost = VIDEO_CREDIT_ESTIMATES[model] ?? 120;
2970
3250
  const budgetCheck = checkCreditBudget(estimatedCost);
2971
3251
  if (!budgetCheck.ok) {
2972
- return {
2973
- content: [{ type: "text", text: budgetCheck.message }],
2974
- isError: true
2975
- };
3252
+ return budgetCheck.error;
2976
3253
  }
2977
- const rateLimit = checkRateLimit("generation", `generate_video:${userId}`);
3254
+ const rateLimit = checkRateLimit(
3255
+ "generation",
3256
+ `generate_video:${userId}`
3257
+ );
2978
3258
  if (!rateLimit.allowed) {
2979
3259
  return {
2980
3260
  content: [
@@ -2993,9 +3273,12 @@ function registerContentTools(server2) {
2993
3273
  model,
2994
3274
  duration: duration ?? 5,
2995
3275
  aspectRatio: aspect_ratio ?? "16:9",
2996
- // Default FALSE (2026-07-13): the old `?? true` default silently multiplied
2997
- // kling-family costs (2.65x on kling 2.6) for callers that never asked for audio.
2998
- enableAudio: enable_audio ?? false,
3276
+ // Default FALSE for most models (2026-07-13): the old `?? true` default silently
3277
+ // multiplied kling-family costs (2.65x on kling 2.6) for callers that never asked
3278
+ // for audio. Exception (2026-07-17): the seedance-2 family bills audio into its base
3279
+ // cost and generates it natively, so a false default there shipped silent no-audio
3280
+ // mp4s — those models default TRUE when the caller omits enable_audio.
3281
+ enableAudio: enable_audio ?? AUDIO_NATIVE_DEFAULT_MODELS.has(model),
2999
3282
  ...image_url && { imageUrl: image_url },
3000
3283
  ...end_frame_url && { endFrameUrl: end_frame_url },
3001
3284
  // The server reads projectId and enforces
@@ -3097,7 +3380,14 @@ function registerContentTools(server2) {
3097
3380
  project_id: z2.string().optional().describe("Project ID to associate the generated image with."),
3098
3381
  response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
3099
3382
  },
3100
- async ({ prompt: prompt2, model, aspect_ratio, image_url, project_id, response_format }) => {
3383
+ async ({
3384
+ prompt: prompt2,
3385
+ model,
3386
+ aspect_ratio,
3387
+ image_url,
3388
+ project_id,
3389
+ response_format
3390
+ }) => {
3101
3391
  const format = response_format ?? "text";
3102
3392
  const userId = await getDefaultUserId();
3103
3393
  const assetBudget = checkAssetBudget();
@@ -3110,12 +3400,12 @@ function registerContentTools(server2) {
3110
3400
  const estimatedCost = IMAGE_CREDIT_ESTIMATES[model] ?? 30;
3111
3401
  const budgetCheck = checkCreditBudget(estimatedCost);
3112
3402
  if (!budgetCheck.ok) {
3113
- return {
3114
- content: [{ type: "text", text: budgetCheck.message }],
3115
- isError: true
3116
- };
3403
+ return budgetCheck.error;
3117
3404
  }
3118
- const rateLimit = checkRateLimit("generation", `generate_image:${userId}`);
3405
+ const rateLimit = checkRateLimit(
3406
+ "generation",
3407
+ `generate_image:${userId}`
3408
+ );
3119
3409
  if (!rateLimit.allowed) {
3120
3410
  return {
3121
3411
  content: [
@@ -3246,6 +3536,15 @@ function registerContentTools(server2) {
3246
3536
  isError: true
3247
3537
  };
3248
3538
  }
3539
+ let projectsDisclosure;
3540
+ const ownScopedProjectId = await getDefaultProjectId();
3541
+ if (!ownScopedProjectId) {
3542
+ const ownUserId = await getDefaultUserId().catch(() => null);
3543
+ if (ownUserId) {
3544
+ const list = await listAccessibleProjectsWithAccountStatus(ownUserId);
3545
+ if (list.length > 0) projectsDisclosure = list;
3546
+ }
3547
+ }
3249
3548
  if (job.external_id && (job.status === "pending" || job.status === "processing")) {
3250
3549
  const { data: liveStatus } = await callEdgeFunction(
3251
3550
  "kie-task-status",
@@ -3269,15 +3568,25 @@ function registerContentTools(server2) {
3269
3568
  if (livePayload.error) {
3270
3569
  lines2.push(`Error: ${livePayload.error}`);
3271
3570
  }
3272
- const fallbackDisclosure2 = buildFallbackDisclosureLine(job.result_metadata);
3571
+ const fallbackDisclosure2 = buildFallbackDisclosureLine(
3572
+ job.result_metadata
3573
+ );
3273
3574
  if (fallbackDisclosure2) lines2.push(fallbackDisclosure2);
3274
3575
  lines2.push(`Credits: ${job.credits_cost}`);
3275
3576
  if (job.billing_status) {
3276
3577
  lines2.push(
3277
- `Billing: ${job.billing_status} (charged ${job.credits_charged ?? 0}, refunded ${job.credits_refunded ?? 0})`
3578
+ `Billing: ${job.billing_status} (charged ${job.credits_charged ?? 0}, refunded ${job.credits_refunded ?? 0})`
3579
+ );
3580
+ }
3581
+ lines2.push(`Created: ${job.created_at}`);
3582
+ if (projectsDisclosure) {
3583
+ lines2.push(
3584
+ "",
3585
+ `Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
3586
+ (p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
3587
+ ).join("; ")}`
3278
3588
  );
3279
3589
  }
3280
- lines2.push(`Created: ${job.created_at}`);
3281
3590
  if (format === "json") {
3282
3591
  return {
3283
3592
  content: [
@@ -3294,7 +3603,8 @@ function registerContentTools(server2) {
3294
3603
  // `job` + `liveStatus`; do not duplicate them here.
3295
3604
  asEnvelope2({
3296
3605
  ...liveStatus,
3297
- ...livePayload
3606
+ ...livePayload,
3607
+ ...projectsDisclosure ? { projects: projectsDisclosure } : {}
3298
3608
  }),
3299
3609
  null,
3300
3610
  2
@@ -3335,9 +3645,11 @@ function registerContentTools(server2) {
3335
3645
  );
3336
3646
  }
3337
3647
  if (job.error_message) {
3338
- lines.push(`Error: ${job.error_message}`);
3648
+ lines.push(`Error: ${redactSensitiveIdentifiers(job.error_message)}`);
3339
3649
  }
3340
- const fallbackDisclosure = buildFallbackDisclosureLine(job.result_metadata);
3650
+ const fallbackDisclosure = buildFallbackDisclosureLine(
3651
+ job.result_metadata
3652
+ );
3341
3653
  if (fallbackDisclosure) lines.push(fallbackDisclosure);
3342
3654
  lines.push(`Credits: ${job.credits_cost}`);
3343
3655
  if (job.billing_status) {
@@ -3349,10 +3661,19 @@ function registerContentTools(server2) {
3349
3661
  if (job.completed_at) {
3350
3662
  lines.push(`Completed: ${job.completed_at}`);
3351
3663
  }
3664
+ if (projectsDisclosure) {
3665
+ lines.push(
3666
+ "",
3667
+ `Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
3668
+ (p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
3669
+ ).join("; ")}`
3670
+ );
3671
+ }
3352
3672
  if (format === "json") {
3353
3673
  const enriched = {
3354
3674
  ...job,
3355
- ...buildCheckStatusPayload(job)
3675
+ ...buildCheckStatusPayload(job),
3676
+ ...projectsDisclosure ? { projects: projectsDisclosure } : {}
3356
3677
  };
3357
3678
  return {
3358
3679
  content: [
@@ -3478,10 +3799,7 @@ Return ONLY valid JSON in this exact format:
3478
3799
  const estimatedCost = 10;
3479
3800
  const budgetCheck = checkCreditBudget(estimatedCost);
3480
3801
  if (!budgetCheck.ok) {
3481
- return {
3482
- content: [{ type: "text", text: budgetCheck.message }],
3483
- isError: true
3484
- };
3802
+ return budgetCheck.error;
3485
3803
  }
3486
3804
  const { data, error } = await callEdgeFunction(
3487
3805
  "social-neuron-ai",
@@ -3568,10 +3886,7 @@ Return ONLY valid JSON in this exact format:
3568
3886
  const estimatedCost = 15;
3569
3887
  const budgetCheck = checkCreditBudget(estimatedCost);
3570
3888
  if (!budgetCheck.ok) {
3571
- return {
3572
- content: [{ type: "text", text: budgetCheck.message }],
3573
- isError: true
3574
- };
3889
+ return budgetCheck.error;
3575
3890
  }
3576
3891
  const rateLimit = checkRateLimit(
3577
3892
  "generation",
@@ -3734,10 +4049,7 @@ Return ONLY valid JSON in this exact format:
3734
4049
  const estimatedCost = 10 + slideCount * 2;
3735
4050
  const budgetCheck = checkCreditBudget(estimatedCost);
3736
4051
  if (!budgetCheck.ok) {
3737
- return {
3738
- content: [{ type: "text", text: budgetCheck.message }],
3739
- isError: true
3740
- };
4052
+ return budgetCheck.error;
3741
4053
  }
3742
4054
  const userId = await getDefaultUserId();
3743
4055
  const rateLimit = checkRateLimit(
@@ -3840,13 +4152,14 @@ Return ONLY valid JSON in this exact format:
3840
4152
  }
3841
4153
  );
3842
4154
  }
3843
- var VIDEO_CREDIT_ESTIMATES, VIDEO_MODEL_ENUM, IMAGE_CREDIT_ESTIMATES;
4155
+ var VIDEO_CREDIT_ESTIMATES, AUDIO_NATIVE_DEFAULT_MODELS, VIDEO_MODEL_ENUM, IMAGE_CREDIT_ESTIMATES;
3844
4156
  var init_content = __esm({
3845
4157
  "src/tools/content.ts"() {
3846
4158
  "use strict";
3847
4159
  init_edge_function();
3848
4160
  init_rate_limit();
3849
4161
  init_supabase();
4162
+ init_sanitize_error();
3850
4163
  init_version();
3851
4164
  init_checkStatusShape();
3852
4165
  init_budget();
@@ -3864,6 +4177,10 @@ var init_content = __esm({
3864
4177
  "seedance-1.5-pro": 150,
3865
4178
  kling: 170
3866
4179
  };
4180
+ AUDIO_NATIVE_DEFAULT_MODELS = /* @__PURE__ */ new Set([
4181
+ "seedance-2-fast",
4182
+ "seedance-2"
4183
+ ]);
3867
4184
  VIDEO_MODEL_ENUM = [
3868
4185
  "seedance-2-fast",
3869
4186
  "kling-3",
@@ -3892,57 +4209,6 @@ var init_content = __esm({
3892
4209
  }
3893
4210
  });
3894
4211
 
3895
- // src/lib/sanitize-error.ts
3896
- function sanitizeError(error) {
3897
- const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
3898
- for (const [pattern, userMessage] of ERROR_PATTERNS) {
3899
- if (pattern.test(msg)) {
3900
- return userMessage;
3901
- }
3902
- }
3903
- return "An unexpected error occurred. Please try again.";
3904
- }
3905
- var ERROR_PATTERNS;
3906
- var init_sanitize_error = __esm({
3907
- "src/lib/sanitize-error.ts"() {
3908
- "use strict";
3909
- ERROR_PATTERNS = [
3910
- // Postgres / PostgREST
3911
- [/PGRST301|permission denied/i, "Access denied. Check your account permissions."],
3912
- [/42P01|does not exist/i, "Service temporarily unavailable. Please try again."],
3913
- [/23505|unique.*constraint|duplicate key/i, "A duplicate record already exists."],
3914
- [/23503|foreign key/i, "Referenced record not found."],
3915
- // Gemini / Google AI
3916
- [/google.*api.*key|googleapis\.com.*40[13]/i, "Content generation failed. Please try again."],
3917
- [
3918
- /RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
3919
- "AI service rate limit reached. Please wait and retry."
3920
- ],
3921
- [
3922
- /SAFETY|prompt.*blocked|content.*filter/i,
3923
- "Content was blocked by the AI safety filter. Try rephrasing."
3924
- ],
3925
- [/gemini.*error|generativelanguage/i, "Content generation failed. Please try again."],
3926
- // Kie.ai
3927
- [/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
3928
- // Stripe
3929
- [/stripe.*api|sk_live_|sk_test_/i, "Payment processing error. Please try again."],
3930
- // Network / fetch
3931
- [
3932
- /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
3933
- "External service unavailable. Please try again."
3934
- ],
3935
- [/fetch failed|network error|abort.*timeout/i, "Network request failed. Please try again."],
3936
- [/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
3937
- // Supabase Edge Function internals
3938
- [/FunctionsHttpError|non-2xx status/i, "Backend service error. Please try again."],
3939
- [/JWT|token.*expired|token.*invalid/i, "Authentication expired. Please re-authenticate."],
3940
- // Generic sensitive patterns (API keys, URLs with secrets)
3941
- [/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i, "An internal error occurred. Please try again."]
3942
- ];
3943
- }
3944
- });
3945
-
3946
4212
  // src/lib/ssrf.ts
3947
4213
  import { promises as dnsPromises } from "node:dns";
3948
4214
  function isBlockedIP(ip) {
@@ -4229,6 +4495,122 @@ var init_quality = __esm({
4229
4495
  }
4230
4496
  });
4231
4497
 
4498
+ // src/lib/connected-account-routing.ts
4499
+ function canonicalPlatform(value) {
4500
+ const normalized = value.trim().toLowerCase();
4501
+ return normalized === "x" ? "twitter" : normalized;
4502
+ }
4503
+ function providerPlatform(value) {
4504
+ return PLATFORM_CASE_MAP[canonicalPlatform(value)] ?? value;
4505
+ }
4506
+ function isUsable(account) {
4507
+ const status = account.effective_status ?? account.status;
4508
+ return status === "active" || status === "expires_soon";
4509
+ }
4510
+ function normalizeRequestedIds(requested) {
4511
+ const ids = /* @__PURE__ */ new Map();
4512
+ for (const [platform3, accountId] of Object.entries(requested ?? {})) {
4513
+ const canonical = canonicalPlatform(platform3);
4514
+ const existing = ids.get(canonical);
4515
+ if (existing && existing !== accountId) {
4516
+ return {
4517
+ error: `Conflicting account IDs were supplied for ${platform3} and its platform alias.`
4518
+ };
4519
+ }
4520
+ ids.set(canonical, accountId);
4521
+ }
4522
+ return { ids };
4523
+ }
4524
+ async function resolveConnectedAccountRouting(input) {
4525
+ const normalizedRequested = normalizeRequestedIds(input.requestedAccountIds);
4526
+ if (normalizedRequested.error) return { error: normalizedRequested.error };
4527
+ const targetPlatforms = new Set(input.platforms.map(canonicalPlatform));
4528
+ for (const requestedPlatform of normalizedRequested.ids?.keys() ?? []) {
4529
+ if (!targetPlatforms.has(requestedPlatform)) {
4530
+ return {
4531
+ error: `An account ID was supplied for untargeted platform ${requestedPlatform}.`
4532
+ };
4533
+ }
4534
+ }
4535
+ const { data, error } = await callEdgeFunction(
4536
+ "mcp-data",
4537
+ {
4538
+ action: "connected-accounts",
4539
+ projectId: input.projectId,
4540
+ project_id: input.projectId
4541
+ },
4542
+ { timeoutMs: 1e4 }
4543
+ );
4544
+ if (error || !Array.isArray(data?.accounts)) {
4545
+ return {
4546
+ error: `Connected-account verification failed: ${error ?? data?.error ?? "no account inventory returned"}.`
4547
+ };
4548
+ }
4549
+ const connectedAccountIds = {};
4550
+ for (const platform3 of input.platforms) {
4551
+ const canonical = canonicalPlatform(platform3);
4552
+ const displayPlatform = providerPlatform(platform3);
4553
+ const platformAccounts = data.accounts.filter(
4554
+ (account) => canonicalPlatform(account.platform) === canonical && account.project_id === input.projectId && isUsable(account)
4555
+ );
4556
+ const requestedId = normalizedRequested.ids?.get(canonical);
4557
+ let selected;
4558
+ if (requestedId) {
4559
+ selected = data.accounts.find((account) => account.id === requestedId);
4560
+ if (!selected) {
4561
+ return {
4562
+ error: `${displayPlatform}: account ${requestedId} is not available for project_id ${input.projectId}.`
4563
+ };
4564
+ }
4565
+ if (canonicalPlatform(selected.platform) !== canonical) {
4566
+ return {
4567
+ error: `${displayPlatform}: account ${requestedId} belongs to ${selected.platform}.`
4568
+ };
4569
+ }
4570
+ if (selected.project_id !== input.projectId) {
4571
+ return {
4572
+ error: `${displayPlatform}: account ${requestedId} is not bound to project_id ${input.projectId}.`
4573
+ };
4574
+ }
4575
+ if (!isUsable(selected)) {
4576
+ return {
4577
+ error: `${displayPlatform}: account ${requestedId} is ${selected.effective_status ?? selected.status}.`
4578
+ };
4579
+ }
4580
+ } else if (platformAccounts.length === 1) {
4581
+ selected = platformAccounts[0];
4582
+ } else if (platformAccounts.length === 0) {
4583
+ return {
4584
+ error: `${displayPlatform}: no active account is bound to project_id ${input.projectId}.`
4585
+ };
4586
+ } else {
4587
+ return {
4588
+ error: `${displayPlatform}: multiple active accounts are bound to project_id ${input.projectId}; pass the exact account ID returned by list_connected_accounts.`
4589
+ };
4590
+ }
4591
+ connectedAccountIds[displayPlatform] = selected.id;
4592
+ }
4593
+ return { connectedAccountIds };
4594
+ }
4595
+ var PLATFORM_CASE_MAP;
4596
+ var init_connected_account_routing = __esm({
4597
+ "src/lib/connected-account-routing.ts"() {
4598
+ "use strict";
4599
+ init_edge_function();
4600
+ PLATFORM_CASE_MAP = {
4601
+ youtube: "YouTube",
4602
+ tiktok: "TikTok",
4603
+ instagram: "Instagram",
4604
+ twitter: "Twitter",
4605
+ x: "Twitter",
4606
+ linkedin: "LinkedIn",
4607
+ facebook: "Facebook",
4608
+ threads: "Threads",
4609
+ bluesky: "Bluesky"
4610
+ };
4611
+ }
4612
+ });
4613
+
4232
4614
  // src/tools/distribution.ts
4233
4615
  import { z as z3 } from "zod";
4234
4616
  import { createHash } from "node:crypto";
@@ -4341,21 +4723,6 @@ async function validatePublishMediaUrl(url) {
4341
4723
  function accountEffectiveStatus(account) {
4342
4724
  return account.effective_status || account.status;
4343
4725
  }
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
4726
  async function rehostExternalUrl(mediaUrl, projectId) {
4360
4727
  const ssrf = await validateUrlForSSRF(mediaUrl);
4361
4728
  if (!ssrf.isValid) {
@@ -4491,17 +4858,17 @@ function registerDistributionTools(server2) {
4491
4858
  schedule_at: z3.string().optional().describe(
4492
4859
  'ISO 8601 UTC datetime for scheduled posting (e.g. "2026-03-20T14:00:00Z"). Omit to post immediately. Must be in the future.'
4493
4860
  ),
4494
- project_id: z3.string().optional().describe(
4861
+ project_id: z3.string().uuid().optional().describe(
4495
4862
  "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
4863
  ),
4497
4864
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
4498
4865
  attribution: z3.boolean().optional().describe(
4499
4866
  'If true, appends "Created with Social Neuron" to the caption. Default: false.'
4500
4867
  ),
4501
- account_id: z3.string().optional().describe(
4502
- "Connected account ID to post from. Use list_connected_accounts to find the right ID. Required when multiple accounts exist for the same platform. If project_id is provided, the account must belong to that project or be unassigned."
4868
+ account_id: z3.string().uuid().optional().describe(
4869
+ "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
4870
  ),
4504
- account_ids: z3.record(z3.string(), z3.string()).optional().describe(
4871
+ account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
4505
4872
  '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
4873
  ),
4507
4874
  auto_rehost: z3.boolean().optional().describe(
@@ -4545,6 +4912,45 @@ function registerDistributionTools(server2) {
4545
4912
  isError: true
4546
4913
  };
4547
4914
  }
4915
+ const projectResolution = await resolveProjectForConnectedAccountTool(
4916
+ project_id,
4917
+ platforms
4918
+ );
4919
+ if (!projectResolution.projectId) {
4920
+ return {
4921
+ content: [
4922
+ {
4923
+ type: "text",
4924
+ 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."
4925
+ }
4926
+ ],
4927
+ isError: true
4928
+ };
4929
+ }
4930
+ const resolvedProjectId = projectResolution.projectId;
4931
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
4932
+ if (account_id && account_ids) {
4933
+ return {
4934
+ content: [
4935
+ {
4936
+ type: "text",
4937
+ text: "Pass either account_id or account_ids, not both."
4938
+ }
4939
+ ],
4940
+ isError: true
4941
+ };
4942
+ }
4943
+ if (account_id && platforms.length !== 1) {
4944
+ return {
4945
+ content: [
4946
+ {
4947
+ type: "text",
4948
+ text: "account_id is valid only for a single target platform. Use account_ids for multi-platform publishing."
4949
+ }
4950
+ ],
4951
+ isError: true
4952
+ };
4953
+ }
4548
4954
  const userId = await getDefaultUserId();
4549
4955
  const rateLimit = checkRateLimit("posting", `schedule_post:${userId}`);
4550
4956
  if (!rateLimit.allowed) {
@@ -4649,11 +5055,16 @@ function registerDistributionTools(server2) {
4649
5055
  }
4650
5056
  const resolvedJobs = resolved;
4651
5057
  resolvedMediaUrls = resolvedJobs.map((item) => item.url);
4652
- resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map((item) => item.trustedR2);
5058
+ resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map(
5059
+ (item) => item.trustedR2
5060
+ );
4653
5061
  }
4654
5062
  const shouldRehost = auto_rehost !== false;
4655
5063
  if (shouldRehost && resolvedMediaUrl && !resolvedMediaUrlIsTrustedR2) {
4656
- const rehost = await rehostExternalUrl(resolvedMediaUrl, project_id);
5064
+ const rehost = await rehostExternalUrl(
5065
+ resolvedMediaUrl,
5066
+ resolvedProjectId
5067
+ );
4657
5068
  if ("error" in rehost) {
4658
5069
  return {
4659
5070
  content: [
@@ -4671,7 +5082,7 @@ function registerDistributionTools(server2) {
4671
5082
  if (shouldRehost && resolvedMediaUrls && resolvedMediaUrls.length > 0) {
4672
5083
  const rehosted = await Promise.all(
4673
5084
  resolvedMediaUrls.map(
4674
- (u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, project_id)
5085
+ (u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, resolvedProjectId)
4675
5086
  )
4676
5087
  );
4677
5088
  const failIdx = rehosted.findIndex((r) => "error" in r);
@@ -4738,7 +5149,7 @@ function registerDistributionTools(server2) {
4738
5149
  }
4739
5150
  }
4740
5151
  const normalizedPlatforms = platforms.map(
4741
- (p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
5152
+ (p) => PLATFORM_CASE_MAP2[p.toLowerCase()] || p
4742
5153
  );
4743
5154
  const blockedPlatforms = normalizedPlatforms.filter(
4744
5155
  (p) => MCP_NOT_LIVE_FOR_POSTING.has(p)
@@ -4754,92 +5165,27 @@ function registerDistributionTools(server2) {
4754
5165
  isError: true
4755
5166
  };
4756
5167
  }
4757
- const { data: accountsData } = await callEdgeFunction(
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;
5168
+ let requestedAccountIds;
4832
5169
  if (account_id) {
4833
- connectedAccountIds = {};
4834
- for (const p of normalizedPlatforms) {
4835
- connectedAccountIds[p] = account_id;
4836
- }
5170
+ requestedAccountIds = { [normalizedPlatforms[0]]: account_id };
4837
5171
  } else if (account_ids) {
4838
- connectedAccountIds = {};
4839
- for (const [key, val] of Object.entries(account_ids)) {
4840
- const normalizedKey = PLATFORM_CASE_MAP[key.toLowerCase()] || key;
4841
- connectedAccountIds[normalizedKey] = val;
4842
- }
5172
+ requestedAccountIds = account_ids;
5173
+ }
5174
+ const routing = await resolveConnectedAccountRouting({
5175
+ projectId: resolvedProjectId,
5176
+ platforms: normalizedPlatforms,
5177
+ requestedAccountIds
5178
+ });
5179
+ if (routing.error || !routing.connectedAccountIds) {
5180
+ return {
5181
+ content: [
5182
+ {
5183
+ type: "text",
5184
+ text: `Cannot post \u2014 ${routing.error ?? "exact connected-account routing could not be established."}`
5185
+ }
5186
+ ],
5187
+ isError: true
5188
+ };
4843
5189
  }
4844
5190
  let finalCaption = caption;
4845
5191
  if (attribution && finalCaption) {
@@ -4893,8 +5239,9 @@ Created with Social Neuron`;
4893
5239
  title,
4894
5240
  hashtags,
4895
5241
  scheduledAt: schedule_at,
4896
- projectId: project_id,
4897
- ...connectedAccountIds ? { connectedAccountIds } : {},
5242
+ projectId: resolvedProjectId,
5243
+ project_id: resolvedProjectId,
5244
+ connectedAccountIds: routing.connectedAccountIds,
4898
5245
  ...normalizedPlatformMetadata ? {
4899
5246
  platformMetadata: convertPlatformMetadata(
4900
5247
  normalizedPlatformMetadata
@@ -4929,10 +5276,14 @@ Created with Social Neuron`;
4929
5276
  isError: true
4930
5277
  };
4931
5278
  }
5279
+ const responseData = projectAutoResolvedNote ? { ...data, project_auto_resolved: projectAutoResolvedNote } : data;
4932
5280
  const lines = [
4933
5281
  data.success ? "Post scheduled successfully." : "Post scheduling had errors.",
4934
5282
  `Scheduled for: ${data.scheduledAt}`
4935
5283
  ];
5284
+ if (projectAutoResolvedNote) {
5285
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
5286
+ }
4936
5287
  if (tiktokAutoInboxApplied) {
4937
5288
  lines.push(
4938
5289
  "",
@@ -4950,7 +5301,7 @@ Created with Social Neuron`;
4950
5301
  }
4951
5302
  }
4952
5303
  if (format === "json") {
4953
- const structuredContent = asEnvelope3(data);
5304
+ const structuredContent = asEnvelope3(responseData);
4954
5305
  return {
4955
5306
  structuredContent,
4956
5307
  content: [
@@ -4963,7 +5314,7 @@ Created with Social Neuron`;
4963
5314
  };
4964
5315
  }
4965
5316
  return {
4966
- structuredContent: asEnvelope3(data),
5317
+ structuredContent: asEnvelope3(responseData),
4967
5318
  content: [{ type: "text", text: lines.join("\n") }],
4968
5319
  isError: !data.success
4969
5320
  };
@@ -4977,7 +5328,9 @@ Created with Social Neuron`;
4977
5328
  project_id: z3.string().uuid().optional().describe(
4978
5329
  "Brand/project ID that owns the post. Defaults to the authenticated key's project or the account default."
4979
5330
  ),
4980
- scheduled_at: z3.string().datetime({ offset: true }).describe("New future publish time as an ISO 8601 datetime with timezone."),
5331
+ scheduled_at: z3.string().datetime({ offset: true }).describe(
5332
+ "New future publish time as an ISO 8601 datetime with timezone."
5333
+ ),
4981
5334
  expected_scheduled_at: z3.string().datetime({ offset: true }).optional().describe(
4982
5335
  "Optional current schedule timestamp. If it changed since you read it, the update is rejected instead of silently overwriting it."
4983
5336
  ),
@@ -5033,7 +5386,11 @@ Created with Social Neuron`;
5033
5386
  projectId: resolvedProjectId,
5034
5387
  project_id: resolvedProjectId,
5035
5388
  scheduled_at: next.toISOString(),
5036
- ...expected_scheduled_at ? { expected_scheduled_at: new Date(expected_scheduled_at).toISOString() } : {}
5389
+ ...expected_scheduled_at ? {
5390
+ expected_scheduled_at: new Date(
5391
+ expected_scheduled_at
5392
+ ).toISOString()
5393
+ } : {}
5037
5394
  });
5038
5395
  if (error || !result?.success) {
5039
5396
  const code = result?.error ?? error ?? "reschedule_failed";
@@ -5066,17 +5423,34 @@ Created with Social Neuron`;
5066
5423
  "list_connected_accounts",
5067
5424
  "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
5425
  {
5069
- project_id: z3.string().optional().describe(
5426
+ project_id: z3.string().uuid().optional().describe(
5070
5427
  "Brand/project ID to scope connected accounts. Use the same project_id when calling schedule_post."
5071
5428
  ),
5072
- include_all: z3.boolean().optional().describe("If true, include expired or inactive accounts as well as usable accounts."),
5429
+ include_all: z3.boolean().optional().describe(
5430
+ "If true, include expired or inactive accounts as well as usable accounts."
5431
+ ),
5073
5432
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
5074
5433
  },
5075
5434
  async ({ project_id, include_all, response_format }) => {
5076
5435
  const format = response_format ?? "text";
5436
+ const projectResolution = await resolveProjectForConnectedAccountTool(project_id);
5437
+ if (!projectResolution.projectId) {
5438
+ return {
5439
+ content: [
5440
+ {
5441
+ type: "text",
5442
+ 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."
5443
+ }
5444
+ ],
5445
+ isError: true
5446
+ };
5447
+ }
5448
+ const resolvedProjectId = projectResolution.projectId;
5449
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
5077
5450
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
5078
5451
  action: "connected-accounts",
5079
- ...project_id ? { projectId: project_id, project_id } : {},
5452
+ projectId: resolvedProjectId,
5453
+ project_id: resolvedProjectId,
5080
5454
  ...include_all ? { includeAll: true } : {}
5081
5455
  });
5082
5456
  if (efError || !result?.success) {
@@ -5090,10 +5464,27 @@ Created with Social Neuron`;
5090
5464
  isError: true
5091
5465
  };
5092
5466
  }
5093
- const accounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
5467
+ const parsedAccounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
5468
+ if (parsedAccounts.some(
5469
+ (account) => account.project_id !== resolvedProjectId
5470
+ )) {
5471
+ return {
5472
+ content: [
5473
+ {
5474
+ type: "text",
5475
+ text: "Connected-account project attestation failed. No account inventory was returned."
5476
+ }
5477
+ ],
5478
+ isError: true
5479
+ };
5480
+ }
5481
+ const accounts = parsedAccounts;
5094
5482
  if (accounts.length === 0) {
5095
5483
  if (format === "json") {
5096
- const structuredContent = asEnvelope3({ accounts: [] });
5484
+ const structuredContent = asEnvelope3({
5485
+ accounts: [],
5486
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
5487
+ });
5097
5488
  return {
5098
5489
  structuredContent,
5099
5490
  content: [
@@ -5108,13 +5499,15 @@ Created with Social Neuron`;
5108
5499
  content: [
5109
5500
  {
5110
5501
  type: "text",
5111
- text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections."
5502
+ text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections." + (projectAutoResolvedNote ? `
5503
+
5504
+ Note: ${projectAutoResolvedNote}` : "")
5112
5505
  }
5113
5506
  ]
5114
5507
  };
5115
5508
  }
5116
5509
  const lines = [
5117
- `${accounts.length} connected account(s)${project_id ? ` for project ${project_id}` : ""}:`,
5510
+ `${accounts.length} connected account(s) for project ${resolvedProjectId}:`,
5118
5511
  ""
5119
5512
  ];
5120
5513
  for (const account of accounts) {
@@ -5126,8 +5519,14 @@ Created with Social Neuron`;
5126
5519
  ` ${platformLower}: ${name} | id=${account.id} | ${project} | status=${status} (connected ${account.created_at.split("T")[0]})`
5127
5520
  );
5128
5521
  }
5522
+ if (projectAutoResolvedNote) {
5523
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
5524
+ }
5129
5525
  if (format === "json") {
5130
- const structuredContent = asEnvelope3({ accounts });
5526
+ const structuredContent = asEnvelope3({
5527
+ accounts,
5528
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
5529
+ });
5131
5530
  return {
5132
5531
  structuredContent,
5133
5532
  content: [
@@ -5322,7 +5721,10 @@ Created with Social Neuron`;
5322
5721
  if (!Number.isFinite(startDate.getTime())) {
5323
5722
  return {
5324
5723
  content: [
5325
- { type: "text", text: "start_after must be a valid ISO datetime." }
5724
+ {
5725
+ type: "text",
5726
+ text: "start_after must be a valid ISO datetime."
5727
+ }
5326
5728
  ],
5327
5729
  isError: true
5328
5730
  };
@@ -5423,11 +5825,14 @@ Created with Social Neuron`;
5423
5825
  "Schedule all posts in a content plan. Optionally auto-assigns time slots and runs quality checks before scheduling. Supports dry-run mode.",
5424
5826
  {
5425
5827
  plan: z3.object({
5828
+ project_id: z3.string().uuid().optional(),
5829
+ account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe("Exact connected-account ID per platform."),
5426
5830
  posts: z3.array(
5427
5831
  z3.object({
5428
5832
  id: z3.string(),
5429
5833
  caption: z3.string(),
5430
5834
  platform: z3.string(),
5835
+ connected_account_id: z3.string().uuid().optional(),
5431
5836
  title: z3.string().optional(),
5432
5837
  media_url: z3.string().optional(),
5433
5838
  schedule_at: z3.string().optional(),
@@ -5435,6 +5840,12 @@ Created with Social Neuron`;
5435
5840
  })
5436
5841
  )
5437
5842
  }).passthrough().optional(),
5843
+ project_id: z3.string().uuid().optional().describe(
5844
+ "Exact brand/project ID. Defaults only when the authenticated user has one project."
5845
+ ),
5846
+ account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
5847
+ "Exact connected-account ID per platform for every post in the plan."
5848
+ ),
5438
5849
  plan_id: z3.string().uuid().optional().describe("Persisted content plan ID from content_plans table"),
5439
5850
  auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
5440
5851
  dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
@@ -5451,6 +5862,8 @@ Created with Social Neuron`;
5451
5862
  async ({
5452
5863
  plan,
5453
5864
  plan_id,
5865
+ project_id,
5866
+ account_ids,
5454
5867
  auto_slot,
5455
5868
  dry_run,
5456
5869
  response_format,
@@ -5460,9 +5873,11 @@ Created with Social Neuron`;
5460
5873
  idempotency_seed
5461
5874
  }) => {
5462
5875
  try {
5876
+ const effectiveBatchSize = batch_size ?? 4;
5463
5877
  let workingPlan = plan;
5464
5878
  let effectivePlanId = plan_id;
5465
- let effectiveProjectId;
5879
+ let effectiveProjectId = project_id;
5880
+ let projectAutoResolvedNote;
5466
5881
  let approvalSummary;
5467
5882
  if (!workingPlan && plan_id) {
5468
5883
  const { data: planResult, error: planError } = await callEdgeFunction("mcp-data", { action: "get-content-plan", plan_id });
@@ -5509,23 +5924,59 @@ Created with Social Neuron`;
5509
5924
  posts: postsFromPayload
5510
5925
  };
5511
5926
  effectivePlanId = stored.id;
5512
- effectiveProjectId = stored.project_id ?? void 0;
5927
+ if (effectiveProjectId && stored.project_id && effectiveProjectId !== stored.project_id) {
5928
+ return {
5929
+ content: [
5930
+ {
5931
+ type: "text",
5932
+ text: `project_id ${effectiveProjectId} does not own plan ${plan_id}.`
5933
+ }
5934
+ ],
5935
+ isError: true
5936
+ };
5937
+ }
5938
+ effectiveProjectId = stored.project_id ?? effectiveProjectId;
5513
5939
  }
5514
5940
  if (!workingPlan) {
5515
5941
  return {
5516
5942
  content: [
5517
5943
  {
5518
5944
  type: "text",
5519
- text: "Provide either `plan` (inline) or `plan_id` (persisted) to schedule content."
5945
+ text: "Provide either `plan` (inline) or `plan_id` (persisted) to schedule content."
5946
+ }
5947
+ ],
5948
+ isError: true
5949
+ };
5950
+ }
5951
+ const planProjectId = workingPlan.project_id;
5952
+ if (effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0 && effectiveProjectId !== planProjectId) {
5953
+ return {
5954
+ content: [
5955
+ {
5956
+ type: "text",
5957
+ text: `Conflicting project_id values were supplied for the plan (${planProjectId}) and request (${effectiveProjectId}).`
5520
5958
  }
5521
5959
  ],
5522
5960
  isError: true
5523
5961
  };
5524
5962
  }
5963
+ if (!effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0) {
5964
+ effectiveProjectId = planProjectId;
5965
+ }
5525
5966
  if (!effectiveProjectId) {
5526
- const planProjectId = workingPlan.project_id;
5527
- if (typeof planProjectId === "string" && planProjectId.length > 0) {
5528
- effectiveProjectId = planProjectId;
5967
+ const projectResolution = await resolveProjectForConnectedAccountTool();
5968
+ effectiveProjectId = projectResolution.projectId;
5969
+ projectAutoResolvedNote = projectResolution.autoResolvedNote;
5970
+ if (!effectiveProjectId) {
5971
+ return {
5972
+ content: [
5973
+ {
5974
+ type: "text",
5975
+ 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."
5976
+ }
5977
+ ],
5978
+ isError: true
5979
+ };
5529
5980
  }
5530
5981
  }
5531
5982
  if (effectivePlanId) {
@@ -5744,6 +6195,61 @@ Created with Social Neuron`;
5744
6195
  isError: false
5745
6196
  };
5746
6197
  }
6198
+ const embeddedAccountIds = workingPlan.account_ids ?? {};
6199
+ for (const [platform3, accountId] of Object.entries(account_ids ?? {})) {
6200
+ if (embeddedAccountIds[platform3] && embeddedAccountIds[platform3] !== accountId) {
6201
+ return {
6202
+ content: [
6203
+ {
6204
+ type: "text",
6205
+ text: `Conflicting account_ids values were supplied for ${platform3}.`
6206
+ }
6207
+ ],
6208
+ isError: true
6209
+ };
6210
+ }
6211
+ }
6212
+ const requestedPlanAccountIds = {
6213
+ ...embeddedAccountIds,
6214
+ ...account_ids ?? {}
6215
+ };
6216
+ for (const post of workingPlan.posts) {
6217
+ if (!post.connected_account_id) continue;
6218
+ const key = post.platform.toLowerCase() === "x" ? "twitter" : post.platform.toLowerCase();
6219
+ const existing = requestedPlanAccountIds[key];
6220
+ if (existing && existing !== post.connected_account_id) {
6221
+ return {
6222
+ content: [
6223
+ {
6224
+ type: "text",
6225
+ text: `Plan contains conflicting connected_account_id values for ${post.platform}. Use separate plans when scheduling the same platform through different accounts.`
6226
+ }
6227
+ ],
6228
+ isError: true
6229
+ };
6230
+ }
6231
+ requestedPlanAccountIds[key] = post.connected_account_id;
6232
+ }
6233
+ const planPlatforms = Array.from(
6234
+ new Set(workingPlan.posts.map((post) => post.platform))
6235
+ );
6236
+ const planRouting = await resolveConnectedAccountRouting({
6237
+ projectId: effectiveProjectId,
6238
+ platforms: planPlatforms,
6239
+ requestedAccountIds: requestedPlanAccountIds
6240
+ });
6241
+ if (planRouting.error || !planRouting.connectedAccountIds) {
6242
+ return {
6243
+ content: [
6244
+ {
6245
+ type: "text",
6246
+ text: `Cannot schedule plan \u2014 ${planRouting.error ?? "exact connected-account routing could not be established."}`
6247
+ }
6248
+ ],
6249
+ isError: true
6250
+ };
6251
+ }
6252
+ const verifiedPlanAccountIds = planRouting.connectedAccountIds;
5747
6253
  let scheduled = 0;
5748
6254
  let failed = 0;
5749
6255
  const results = [];
@@ -5772,7 +6278,7 @@ Created with Social Neuron`;
5772
6278
  retryable: false
5773
6279
  };
5774
6280
  }
5775
- const normalizedPlatform = PLATFORM_CASE_MAP[post.platform.toLowerCase()] ?? post.platform;
6281
+ const normalizedPlatform = PLATFORM_CASE_MAP2[post.platform.toLowerCase()] ?? post.platform;
5776
6282
  const idempotencyKey = buildIdempotencyKey(post);
5777
6283
  const { data, error } = await callEdgeFunction(
5778
6284
  "schedule-post",
@@ -5783,6 +6289,13 @@ Created with Social Neuron`;
5783
6289
  mediaUrl: post.media_url,
5784
6290
  scheduledAt: post.schedule_at,
5785
6291
  hashtags: post.hashtags,
6292
+ ...effectiveProjectId ? {
6293
+ projectId: effectiveProjectId,
6294
+ project_id: effectiveProjectId
6295
+ } : {},
6296
+ connectedAccountIds: {
6297
+ [normalizedPlatform]: verifiedPlanAccountIds[normalizedPlatform]
6298
+ },
5786
6299
  ...effectivePlanId ? { planId: effectivePlanId } : {},
5787
6300
  idempotencyKey
5788
6301
  },
@@ -5841,7 +6354,7 @@ Created with Social Neuron`;
5841
6354
  const platformBatches = Array.from(grouped.entries()).map(
5842
6355
  async ([platform3, platformPosts]) => {
5843
6356
  const platformResults = [];
5844
- const batches = chunk(platformPosts, batch_size);
6357
+ const batches = chunk(platformPosts, effectiveBatchSize);
5845
6358
  for (const batch of batches) {
5846
6359
  const settled = await Promise.allSettled(
5847
6360
  batch.map((post) => scheduleOne(post))
@@ -5902,7 +6415,8 @@ Created with Social Neuron`;
5902
6415
  total_posts: workingPlan.posts.length,
5903
6416
  scheduled,
5904
6417
  failed
5905
- }
6418
+ },
6419
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
5906
6420
  }),
5907
6421
  null,
5908
6422
  2
@@ -5926,6 +6440,9 @@ Created with Social Neuron`;
5926
6440
  lines.push(
5927
6441
  `Scheduled: ${scheduled}/${workingPlan.posts.length} | Failed: ${failed}/${workingPlan.posts.length}`
5928
6442
  );
6443
+ if (projectAutoResolvedNote) {
6444
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
6445
+ }
5929
6446
  return {
5930
6447
  content: [{ type: "text", text: lines.join("\n") }],
5931
6448
  isError: failed > 0
@@ -5945,7 +6462,7 @@ Created with Social Neuron`;
5945
6462
  }
5946
6463
  );
5947
6464
  }
5948
- var PLATFORM_CASE_MAP, TIKTOK_AUDIT_APPROVED, MCP_NOT_LIVE_FOR_POSTING, VIDEO_FILE_EXTENSIONS, IMAGE_FILE_EXTENSIONS;
6465
+ var PLATFORM_CASE_MAP2, TIKTOK_AUDIT_APPROVED, MCP_NOT_LIVE_FOR_POSTING, VIDEO_FILE_EXTENSIONS, IMAGE_FILE_EXTENSIONS;
5949
6466
  var init_distribution = __esm({
5950
6467
  "src/tools/distribution.ts"() {
5951
6468
  "use strict";
@@ -5956,11 +6473,17 @@ var init_distribution = __esm({
5956
6473
  init_supabase();
5957
6474
  init_quality();
5958
6475
  init_version();
5959
- PLATFORM_CASE_MAP = {
6476
+ init_connected_account_routing();
6477
+ PLATFORM_CASE_MAP2 = {
5960
6478
  youtube: "YouTube",
5961
6479
  tiktok: "TikTok",
5962
6480
  instagram: "Instagram",
5963
6481
  twitter: "Twitter",
6482
+ // 'x' is the platform's current branding but connected_account_routing.ts
6483
+ // (and the DB convention) still key on 'Twitter' — keep both aliases
6484
+ // resolving to the same case so schedule_content_plan's platform:'x' posts
6485
+ // don't fall through to an undefined binding (F8, 2026-07-15).
6486
+ x: "Twitter",
5964
6487
  linkedin: "LinkedIn",
5965
6488
  facebook: "Facebook",
5966
6489
  threads: "Threads",
@@ -6502,15 +7025,35 @@ function registerAnalyticsTools(server2) {
6502
7025
  content_id: z5.string().uuid().optional().describe(
6503
7026
  "Filter to a specific content_history ID to see performance of one piece of content."
6504
7027
  ),
6505
- project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
7028
+ project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
6506
7029
  limit: z5.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
6507
7030
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6508
7031
  },
6509
- async ({ platform: platform3, days, content_id, project_id, limit, response_format }) => {
7032
+ async ({
7033
+ platform: platform3,
7034
+ days,
7035
+ content_id,
7036
+ project_id,
7037
+ limit,
7038
+ response_format
7039
+ }) => {
6510
7040
  const format = response_format ?? "text";
6511
7041
  const lookbackDays = days ?? 30;
6512
7042
  const maxPosts = limit ?? 20;
6513
- const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
7043
+ const projectResolution = await resolveProjectStrict(project_id);
7044
+ if (!projectResolution.projectId) {
7045
+ return {
7046
+ content: [
7047
+ {
7048
+ type: "text",
7049
+ 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."
7050
+ }
7051
+ ],
7052
+ isError: true
7053
+ };
7054
+ }
7055
+ const resolvedProjectId = projectResolution.projectId;
7056
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
6514
7057
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
6515
7058
  action: "analytics",
6516
7059
  platform: platform3,
@@ -6521,11 +7064,17 @@ function registerAnalyticsTools(server2) {
6521
7064
  limit: Math.min(maxPosts * 5, 100),
6522
7065
  latestOnly: true,
6523
7066
  contentId: content_id,
6524
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
7067
+ projectId: resolvedProjectId,
7068
+ project_id: resolvedProjectId
6525
7069
  });
6526
7070
  if (efError) {
6527
7071
  return {
6528
- content: [{ type: "text", text: `Failed to fetch analytics: ${efError}` }],
7072
+ content: [
7073
+ {
7074
+ type: "text",
7075
+ text: `Failed to fetch analytics: ${efError}`
7076
+ }
7077
+ ],
6529
7078
  isError: true
6530
7079
  };
6531
7080
  }
@@ -6545,7 +7094,8 @@ function registerAnalyticsTools(server2) {
6545
7094
  totalViews: 0,
6546
7095
  totalEngagement: 0,
6547
7096
  postCount: 0,
6548
- posts: []
7097
+ posts: [],
7098
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
6549
7099
  });
6550
7100
  return {
6551
7101
  structuredContent,
@@ -6561,7 +7111,9 @@ function registerAnalyticsTools(server2) {
6561
7111
  content: [
6562
7112
  {
6563
7113
  type: "text",
6564
- text: `No analytics data found for the last ${lookbackDays} days${platform3 ? ` on ${platform3}` : ""}.`
7114
+ text: `No analytics data found for the last ${lookbackDays} days${platform3 ? ` on ${platform3}` : ""}.` + (projectAutoResolvedNote ? `
7115
+
7116
+ Note: ${projectAutoResolvedNote}` : "")
6565
7117
  }
6566
7118
  ]
6567
7119
  };
@@ -6593,20 +7145,27 @@ function registerAnalyticsTools(server2) {
6593
7145
  postCount: posts.length,
6594
7146
  posts
6595
7147
  };
6596
- return formatAnalytics(summary, lookbackDays, format);
7148
+ return formatAnalytics(
7149
+ summary,
7150
+ lookbackDays,
7151
+ format,
7152
+ projectAutoResolvedNote
7153
+ );
6597
7154
  }
6598
7155
  );
6599
7156
  server2.tool(
6600
7157
  "refresh_platform_analytics",
6601
7158
  "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
7159
  {
6603
- project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
7160
+ project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
6604
7161
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6605
7162
  },
6606
7163
  async ({ project_id, response_format }) => {
6607
7164
  const format = response_format ?? "text";
6608
7165
  const userId = await getDefaultUserId();
6609
- const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
7166
+ const projectResolution = await resolveProjectStrict(project_id);
7167
+ const resolvedProjectId = projectResolution.projectId;
7168
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
6610
7169
  const rateLimit = checkRateLimit(
6611
7170
  "posting",
6612
7171
  `refresh_platform_analytics:${userId}:${resolvedProjectId ?? "default"}`
@@ -6622,25 +7181,48 @@ function registerAnalyticsTools(server2) {
6622
7181
  isError: true
6623
7182
  };
6624
7183
  }
7184
+ if (!resolvedProjectId) {
7185
+ return {
7186
+ content: [
7187
+ {
7188
+ type: "text",
7189
+ 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."
7190
+ }
7191
+ ],
7192
+ isError: true
7193
+ };
7194
+ }
6625
7195
  const { data, error } = await callEdgeFunction("fetch-analytics", {
6626
7196
  userId,
6627
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
7197
+ projectId: resolvedProjectId,
7198
+ project_id: resolvedProjectId
6628
7199
  });
6629
7200
  if (error) {
6630
7201
  return {
6631
- content: [{ type: "text", text: `Error refreshing analytics: ${error}` }],
7202
+ content: [
7203
+ {
7204
+ type: "text",
7205
+ text: `Error refreshing analytics: ${error}`
7206
+ }
7207
+ ],
6632
7208
  isError: true
6633
7209
  };
6634
7210
  }
6635
7211
  const result = data;
6636
7212
  if (!result.success) {
6637
7213
  return {
6638
- content: [{ type: "text", text: "Analytics refresh failed." }],
7214
+ content: [
7215
+ { type: "text", text: "Analytics refresh failed." }
7216
+ ],
6639
7217
  isError: true
6640
7218
  };
6641
7219
  }
6642
- const queued = (result.results ?? []).filter((r) => r.status === "queued").length;
6643
- const errored = (result.results ?? []).filter((r) => r.status === "error").length;
7220
+ const queued = (result.results ?? []).filter(
7221
+ (r) => r.status === "queued"
7222
+ ).length;
7223
+ const errored = (result.results ?? []).filter(
7224
+ (r) => r.status === "error"
7225
+ ).length;
6644
7226
  const lines = [
6645
7227
  `Analytics refresh triggered successfully.`,
6646
7228
  ` Posts processed: ${result.postsProcessed}`,
@@ -6649,13 +7231,17 @@ function registerAnalyticsTools(server2) {
6649
7231
  if (errored > 0) {
6650
7232
  lines.push(` Errors: ${errored}`);
6651
7233
  }
7234
+ if (projectAutoResolvedNote) {
7235
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
7236
+ }
6652
7237
  if (format === "json") {
6653
7238
  const structuredContent = asEnvelope4({
6654
7239
  success: true,
6655
7240
  postsProcessed: result.postsProcessed,
6656
7241
  queued,
6657
7242
  errored,
6658
- projectId: resolvedProjectId ?? null
7243
+ projectId: resolvedProjectId ?? null,
7244
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
6659
7245
  });
6660
7246
  return {
6661
7247
  structuredContent,
@@ -6671,12 +7257,21 @@ function registerAnalyticsTools(server2) {
6671
7257
  }
6672
7258
  );
6673
7259
  }
6674
- function formatAnalytics(summary, days, format) {
6675
- const structuredContent = asEnvelope4({ ...summary, days });
7260
+ function formatAnalytics(summary, days, format, projectAutoResolvedNote) {
7261
+ const structuredContent = asEnvelope4({
7262
+ ...summary,
7263
+ days,
7264
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
7265
+ });
6676
7266
  if (format === "json") {
6677
7267
  return {
6678
7268
  structuredContent,
6679
- content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }]
7269
+ content: [
7270
+ {
7271
+ type: "text",
7272
+ text: JSON.stringify(structuredContent, null, 2)
7273
+ }
7274
+ ]
6680
7275
  };
6681
7276
  }
6682
7277
  const lines = [
@@ -6700,6 +7295,9 @@ function formatAnalytics(summary, days, format) {
6700
7295
  lines.push(line);
6701
7296
  }
6702
7297
  }
7298
+ if (projectAutoResolvedNote) {
7299
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
7300
+ }
6703
7301
  return {
6704
7302
  structuredContent,
6705
7303
  content: [{ type: "text", text: lines.join("\n") }]
@@ -8129,15 +8727,64 @@ function registerYouTubeAnalyticsTools(server2) {
8129
8727
  start_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Start date in YYYY-MM-DD format."),
8130
8728
  end_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("End date in YYYY-MM-DD format."),
8131
8729
  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('Max videos to return for "topVideos" action. Defaults to 10.'),
8730
+ max_results: z10.number().min(1).max(50).optional().describe(
8731
+ 'Max videos to return for "topVideos" action. Defaults to 10.'
8732
+ ),
8733
+ connected_account_id: z10.string().uuid().optional().describe(
8734
+ "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."
8735
+ ),
8736
+ project_id: z10.string().uuid().optional().describe(
8737
+ "Exact brand/project ID. Defaults only when the authenticated user has one project."
8738
+ ),
8133
8739
  response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8134
8740
  },
8135
- async ({ action, start_date, end_date, video_id, max_results, response_format }) => {
8741
+ async ({
8742
+ action,
8743
+ start_date,
8744
+ end_date,
8745
+ video_id,
8746
+ max_results,
8747
+ connected_account_id,
8748
+ project_id,
8749
+ response_format
8750
+ }) => {
8136
8751
  const format = response_format ?? "text";
8137
8752
  if (action === "video" && !video_id) {
8138
8753
  return {
8139
8754
  content: [
8140
- { type: "text", text: 'Error: video_id is required when action is "video".' }
8755
+ {
8756
+ type: "text",
8757
+ text: 'Error: video_id is required when action is "video".'
8758
+ }
8759
+ ],
8760
+ isError: true
8761
+ };
8762
+ }
8763
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
8764
+ if (!resolvedProjectId) {
8765
+ return {
8766
+ content: [
8767
+ {
8768
+ type: "text",
8769
+ text: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
8770
+ }
8771
+ ],
8772
+ isError: true
8773
+ };
8774
+ }
8775
+ const routing = await resolveConnectedAccountRouting({
8776
+ projectId: resolvedProjectId,
8777
+ platforms: ["youtube"],
8778
+ requestedAccountIds: connected_account_id ? { youtube: connected_account_id } : void 0
8779
+ });
8780
+ const resolvedAccountId = routing.connectedAccountIds?.YouTube;
8781
+ if (routing.error || !resolvedAccountId) {
8782
+ return {
8783
+ content: [
8784
+ {
8785
+ type: "text",
8786
+ text: routing.error ?? "YouTube: exact connected-account routing could not be established."
8787
+ }
8141
8788
  ],
8142
8789
  isError: true
8143
8790
  };
@@ -8147,11 +8794,19 @@ function registerYouTubeAnalyticsTools(server2) {
8147
8794
  startDate: start_date,
8148
8795
  endDate: end_date,
8149
8796
  videoId: video_id,
8150
- maxResults: max_results ?? 10
8797
+ maxResults: max_results ?? 10,
8798
+ projectId: resolvedProjectId,
8799
+ project_id: resolvedProjectId,
8800
+ connectedAccountId: resolvedAccountId
8151
8801
  });
8152
8802
  if (error) {
8153
8803
  return {
8154
- content: [{ type: "text", text: `YouTube Analytics error: ${error}` }],
8804
+ content: [
8805
+ {
8806
+ type: "text",
8807
+ text: `YouTube Analytics error: ${error}`
8808
+ }
8809
+ ],
8155
8810
  isError: true
8156
8811
  };
8157
8812
  }
@@ -8164,7 +8819,12 @@ function registerYouTubeAnalyticsTools(server2) {
8164
8819
  {
8165
8820
  type: "text",
8166
8821
  text: JSON.stringify(
8167
- asEnvelope7({ action, startDate: start_date, endDate: end_date, analytics: a }),
8822
+ asEnvelope7({
8823
+ action,
8824
+ startDate: start_date,
8825
+ endDate: end_date,
8826
+ analytics: a
8827
+ }),
8168
8828
  null,
8169
8829
  2
8170
8830
  )
@@ -8191,7 +8851,10 @@ function registerYouTubeAnalyticsTools(server2) {
8191
8851
  if (days.length === 0) {
8192
8852
  return {
8193
8853
  content: [
8194
- { type: "text", text: "No daily analytics data found for this period." }
8854
+ {
8855
+ type: "text",
8856
+ text: "No daily analytics data found for this period."
8857
+ }
8195
8858
  ]
8196
8859
  };
8197
8860
  }
@@ -8214,7 +8877,10 @@ function registerYouTubeAnalyticsTools(server2) {
8214
8877
  ]
8215
8878
  };
8216
8879
  }
8217
- const lines = [`YouTube Daily Analytics (${start_date} to ${end_date}):`, ""];
8880
+ const lines = [
8881
+ `YouTube Daily Analytics (${start_date} to ${end_date}):`,
8882
+ ""
8883
+ ];
8218
8884
  for (const d of days) {
8219
8885
  lines.push(
8220
8886
  ` ${d.date}: ${d.views.toLocaleString()} views, ${d.watchTimeMinutes.toLocaleString()} min watch, +${d.subscribersGained} subs, ${d.likes} likes, ${d.comments} comments`
@@ -8261,7 +8927,12 @@ function registerYouTubeAnalyticsTools(server2) {
8261
8927
  const videos = result.topVideos ?? [];
8262
8928
  if (videos.length === 0) {
8263
8929
  return {
8264
- content: [{ type: "text", text: "No top videos found for this period." }]
8930
+ content: [
8931
+ {
8932
+ type: "text",
8933
+ text: "No top videos found for this period."
8934
+ }
8935
+ ]
8265
8936
  };
8266
8937
  }
8267
8938
  if (format === "json") {
@@ -8283,7 +8954,10 @@ function registerYouTubeAnalyticsTools(server2) {
8283
8954
  ]
8284
8955
  };
8285
8956
  }
8286
- const lines = [`Top ${videos.length} YouTube Videos (${start_date} to ${end_date}):`, ""];
8957
+ const lines = [
8958
+ `Top ${videos.length} YouTube Videos (${start_date} to ${end_date}):`,
8959
+ ""
8960
+ ];
8287
8961
  for (let i = 0; i < videos.length; i++) {
8288
8962
  const v = videos[i];
8289
8963
  lines.push(
@@ -8296,11 +8970,18 @@ function registerYouTubeAnalyticsTools(server2) {
8296
8970
  }
8297
8971
  if (format === "json") {
8298
8972
  return {
8299
- content: [{ type: "text", text: JSON.stringify(asEnvelope7(result), null, 2) }]
8973
+ content: [
8974
+ {
8975
+ type: "text",
8976
+ text: JSON.stringify(asEnvelope7(result), null, 2)
8977
+ }
8978
+ ]
8300
8979
  };
8301
8980
  }
8302
8981
  return {
8303
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
8982
+ content: [
8983
+ { type: "text", text: JSON.stringify(result, null, 2) }
8984
+ ]
8304
8985
  };
8305
8986
  }
8306
8987
  );
@@ -8309,6 +8990,8 @@ var init_youtube_analytics = __esm({
8309
8990
  "src/tools/youtube-analytics.ts"() {
8310
8991
  "use strict";
8311
8992
  init_edge_function();
8993
+ init_supabase();
8994
+ init_connected_account_routing();
8312
8995
  init_version();
8313
8996
  }
8314
8997
  });
@@ -8324,6 +9007,29 @@ function asEnvelope8(data) {
8324
9007
  data
8325
9008
  };
8326
9009
  }
9010
+ async function exactYouTubeRoute(projectId, connectedAccountId) {
9011
+ const resolvedProjectId = projectId ?? await getDefaultProjectId() ?? void 0;
9012
+ if (!resolvedProjectId) {
9013
+ return {
9014
+ error: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
9015
+ };
9016
+ }
9017
+ const routing = await resolveConnectedAccountRouting({
9018
+ projectId: resolvedProjectId,
9019
+ platforms: ["youtube"],
9020
+ requestedAccountIds: connectedAccountId ? { youtube: connectedAccountId } : void 0
9021
+ });
9022
+ const resolvedAccountId = routing.connectedAccountIds?.YouTube;
9023
+ if (routing.error || !resolvedAccountId) {
9024
+ return {
9025
+ error: routing.error ?? "YouTube: exact connected-account routing could not be established."
9026
+ };
9027
+ }
9028
+ return {
9029
+ projectId: resolvedProjectId,
9030
+ connectedAccountId: resolvedAccountId
9031
+ };
9032
+ }
8327
9033
  function registerCommentsTools(server2) {
8328
9034
  server2.tool(
8329
9035
  "list_comments",
@@ -8336,19 +9042,42 @@ function registerCommentsTools(server2) {
8336
9042
  page_token: z11.string().optional().describe(
8337
9043
  "Pagination cursor from previous list_comments response nextPageToken field. Omit for first page of results."
8338
9044
  ),
9045
+ connected_account_id: z11.string().uuid().optional().describe(
9046
+ "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."
9047
+ ),
9048
+ project_id: PROJECT_ID_SCHEMA,
8339
9049
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8340
9050
  },
8341
- async ({ video_id, max_results, page_token, response_format }) => {
9051
+ async ({
9052
+ video_id,
9053
+ max_results,
9054
+ page_token,
9055
+ connected_account_id,
9056
+ project_id,
9057
+ response_format
9058
+ }) => {
8342
9059
  const format = response_format ?? "text";
9060
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
9061
+ if ("error" in route) {
9062
+ return {
9063
+ content: [{ type: "text", text: route.error }],
9064
+ isError: true
9065
+ };
9066
+ }
8343
9067
  const { data, error } = await callEdgeFunction("youtube-comments", {
8344
9068
  action: "list",
8345
9069
  videoId: video_id,
8346
9070
  maxResults: max_results ?? 50,
8347
- pageToken: page_token
9071
+ pageToken: page_token,
9072
+ projectId: route.projectId,
9073
+ project_id: route.projectId,
9074
+ connectedAccountId: route.connectedAccountId
8348
9075
  });
8349
9076
  if (error) {
8350
9077
  return {
8351
- content: [{ type: "text", text: `Error listing comments: ${error}` }],
9078
+ content: [
9079
+ { type: "text", text: `Error listing comments: ${error}` }
9080
+ ],
8352
9081
  isError: true
8353
9082
  };
8354
9083
  }
@@ -8360,7 +9089,10 @@ function registerCommentsTools(server2) {
8360
9089
  {
8361
9090
  type: "text",
8362
9091
  text: JSON.stringify(
8363
- asEnvelope8({ comments, nextPageToken: result.nextPageToken ?? null }),
9092
+ asEnvelope8({
9093
+ comments,
9094
+ nextPageToken: result.nextPageToken ?? null
9095
+ }),
8364
9096
  null,
8365
9097
  2
8366
9098
  )
@@ -8397,12 +9129,31 @@ function registerCommentsTools(server2) {
8397
9129
  "reply_to_comment",
8398
9130
  "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
9131
  {
8400
- parent_id: z11.string().describe("The ID of the parent comment to reply to (from list_comments)."),
9132
+ parent_id: z11.string().describe(
9133
+ "The ID of the parent comment to reply to (from list_comments)."
9134
+ ),
8401
9135
  text: z11.string().min(1).describe("The reply text."),
9136
+ connected_account_id: z11.string().uuid().optional().describe(
9137
+ "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."
9138
+ ),
9139
+ project_id: PROJECT_ID_SCHEMA,
8402
9140
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8403
9141
  },
8404
- async ({ parent_id, text, response_format }) => {
9142
+ async ({
9143
+ parent_id,
9144
+ text,
9145
+ connected_account_id,
9146
+ project_id,
9147
+ response_format
9148
+ }) => {
8405
9149
  const format = response_format ?? "text";
9150
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
9151
+ if ("error" in route) {
9152
+ return {
9153
+ content: [{ type: "text", text: route.error }],
9154
+ isError: true
9155
+ };
9156
+ }
8406
9157
  const userId = await getDefaultUserId();
8407
9158
  const rateLimit = checkRateLimit("posting", `reply_to_comment:${userId}`);
8408
9159
  if (!rateLimit.allowed) {
@@ -8419,18 +9170,31 @@ function registerCommentsTools(server2) {
8419
9170
  const { data, error } = await callEdgeFunction("youtube-comments", {
8420
9171
  action: "reply",
8421
9172
  parentId: parent_id,
8422
- text
9173
+ text,
9174
+ projectId: route.projectId,
9175
+ project_id: route.projectId,
9176
+ connectedAccountId: route.connectedAccountId
8423
9177
  });
8424
9178
  if (error) {
8425
9179
  return {
8426
- content: [{ type: "text", text: `Error replying to comment: ${error}` }],
9180
+ content: [
9181
+ {
9182
+ type: "text",
9183
+ text: `Error replying to comment: ${error}`
9184
+ }
9185
+ ],
8427
9186
  isError: true
8428
9187
  };
8429
9188
  }
8430
9189
  const result = data;
8431
9190
  if (format === "json") {
8432
9191
  return {
8433
- content: [{ type: "text", text: JSON.stringify(asEnvelope8(result), null, 2) }]
9192
+ content: [
9193
+ {
9194
+ type: "text",
9195
+ text: JSON.stringify(asEnvelope8(result), null, 2)
9196
+ }
9197
+ ]
8434
9198
  };
8435
9199
  }
8436
9200
  return {
@@ -8451,10 +9215,27 @@ function registerCommentsTools(server2) {
8451
9215
  {
8452
9216
  video_id: z11.string().describe("The YouTube video ID to comment on."),
8453
9217
  text: z11.string().min(1).describe("The comment text."),
9218
+ connected_account_id: z11.string().uuid().optional().describe(
9219
+ "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."
9220
+ ),
9221
+ project_id: PROJECT_ID_SCHEMA,
8454
9222
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8455
9223
  },
8456
- async ({ video_id, text, response_format }) => {
9224
+ async ({
9225
+ video_id,
9226
+ text,
9227
+ connected_account_id,
9228
+ project_id,
9229
+ response_format
9230
+ }) => {
8457
9231
  const format = response_format ?? "text";
9232
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
9233
+ if ("error" in route) {
9234
+ return {
9235
+ content: [{ type: "text", text: route.error }],
9236
+ isError: true
9237
+ };
9238
+ }
8458
9239
  const userId = await getDefaultUserId();
8459
9240
  const rateLimit = checkRateLimit("posting", `post_comment:${userId}`);
8460
9241
  if (!rateLimit.allowed) {
@@ -8471,18 +9252,28 @@ function registerCommentsTools(server2) {
8471
9252
  const { data, error } = await callEdgeFunction("youtube-comments", {
8472
9253
  action: "post",
8473
9254
  videoId: video_id,
8474
- text
9255
+ text,
9256
+ projectId: route.projectId,
9257
+ project_id: route.projectId,
9258
+ connectedAccountId: route.connectedAccountId
8475
9259
  });
8476
9260
  if (error) {
8477
9261
  return {
8478
- content: [{ type: "text", text: `Error posting comment: ${error}` }],
9262
+ content: [
9263
+ { type: "text", text: `Error posting comment: ${error}` }
9264
+ ],
8479
9265
  isError: true
8480
9266
  };
8481
9267
  }
8482
9268
  const result = data;
8483
9269
  if (format === "json") {
8484
9270
  return {
8485
- content: [{ type: "text", text: JSON.stringify(asEnvelope8(result), null, 2) }]
9271
+ content: [
9272
+ {
9273
+ type: "text",
9274
+ text: JSON.stringify(asEnvelope8(result), null, 2)
9275
+ }
9276
+ ]
8486
9277
  };
8487
9278
  }
8488
9279
  return {
@@ -8503,10 +9294,27 @@ function registerCommentsTools(server2) {
8503
9294
  {
8504
9295
  comment_id: z11.string().describe("The comment ID to moderate."),
8505
9296
  moderation_status: z11.enum(["published", "rejected"]).describe('"published" to approve, "rejected" to hide.'),
9297
+ connected_account_id: z11.string().uuid().optional().describe(
9298
+ "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."
9299
+ ),
9300
+ project_id: PROJECT_ID_SCHEMA,
8506
9301
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8507
9302
  },
8508
- async ({ comment_id, moderation_status, response_format }) => {
9303
+ async ({
9304
+ comment_id,
9305
+ moderation_status,
9306
+ connected_account_id,
9307
+ project_id,
9308
+ response_format
9309
+ }) => {
8509
9310
  const format = response_format ?? "text";
9311
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
9312
+ if ("error" in route) {
9313
+ return {
9314
+ content: [{ type: "text", text: route.error }],
9315
+ isError: true
9316
+ };
9317
+ }
8510
9318
  const userId = await getDefaultUserId();
8511
9319
  const rateLimit = checkRateLimit("posting", `moderate_comment:${userId}`);
8512
9320
  if (!rateLimit.allowed) {
@@ -8523,11 +9331,19 @@ function registerCommentsTools(server2) {
8523
9331
  const { error } = await callEdgeFunction("youtube-comments", {
8524
9332
  action: "moderate",
8525
9333
  commentId: comment_id,
8526
- moderationStatus: moderation_status
9334
+ moderationStatus: moderation_status,
9335
+ projectId: route.projectId,
9336
+ project_id: route.projectId,
9337
+ connectedAccountId: route.connectedAccountId
8527
9338
  });
8528
9339
  if (error) {
8529
9340
  return {
8530
- content: [{ type: "text", text: `Error moderating comment: ${error}` }],
9341
+ content: [
9342
+ {
9343
+ type: "text",
9344
+ text: `Error moderating comment: ${error}`
9345
+ }
9346
+ ],
8531
9347
  isError: true
8532
9348
  };
8533
9349
  }
@@ -8564,10 +9380,26 @@ function registerCommentsTools(server2) {
8564
9380
  "Delete a YouTube comment. Only works for comments owned by the authenticated channel.",
8565
9381
  {
8566
9382
  comment_id: z11.string().describe("The comment ID to delete."),
9383
+ connected_account_id: z11.string().uuid().optional().describe(
9384
+ "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."
9385
+ ),
9386
+ project_id: PROJECT_ID_SCHEMA,
8567
9387
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8568
9388
  },
8569
- async ({ comment_id, response_format }) => {
9389
+ async ({
9390
+ comment_id,
9391
+ connected_account_id,
9392
+ project_id,
9393
+ response_format
9394
+ }) => {
8570
9395
  const format = response_format ?? "text";
9396
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
9397
+ if ("error" in route) {
9398
+ return {
9399
+ content: [{ type: "text", text: route.error }],
9400
+ isError: true
9401
+ };
9402
+ }
8571
9403
  const userId = await getDefaultUserId();
8572
9404
  const rateLimit = checkRateLimit("posting", `delete_comment:${userId}`);
8573
9405
  if (!rateLimit.allowed) {
@@ -8583,11 +9415,16 @@ function registerCommentsTools(server2) {
8583
9415
  }
8584
9416
  const { error } = await callEdgeFunction("youtube-comments", {
8585
9417
  action: "delete",
8586
- commentId: comment_id
9418
+ commentId: comment_id,
9419
+ projectId: route.projectId,
9420
+ project_id: route.projectId,
9421
+ connectedAccountId: route.connectedAccountId
8587
9422
  });
8588
9423
  if (error) {
8589
9424
  return {
8590
- content: [{ type: "text", text: `Error deleting comment: ${error}` }],
9425
+ content: [
9426
+ { type: "text", text: `Error deleting comment: ${error}` }
9427
+ ],
8591
9428
  isError: true
8592
9429
  };
8593
9430
  }
@@ -8596,24 +9433,38 @@ function registerCommentsTools(server2) {
8596
9433
  content: [
8597
9434
  {
8598
9435
  type: "text",
8599
- text: JSON.stringify(asEnvelope8({ success: true, commentId: comment_id }), null, 2)
9436
+ text: JSON.stringify(
9437
+ asEnvelope8({ success: true, commentId: comment_id }),
9438
+ null,
9439
+ 2
9440
+ )
8600
9441
  }
8601
9442
  ]
8602
9443
  };
8603
9444
  }
8604
9445
  return {
8605
- content: [{ type: "text", text: `Comment ${comment_id} deleted successfully.` }]
9446
+ content: [
9447
+ {
9448
+ type: "text",
9449
+ text: `Comment ${comment_id} deleted successfully.`
9450
+ }
9451
+ ]
8606
9452
  };
8607
9453
  }
8608
9454
  );
8609
9455
  }
9456
+ var PROJECT_ID_SCHEMA;
8610
9457
  var init_comments = __esm({
8611
9458
  "src/tools/comments.ts"() {
8612
9459
  "use strict";
8613
9460
  init_edge_function();
8614
9461
  init_rate_limit();
8615
9462
  init_supabase();
9463
+ init_connected_account_routing();
8616
9464
  init_version();
9465
+ PROJECT_ID_SCHEMA = z11.string().uuid().optional().describe(
9466
+ "Exact brand/project ID. Defaults only when the authenticated user has one project."
9467
+ );
8617
9468
  }
8618
9469
  });
8619
9470
 
@@ -11324,6 +12175,14 @@ var init_plan_approvals = __esm({
11324
12175
 
11325
12176
  // src/tools/discovery.ts
11326
12177
  import { z as z23 } from "zod";
12178
+ function isHostedTransport() {
12179
+ return process.env.MCP_TRANSPORT === "http";
12180
+ }
12181
+ function isPubliclyDiscoverable(tool) {
12182
+ if (tool.internal || tool.hiddenFromPublicCount) return false;
12183
+ if (tool.localOnly && isHostedTransport()) return false;
12184
+ return true;
12185
+ }
11327
12186
  function toolKnowledgeDocument(tool) {
11328
12187
  const lines = [
11329
12188
  `Tool: ${tool.name}`,
@@ -11334,7 +12193,8 @@ function toolKnowledgeDocument(tool) {
11334
12193
  if (tool.task_intent) lines.push(`Task intent: ${tool.task_intent}`);
11335
12194
  if (tool.use_when) lines.push(`Use when: ${tool.use_when}`);
11336
12195
  if (tool.avoid_when) lines.push(`Avoid when: ${tool.avoid_when}`);
11337
- if (tool.next_tools?.length) lines.push(`Common next tools: ${tool.next_tools.join(", ")}`);
12196
+ if (tool.next_tools?.length)
12197
+ lines.push(`Common next tools: ${tool.next_tools.join(", ")}`);
11338
12198
  return {
11339
12199
  id: `tool:${tool.name}`,
11340
12200
  title: `MCP tool: ${tool.name}`,
@@ -11351,9 +12211,7 @@ function toolKnowledgeDocument(tool) {
11351
12211
  function getKnowledgeDocuments() {
11352
12212
  return [
11353
12213
  ...STATIC_KNOWLEDGE_DOCUMENTS,
11354
- ...TOOL_CATALOG.filter((t) => !t.internal && !t.hiddenFromPublicCount).map(
11355
- toolKnowledgeDocument
11356
- )
12214
+ ...TOOL_CATALOG.filter(isPubliclyDiscoverable).map(toolKnowledgeDocument)
11357
12215
  ];
11358
12216
  }
11359
12217
  function tokenize(input) {
@@ -11407,7 +12265,9 @@ function registerDiscoveryTools(server2) {
11407
12265
  };
11408
12266
  return {
11409
12267
  structuredContent,
11410
- content: [{ type: "text", text: JSON.stringify(structuredContent) }]
12268
+ content: [
12269
+ { type: "text", text: JSON.stringify(structuredContent) }
12270
+ ]
11411
12271
  };
11412
12272
  }
11413
12273
  );
@@ -11428,10 +12288,14 @@ function registerDiscoveryTools(server2) {
11428
12288
  }
11429
12289
  },
11430
12290
  async ({ id }) => {
11431
- const doc = getKnowledgeDocuments().find((candidate) => candidate.id === id);
12291
+ const doc = getKnowledgeDocuments().find(
12292
+ (candidate) => candidate.id === id
12293
+ );
11432
12294
  if (!doc) {
11433
12295
  return {
11434
- content: [{ type: "text", text: `Document not found: ${id}` }],
12296
+ content: [
12297
+ { type: "text", text: `Document not found: ${id}` }
12298
+ ],
11435
12299
  isError: true
11436
12300
  };
11437
12301
  }
@@ -11444,7 +12308,9 @@ function registerDiscoveryTools(server2) {
11444
12308
  };
11445
12309
  return {
11446
12310
  structuredContent,
11447
- content: [{ type: "text", text: JSON.stringify(structuredContent) }]
12311
+ content: [
12312
+ { type: "text", text: JSON.stringify(structuredContent) }
12313
+ ]
11448
12314
  };
11449
12315
  }
11450
12316
  );
@@ -11453,7 +12319,9 @@ function registerDiscoveryTools(server2) {
11453
12319
  'Search available tools by name, description, module, or scope. Use "name" detail (~50 tokens) for quick lookup, "summary" (~500 tokens) for descriptions, "full" for complete input schemas. Start here if unsure which tool to call \u2014 filter by module (e.g. "planning", "content", "analytics") to narrow results.',
11454
12320
  {
11455
12321
  query: z23.string().optional().describe("Search query to filter tools by name or description"),
11456
- module: z23.string().optional().describe('Filter by module name (e.g. "planning", "content", "analytics")'),
12322
+ module: z23.string().optional().describe(
12323
+ 'Filter by module name (e.g. "planning", "content", "analytics")'
12324
+ ),
11457
12325
  scope: z23.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
11458
12326
  detail: z23.enum(["name", "summary", "full"]).default("summary").describe(
11459
12327
  'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
@@ -11470,14 +12338,18 @@ function registerDiscoveryTools(server2) {
11470
12338
  if (query) {
11471
12339
  results = searchTools(query);
11472
12340
  }
11473
- results = results.filter((t) => !t.internal && !t.hiddenFromPublicCount);
12341
+ results = results.filter(isPubliclyDiscoverable);
11474
12342
  if (module) {
11475
12343
  const moduleTools = getToolsByModule(module);
11476
- results = results.filter((t) => moduleTools.some((mt) => mt.name === t.name));
12344
+ results = results.filter(
12345
+ (t) => moduleTools.some((mt) => mt.name === t.name)
12346
+ );
11477
12347
  }
11478
12348
  if (scope) {
11479
12349
  const scopeTools = getToolsByScope(scope);
11480
- results = results.filter((t) => scopeTools.some((st) => st.name === t.name));
12350
+ results = results.filter(
12351
+ (t) => scopeTools.some((st) => st.name === t.name)
12352
+ );
11481
12353
  }
11482
12354
  if (available_only) {
11483
12355
  results = results.filter(isAvailable);
@@ -11515,7 +12387,12 @@ function registerDiscoveryTools(server2) {
11515
12387
  text: JSON.stringify(
11516
12388
  {
11517
12389
  toolCount: results.length,
11518
- ...hasKnownScopes ? { scopes: { available: currentScopes, unavailable_matches: unavailableCount } } : {},
12390
+ ...hasKnownScopes ? {
12391
+ scopes: {
12392
+ available: currentScopes,
12393
+ unavailable_matches: unavailableCount
12394
+ }
12395
+ } : {},
11519
12396
  tools: output
11520
12397
  },
11521
12398
  null,
@@ -11606,7 +12483,10 @@ var init_discovery = __esm({
11606
12483
  import { z as z24 } from "zod";
11607
12484
  import { randomUUID as randomUUID3 } from "node:crypto";
11608
12485
  function asEnvelope20(data) {
11609
- return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
12486
+ return {
12487
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
12488
+ data
12489
+ };
11610
12490
  }
11611
12491
  function registerPipelineTools(server2) {
11612
12492
  server2.tool(
@@ -11629,7 +12509,10 @@ function registerPipelineTools(server2) {
11629
12509
  action: "pipeline-readiness",
11630
12510
  platforms,
11631
12511
  estimated_posts,
11632
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
12512
+ ...resolvedProjectId ? {
12513
+ projectId: resolvedProjectId,
12514
+ project_id: resolvedProjectId
12515
+ } : {}
11633
12516
  },
11634
12517
  { timeoutMs: 15e3 }
11635
12518
  );
@@ -11647,16 +12530,24 @@ function registerPipelineTools(server2) {
11647
12530
  const blockers = [];
11648
12531
  const warnings = [];
11649
12532
  if (!isUnlimited && credits < estimatedCost) {
11650
- blockers.push(`Insufficient credits: ${credits} available, ~${estimatedCost} needed`);
12533
+ blockers.push(
12534
+ `Insufficient credits: ${credits} available, ~${estimatedCost} needed`
12535
+ );
11651
12536
  }
11652
12537
  if (missingPlatforms.length > 0) {
11653
- blockers.push(`Missing connected accounts: ${missingPlatforms.join(", ")}`);
12538
+ blockers.push(
12539
+ `Missing connected accounts: ${missingPlatforms.join(", ")}`
12540
+ );
11654
12541
  }
11655
12542
  if (!hasBrand) {
11656
- warnings.push("No brand profile found. Content will use generic voice.");
12543
+ warnings.push(
12544
+ "No brand profile found. Content will use generic voice."
12545
+ );
11657
12546
  }
11658
12547
  if (pendingApprovals > 0) {
11659
- warnings.push(`${pendingApprovals} pending approval(s) from previous runs.`);
12548
+ warnings.push(
12549
+ `${pendingApprovals} pending approval(s) from previous runs.`
12550
+ );
11660
12551
  }
11661
12552
  if (!insightsFresh) {
11662
12553
  warnings.push(
@@ -11671,7 +12562,10 @@ function registerPipelineTools(server2) {
11671
12562
  estimated_cost: estimatedCost,
11672
12563
  sufficient: credits >= estimatedCost
11673
12564
  },
11674
- connected_accounts: { platforms: connectedPlatforms, missing: missingPlatforms },
12565
+ connected_accounts: {
12566
+ platforms: connectedPlatforms,
12567
+ missing: missingPlatforms
12568
+ },
11675
12569
  brand_profile: { exists: hasBrand },
11676
12570
  pending_approvals: { count: pendingApprovals },
11677
12571
  insights_available: {
@@ -11685,11 +12579,18 @@ function registerPipelineTools(server2) {
11685
12579
  };
11686
12580
  if (format === "json") {
11687
12581
  return {
11688
- content: [{ type: "text", text: JSON.stringify(asEnvelope20(result), null, 2) }]
12582
+ content: [
12583
+ {
12584
+ type: "text",
12585
+ text: JSON.stringify(asEnvelope20(result), null, 2)
12586
+ }
12587
+ ]
11689
12588
  };
11690
12589
  }
11691
12590
  const lines = [];
11692
- lines.push(`Pipeline Readiness: ${result.ready ? "READY" : "NOT READY"}`);
12591
+ lines.push(
12592
+ `Pipeline Readiness: ${result.ready ? "READY" : "NOT READY"}`
12593
+ );
11693
12594
  lines.push("=".repeat(40));
11694
12595
  lines.push(
11695
12596
  `Credits: ${credits} available, ~${estimatedCost} needed \u2014 ${credits >= estimatedCost ? "OK" : "BLOCKED"}`
@@ -11697,7 +12598,9 @@ function registerPipelineTools(server2) {
11697
12598
  lines.push(
11698
12599
  `Accounts: ${connectedPlatforms.length} connected${missingPlatforms.length > 0 ? ` (missing: ${missingPlatforms.join(", ")})` : " \u2014 OK"}`
11699
12600
  );
11700
- lines.push(`Brand: ${hasBrand ? "OK" : "Missing (will use generic voice)"}`);
12601
+ lines.push(
12602
+ `Brand: ${hasBrand ? "OK" : "Missing (will use generic voice)"}`
12603
+ );
11701
12604
  lines.push(`Pending Approvals: ${pendingApprovals}`);
11702
12605
  lines.push(
11703
12606
  `Insights: ${insightsFresh ? "Fresh" : insightAge === null ? "None available" : `${insightAge} days old`}`
@@ -11716,7 +12619,12 @@ function registerPipelineTools(server2) {
11716
12619
  } catch (err) {
11717
12620
  const message = sanitizeError(err);
11718
12621
  return {
11719
- content: [{ type: "text", text: `Readiness check failed: ${message}` }],
12622
+ content: [
12623
+ {
12624
+ type: "text",
12625
+ text: `Readiness check failed: ${message}`
12626
+ }
12627
+ ],
11720
12628
  isError: true
11721
12629
  };
11722
12630
  }
@@ -11730,6 +12638,9 @@ function registerPipelineTools(server2) {
11730
12638
  topic: z24.string().optional().describe("Content topic (required if no source_url)"),
11731
12639
  source_url: z24.string().optional().describe("URL to extract content from"),
11732
12640
  platforms: z24.array(PLATFORM_ENUM2).min(1).describe("Target platforms"),
12641
+ account_ids: z24.record(z24.string(), z24.string().uuid()).optional().describe(
12642
+ "Exact connected-account ID per target platform. Required when a project has multiple accounts on one platform."
12643
+ ),
11733
12644
  days: z24.number().min(1).max(7).default(5).describe("Days to plan"),
11734
12645
  posts_per_day: z24.number().min(1).max(3).default(1).describe("Posts per platform per day"),
11735
12646
  approval_mode: z24.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe(
@@ -11751,6 +12662,7 @@ function registerPipelineTools(server2) {
11751
12662
  topic,
11752
12663
  source_url,
11753
12664
  platforms,
12665
+ account_ids,
11754
12666
  days,
11755
12667
  posts_per_day,
11756
12668
  approval_mode,
@@ -11768,7 +12680,12 @@ function registerPipelineTools(server2) {
11768
12680
  let creditsUsed = 0;
11769
12681
  if (!topic && !source_url) {
11770
12682
  return {
11771
- content: [{ type: "text", text: "Either topic or source_url is required." }],
12683
+ content: [
12684
+ {
12685
+ type: "text",
12686
+ text: "Either topic or source_url is required."
12687
+ }
12688
+ ],
11772
12689
  isError: true
11773
12690
  };
11774
12691
  }
@@ -11798,6 +12715,35 @@ function registerPipelineTools(server2) {
11798
12715
  }
11799
12716
  try {
11800
12717
  const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
12718
+ if (schedulingRequested && !resolvedProjectId) {
12719
+ return {
12720
+ content: [
12721
+ {
12722
+ type: "text",
12723
+ text: "A project_id is required to schedule pipeline output. Configure an explicit project or use an API key scoped to exactly one project."
12724
+ }
12725
+ ],
12726
+ isError: true
12727
+ };
12728
+ }
12729
+ if (schedulingRequested) {
12730
+ const preflightRouting = await resolveConnectedAccountRouting({
12731
+ projectId: resolvedProjectId,
12732
+ platforms,
12733
+ requestedAccountIds: account_ids
12734
+ });
12735
+ if (preflightRouting.error || !preflightRouting.connectedAccountIds) {
12736
+ return {
12737
+ content: [
12738
+ {
12739
+ type: "text",
12740
+ text: `Cannot run publishing pipeline \u2014 ${preflightRouting.error ?? "exact connected-account routing could not be established."} (checked before any credits were spent.)`
12741
+ }
12742
+ ],
12743
+ isError: true
12744
+ };
12745
+ }
12746
+ }
11801
12747
  const estimatedCost = BASE_PLAN_CREDITS + (source_url ? SOURCE_EXTRACTION_CREDITS : 0);
11802
12748
  const { data: budgetData } = await callEdgeFunction(
11803
12749
  "mcp-data",
@@ -11851,7 +12797,13 @@ function registerPipelineTools(server2) {
11851
12797
  "social-neuron-ai",
11852
12798
  {
11853
12799
  type: "generation",
11854
- prompt: buildPlanPrompt(resolvedTopic, platforms, days, posts_per_day, source_url),
12800
+ prompt: buildPlanPrompt(
12801
+ resolvedTopic,
12802
+ platforms,
12803
+ days,
12804
+ posts_per_day,
12805
+ source_url
12806
+ ),
11855
12807
  model: "gemini-2.5-flash",
11856
12808
  responseFormat: "json",
11857
12809
  ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
@@ -11859,7 +12811,10 @@ function registerPipelineTools(server2) {
11859
12811
  { timeoutMs: 6e4 }
11860
12812
  );
11861
12813
  if (planError || !planData) {
11862
- errors.push({ stage: "planning", message: planError ?? "No AI response" });
12814
+ errors.push({
12815
+ stage: "planning",
12816
+ message: planError ?? "No AI response"
12817
+ });
11863
12818
  await callEdgeFunction(
11864
12819
  "mcp-data",
11865
12820
  {
@@ -11876,7 +12831,10 @@ function registerPipelineTools(server2) {
11876
12831
  );
11877
12832
  return {
11878
12833
  content: [
11879
- { type: "text", text: `Planning failed: ${planError ?? "No AI response"}` }
12834
+ {
12835
+ type: "text",
12836
+ text: `Planning failed: ${planError ?? "No AI response"}`
12837
+ }
11880
12838
  ],
11881
12839
  isError: true
11882
12840
  };
@@ -11906,20 +12864,22 @@ function registerPipelineTools(server2) {
11906
12864
  const postsArray = extractJsonArray(rawText);
11907
12865
  const requestedPlatformSet = new Set(platforms);
11908
12866
  const maxPosts = platforms.length * days * posts_per_day;
11909
- const parsedPosts = (postsArray ?? []).map((p) => ({
11910
- id: String(p.id ?? randomUUID3().slice(0, 8)),
11911
- day: Number(p.day ?? 1),
11912
- date: String(p.date ?? ""),
11913
- platform: String(p.platform ?? ""),
11914
- content_type: p.content_type ?? "caption",
11915
- caption: String(p.caption ?? ""),
11916
- title: p.title ? String(p.title) : void 0,
11917
- hashtags: Array.isArray(p.hashtags) ? p.hashtags.map(String) : void 0,
11918
- hook: String(p.hook ?? ""),
11919
- angle: String(p.angle ?? ""),
11920
- visual_direction: p.visual_direction ? String(p.visual_direction) : void 0,
11921
- media_type: p.media_type ? String(p.media_type) : void 0
11922
- }));
12867
+ const parsedPosts = (postsArray ?? []).map(
12868
+ (p) => ({
12869
+ id: String(p.id ?? randomUUID3().slice(0, 8)),
12870
+ day: Number(p.day ?? 1),
12871
+ date: String(p.date ?? ""),
12872
+ platform: String(p.platform ?? ""),
12873
+ content_type: p.content_type ?? "caption",
12874
+ caption: String(p.caption ?? ""),
12875
+ title: p.title ? String(p.title) : void 0,
12876
+ hashtags: Array.isArray(p.hashtags) ? p.hashtags.map(String) : void 0,
12877
+ hook: String(p.hook ?? ""),
12878
+ angle: String(p.angle ?? ""),
12879
+ visual_direction: p.visual_direction ? String(p.visual_direction) : void 0,
12880
+ media_type: p.media_type ? String(p.media_type) : void 0
12881
+ })
12882
+ );
11923
12883
  const platformFilteredPosts = parsedPosts.filter(
11924
12884
  (post) => requestedPlatformSet.has(post.platform)
11925
12885
  );
@@ -12002,7 +12962,10 @@ function registerPipelineTools(server2) {
12002
12962
  estimated_credits: estimatedCost,
12003
12963
  generated_at: (/* @__PURE__ */ new Date()).toISOString()
12004
12964
  },
12005
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
12965
+ ...resolvedProjectId ? {
12966
+ projectId: resolvedProjectId,
12967
+ project_id: resolvedProjectId
12968
+ } : {}
12006
12969
  },
12007
12970
  { timeoutMs: 1e4 }
12008
12971
  );
@@ -12055,6 +13018,22 @@ function registerPipelineTools(server2) {
12055
13018
  }
12056
13019
  }
12057
13020
  let postsScheduled = 0;
13021
+ const pipelineRouting = schedulingRequested && postsApproved > 0 ? await resolveConnectedAccountRouting({
13022
+ projectId: resolvedProjectId,
13023
+ platforms,
13024
+ requestedAccountIds: account_ids
13025
+ }) : void 0;
13026
+ if (pipelineRouting?.error || schedulingRequested && postsApproved > 0 && !pipelineRouting?.connectedAccountIds) {
13027
+ return {
13028
+ content: [
13029
+ {
13030
+ type: "text",
13031
+ text: `Cannot run publishing pipeline \u2014 ${pipelineRouting?.error ?? "exact connected-account routing could not be established."}`
13032
+ }
13033
+ ],
13034
+ isError: true
13035
+ };
13036
+ }
12058
13037
  if (!dry_run && !skipSet.has("schedule") && postsApproved > 0) {
12059
13038
  const approvedPosts = posts.filter((p) => p.status === "approved");
12060
13039
  const scheduleBase = /* @__PURE__ */ new Date();
@@ -12062,10 +13041,27 @@ function registerPipelineTools(server2) {
12062
13041
  const scheduleBaseMs = scheduleBase.getTime();
12063
13042
  for (const post of approvedPosts) {
12064
13043
  if (creditsUsed >= creditLimit) {
12065
- errors.push({ stage: "schedule", message: "Credit limit reached" });
13044
+ errors.push({
13045
+ stage: "schedule",
13046
+ message: "Credit limit reached"
13047
+ });
12066
13048
  break;
12067
13049
  }
12068
- const scheduledAt = post.schedule_at ?? new Date(scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5).toISOString();
13050
+ const scheduledAt = post.schedule_at ?? new Date(
13051
+ scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5
13052
+ ).toISOString();
13053
+ const route = Object.entries(
13054
+ pipelineRouting.connectedAccountIds
13055
+ ).find(
13056
+ ([platform3]) => platform3.toLowerCase() === post.platform.toLowerCase()
13057
+ );
13058
+ if (!route) {
13059
+ errors.push({
13060
+ stage: "schedule",
13061
+ message: `No verified connected account route for ${post.platform}`
13062
+ });
13063
+ continue;
13064
+ }
12069
13065
  try {
12070
13066
  const { error: schedError } = await callEdgeFunction(
12071
13067
  "schedule-post",
@@ -12078,7 +13074,11 @@ function registerPipelineTools(server2) {
12078
13074
  scheduledAt,
12079
13075
  planId,
12080
13076
  idempotencyKey: `pipeline-${planId}-${post.id}`,
12081
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
13077
+ ...resolvedProjectId ? {
13078
+ projectId: resolvedProjectId,
13079
+ project_id: resolvedProjectId
13080
+ } : {},
13081
+ connectedAccountIds: { [route[0]]: route[1] }
12082
13082
  },
12083
13083
  { timeoutMs: 15e3 }
12084
13084
  );
@@ -12144,12 +13144,17 @@ function registerPipelineTools(server2) {
12144
13144
  if (response_format === "json") {
12145
13145
  return {
12146
13146
  content: [
12147
- { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
13147
+ {
13148
+ type: "text",
13149
+ text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
13150
+ }
12148
13151
  ]
12149
13152
  };
12150
13153
  }
12151
13154
  const lines = [];
12152
- lines.push(`Pipeline ${pipelineId.slice(0, 8)}... ${finalStatus.toUpperCase()}`);
13155
+ lines.push(
13156
+ `Pipeline ${pipelineId.slice(0, 8)}... ${finalStatus.toUpperCase()}`
13157
+ );
12153
13158
  lines.push("=".repeat(40));
12154
13159
  lines.push(`Posts generated: ${posts.length}`);
12155
13160
  lines.push(`Posts approved: ${postsApproved}`);
@@ -12188,7 +13193,9 @@ function registerPipelineTools(server2) {
12188
13193
  } catch {
12189
13194
  }
12190
13195
  return {
12191
- content: [{ type: "text", text: `Pipeline failed: ${message}` }],
13196
+ content: [
13197
+ { type: "text", text: `Pipeline failed: ${message}` }
13198
+ ],
12192
13199
  isError: true
12193
13200
  };
12194
13201
  }
@@ -12225,7 +13232,12 @@ function registerPipelineTools(server2) {
12225
13232
  }
12226
13233
  if (format === "json") {
12227
13234
  return {
12228
- content: [{ type: "text", text: JSON.stringify(asEnvelope20(data), null, 2) }]
13235
+ content: [
13236
+ {
13237
+ type: "text",
13238
+ text: JSON.stringify(asEnvelope20(data), null, 2)
13239
+ }
13240
+ ]
12229
13241
  };
12230
13242
  }
12231
13243
  const lines = [];
@@ -12265,10 +13277,19 @@ function registerPipelineTools(server2) {
12265
13277
  },
12266
13278
  async ({ plan_id, quality_threshold, response_format }) => {
12267
13279
  try {
12268
- const { data: loadResult, error: loadError } = await callEdgeFunction("mcp-data", { action: "auto-approve-plan", plan_id }, { timeoutMs: 1e4 });
13280
+ const { data: loadResult, error: loadError } = await callEdgeFunction(
13281
+ "mcp-data",
13282
+ { action: "auto-approve-plan", plan_id },
13283
+ { timeoutMs: 1e4 }
13284
+ );
12269
13285
  if (loadError) {
12270
13286
  return {
12271
- content: [{ type: "text", text: `Failed to load plan: ${loadError}` }],
13287
+ content: [
13288
+ {
13289
+ type: "text",
13290
+ text: `Failed to load plan: ${loadError}`
13291
+ }
13292
+ ],
12272
13293
  isError: true
12273
13294
  };
12274
13295
  }
@@ -12276,7 +13297,10 @@ function registerPipelineTools(server2) {
12276
13297
  if (!stored?.plan_payload) {
12277
13298
  return {
12278
13299
  content: [
12279
- { type: "text", text: `No content plan found for plan_id=${plan_id}` }
13300
+ {
13301
+ type: "text",
13302
+ text: `No content plan found for plan_id=${plan_id}`
13303
+ }
12280
13304
  ],
12281
13305
  isError: true
12282
13306
  };
@@ -12303,7 +13327,11 @@ function registerPipelineTools(server2) {
12303
13327
  blockers: []
12304
13328
  };
12305
13329
  autoApproved++;
12306
- details.push({ post_id: post.id, action: "approved", score: quality.total });
13330
+ details.push({
13331
+ post_id: post.id,
13332
+ action: "approved",
13333
+ score: quality.total
13334
+ });
12307
13335
  } else if (quality.total >= quality_threshold - 5) {
12308
13336
  post.status = "needs_edit";
12309
13337
  post.quality = {
@@ -12313,7 +13341,11 @@ function registerPipelineTools(server2) {
12313
13341
  blockers: quality.blockers
12314
13342
  };
12315
13343
  flagged++;
12316
- details.push({ post_id: post.id, action: "flagged", score: quality.total });
13344
+ details.push({
13345
+ post_id: post.id,
13346
+ action: "flagged",
13347
+ score: quality.total
13348
+ });
12317
13349
  } else {
12318
13350
  post.status = "rejected";
12319
13351
  post.quality = {
@@ -12323,7 +13355,11 @@ function registerPipelineTools(server2) {
12323
13355
  blockers: quality.blockers
12324
13356
  };
12325
13357
  rejected++;
12326
- details.push({ post_id: post.id, action: "rejected", score: quality.total });
13358
+ details.push({
13359
+ post_id: post.id,
13360
+ action: "rejected",
13361
+ score: quality.total
13362
+ });
12327
13363
  }
12328
13364
  }
12329
13365
  const newStatus = flagged === 0 && rejected === 0 ? "approved" : "in_review";
@@ -12359,7 +13395,10 @@ function registerPipelineTools(server2) {
12359
13395
  if (response_format === "json") {
12360
13396
  return {
12361
13397
  content: [
12362
- { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
13398
+ {
13399
+ type: "text",
13400
+ text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
13401
+ }
12363
13402
  ]
12364
13403
  };
12365
13404
  }
@@ -12380,7 +13419,9 @@ function registerPipelineTools(server2) {
12380
13419
  } catch (err) {
12381
13420
  const message = sanitizeError(err);
12382
13421
  return {
12383
- content: [{ type: "text", text: `Auto-approve failed: ${message}` }],
13422
+ content: [
13423
+ { type: "text", text: `Auto-approve failed: ${message}` }
13424
+ ],
12384
13425
  isError: true
12385
13426
  };
12386
13427
  }
@@ -12414,6 +13455,7 @@ var init_pipeline = __esm({
12414
13455
  init_quality();
12415
13456
  init_version();
12416
13457
  init_parse_utils();
13458
+ init_connected_account_routing();
12417
13459
  PLATFORM_ENUM2 = z24.enum([
12418
13460
  "youtube",
12419
13461
  "tiktok",
@@ -13941,25 +14983,39 @@ function registerCarouselTools(server2) {
13941
14983
  ]).optional().describe("Carousel template. Default: hormozi-authority."),
13942
14984
  slide_count: z28.number().min(3).max(10).optional().describe("Number of slides (3-10). Default: 7."),
13943
14985
  aspect_ratio: z28.enum(["1:1", "4:5", "9:16"]).optional().describe("Aspect ratio for both carousel and images. Default: 1:1."),
13944
- style: z28.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe("Visual style. Default: hormozi for hormozi-authority template."),
14986
+ style: z28.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
14987
+ "Visual style. Default: hormozi for hormozi-authority template."
14988
+ ),
13945
14989
  image_style_suffix: z28.string().max(500).optional().describe(
13946
14990
  'Style suffix appended to every image prompt for visual consistency across slides. Example: "dark moody lighting, cinematic, 35mm film grain".'
13947
14991
  ),
13948
14992
  hook: z28.string().max(300).optional().describe(
13949
14993
  "Explicit hook/opener for slide 1. Overrides any hook derived from topic. Keep under 15 words for strongest scroll-stop."
13950
14994
  ),
13951
- hook_family: z28.enum(["curiosity", "authority", "pain_point", "contrarian", "data_driven"]).optional().describe(
14995
+ hook_family: z28.enum([
14996
+ "curiosity",
14997
+ "authority",
14998
+ "pain_point",
14999
+ "contrarian",
15000
+ "data_driven"
15001
+ ]).optional().describe(
13952
15002
  "Hook family tag. Recorded on the carousel so downstream analytics can attribute engagement to hook pattern."
13953
15003
  ),
13954
- cta_text: z28.string().max(200).optional().describe("Explicit CTA copy for the final slide. If omitted, derived from topic."),
13955
- cta_url: z28.string().url().optional().describe("URL promoted on the CTA slide. Defaults to the project landing page."),
15004
+ cta_text: z28.string().max(200).optional().describe(
15005
+ "Explicit CTA copy for the final slide. If omitted, derived from topic."
15006
+ ),
15007
+ cta_url: z28.string().url().optional().describe(
15008
+ "URL promoted on the CTA slide. Defaults to the project landing page."
15009
+ ),
13956
15010
  tone: z28.string().max(200).optional().describe(
13957
15011
  'Voice/tone override. Composes with the brand profile voice when present. Example: "educational, confident, not arrogant".'
13958
15012
  ),
13959
15013
  constraints: z28.string().max(500).optional().describe(
13960
15014
  'Content constraints applied at generation time. Example: "No fabricated statistics. Sentence case only. No ALL CAPS."'
13961
15015
  ),
13962
- platform: z28.enum(["linkedin", "instagram", "tiktok", "x"]).optional().describe("Target platform. Affects tone conventions and slide-count guardrails."),
15016
+ platform: z28.enum(["linkedin", "instagram", "tiktok", "x"]).optional().describe(
15017
+ "Target platform. Affects tone conventions and slide-count guardrails."
15018
+ ),
13963
15019
  brand_id: z28.string().optional().describe(
13964
15020
  "Brand/project ID to pull visual context from (colors, logo, mood). Falls back to project_id, then default project."
13965
15021
  ),
@@ -13991,7 +15047,9 @@ function registerCarouselTools(server2) {
13991
15047
  const slideCount = slide_count ?? 7;
13992
15048
  const ratio = aspect_ratio ?? "1:1";
13993
15049
  let brandContext = null;
13994
- const brandProjectId = brand_id || project_id || await getDefaultProjectId();
15050
+ const defaultProjectId = await getDefaultProjectId();
15051
+ const brandProjectId = brand_id || project_id || defaultProjectId;
15052
+ const resolvedProjectId = project_id || defaultProjectId;
13995
15053
  if (brandProjectId) {
13996
15054
  brandContext = await fetchBrandVisualContext(brandProjectId);
13997
15055
  }
@@ -14000,10 +15058,7 @@ function registerCarouselTools(server2) {
14000
15058
  const totalEstimatedCost = carouselTextCost + slideCount * perImageCost;
14001
15059
  const budgetCheck = checkCreditBudget(totalEstimatedCost);
14002
15060
  if (!budgetCheck.ok) {
14003
- return {
14004
- content: [{ type: "text", text: budgetCheck.message }],
14005
- isError: true
14006
- };
15061
+ return budgetCheck.error;
14007
15062
  }
14008
15063
  const assetBudget = checkAssetBudget(slideCount);
14009
15064
  if (!assetBudget.ok) {
@@ -14013,7 +15068,10 @@ function registerCarouselTools(server2) {
14013
15068
  };
14014
15069
  }
14015
15070
  const userId = await getDefaultUserId();
14016
- const rateLimit = checkRateLimit("generation", `create_carousel:${userId}`);
15071
+ const rateLimit = checkRateLimit(
15072
+ "generation",
15073
+ `create_carousel:${userId}`
15074
+ );
14017
15075
  if (!rateLimit.allowed) {
14018
15076
  return {
14019
15077
  content: [
@@ -14033,7 +15091,7 @@ function registerCarouselTools(server2) {
14033
15091
  slideCount,
14034
15092
  aspectRatio: ratio,
14035
15093
  style: resolvedStyle,
14036
- projectId: project_id,
15094
+ projectId: resolvedProjectId,
14037
15095
  hook,
14038
15096
  hookFamily: hook_family,
14039
15097
  ctaText: cta_text,
@@ -14047,7 +15105,12 @@ function registerCarouselTools(server2) {
14047
15105
  if (carouselError || !carouselData?.carousel) {
14048
15106
  const errMsg = carouselError ?? "No carousel data returned";
14049
15107
  return {
14050
- content: [{ type: "text", text: `Carousel text generation failed: ${errMsg}` }],
15108
+ content: [
15109
+ {
15110
+ type: "text",
15111
+ text: `Carousel text generation failed: ${errMsg}`
15112
+ }
15113
+ ],
14051
15114
  isError: true
14052
15115
  };
14053
15116
  }
@@ -14076,7 +15139,8 @@ function registerCarouselTools(server2) {
14076
15139
  {
14077
15140
  prompt: imagePrompt,
14078
15141
  model: image_model,
14079
- aspectRatio: ratio
15142
+ aspectRatio: ratio,
15143
+ ...resolvedProjectId && { projectId: resolvedProjectId }
14080
15144
  },
14081
15145
  { timeoutMs: 3e4 }
14082
15146
  );
@@ -14133,14 +15197,19 @@ function registerCarouselTools(server2) {
14133
15197
  type: "text",
14134
15198
  text: JSON.stringify(
14135
15199
  {
14136
- _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
15200
+ _meta: {
15201
+ version: MCP_VERSION,
15202
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
15203
+ },
14137
15204
  data: {
14138
15205
  carouselId: carousel.id,
14139
15206
  templateId,
14140
15207
  style: resolvedStyle,
14141
15208
  slideCount: carousel.slides.length,
14142
15209
  slides: carousel.slides.map((s) => {
14143
- const job = imageJobs.find((j) => j.slideNumber === s.slideNumber);
15210
+ const job = imageJobs.find(
15211
+ (j) => j.slideNumber === s.slideNumber
15212
+ );
14144
15213
  return {
14145
15214
  ...s,
14146
15215
  imageJobId: job?.jobId ?? null,
@@ -14167,9 +15236,17 @@ function registerCarouselTools(server2) {
14167
15236
  credits: {
14168
15237
  textGeneration: textCredits,
14169
15238
  imagesEstimated: successfulJobs.length * perImageCost,
14170
- imagesCharged: imageJobs.reduce((sum, job) => sum + (job.creditsCharged ?? 0), 0),
14171
- imagesRefunded: imageJobs.reduce((sum, job) => sum + (job.creditsRefunded ?? 0), 0),
14172
- billingUnknownSlides: imageJobs.filter((job) => job.billingStatus === "unknown").length,
15239
+ imagesCharged: imageJobs.reduce(
15240
+ (sum, job) => sum + (job.creditsCharged ?? 0),
15241
+ 0
15242
+ ),
15243
+ imagesRefunded: imageJobs.reduce(
15244
+ (sum, job) => sum + (job.creditsRefunded ?? 0),
15245
+ 0
15246
+ ),
15247
+ billingUnknownSlides: imageJobs.filter(
15248
+ (job) => job.billingStatus === "unknown"
15249
+ ).length,
14173
15250
  totalEstimated: textCredits + successfulJobs.length * perImageCost
14174
15251
  }
14175
15252
  }
@@ -14197,7 +15274,9 @@ function registerCarouselTools(server2) {
14197
15274
  for (const slide of carousel.slides) {
14198
15275
  const job = imageJobs.find((j) => j.slideNumber === slide.slideNumber);
14199
15276
  const status = job?.jobId ? `image: ${job.jobId}` : `image FAILED: ${job?.error}`;
14200
- lines.push(` ${slide.slideNumber}. ${slide.headline || "(no headline)"} [${status}]`);
15277
+ lines.push(
15278
+ ` ${slide.slideNumber}. ${slide.headline || "(no headline)"} [${status}]`
15279
+ );
14201
15280
  }
14202
15281
  if (failedJobs.length > 0) {
14203
15282
  lines.push("");
@@ -14208,7 +15287,9 @@ function registerCarouselTools(server2) {
14208
15287
  const jobIdList = successfulJobs.map((j) => j.jobId).join(", ");
14209
15288
  lines.push("");
14210
15289
  lines.push("Next steps:");
14211
- lines.push(` 1. Poll each job: check_status with job_id for each of: ${jobIdList}`);
15290
+ lines.push(
15291
+ ` 1. Poll each job: check_status with job_id for each of: ${jobIdList}`
15292
+ );
14212
15293
  lines.push(
14213
15294
  " 2. When all complete: schedule_post with job_ids=[...] and media_type=CAROUSEL_ALBUM"
14214
15295
  );
@@ -15078,28 +16159,33 @@ var init_analytics_pulse = __esm({
15078
16159
 
15079
16160
  // src/tools/connections.ts
15080
16161
  import { z as z33 } from "zod";
15081
- function findActiveAccount(accounts, platform3) {
16162
+ function findActiveAccounts(accounts, platform3, projectId) {
15082
16163
  const target = platform3.toLowerCase();
15083
- return accounts.find((a) => {
16164
+ return accounts.filter((a) => {
15084
16165
  const effectiveStatus = a.effective_status || a.status;
15085
- return a.platform.toLowerCase() === target && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
15086
- }) ?? null;
16166
+ return a.platform.toLowerCase() === target && a.project_id === projectId && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
16167
+ });
15087
16168
  }
15088
16169
  function registerConnectionTools(server2) {
15089
16170
  server2.tool(
15090
16171
  "start_platform_connection",
15091
16172
  "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
16173
  {
15093
- platform: z33.enum(PLATFORM_ENUM4).describe("Platform to connect. Lower-case: instagram, tiktok, youtube, etc."),
15094
- project_id: z33.string().optional().describe(
15095
- "Brand/project ID to bind the new social account to. Use this when connecting a brand-specific account."
16174
+ platform: z33.enum(PLATFORM_ENUM4).describe(
16175
+ "Platform to connect. Lower-case: instagram, tiktok, youtube, etc."
16176
+ ),
16177
+ project_id: z33.string().uuid().optional().describe(
16178
+ "Brand/project ID to bind the new social account to. Required when the account has multiple brands."
15096
16179
  ),
15097
16180
  response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
15098
16181
  },
15099
16182
  async ({ platform: platform3, project_id, response_format }) => {
15100
16183
  const format = response_format ?? "text";
15101
16184
  const userId = await getDefaultUserId();
15102
- const rl = checkRateLimit("posting", `start_platform_connection:${userId}`);
16185
+ const rl = checkRateLimit(
16186
+ "posting",
16187
+ `start_platform_connection:${userId}`
16188
+ );
15103
16189
  if (!rl.allowed) {
15104
16190
  return {
15105
16191
  content: [
@@ -15111,12 +16197,27 @@ function registerConnectionTools(server2) {
15111
16197
  isError: true
15112
16198
  };
15113
16199
  }
16200
+ const projectResolution = await resolveProjectStrict(project_id);
16201
+ if (!projectResolution.projectId) {
16202
+ return {
16203
+ content: [
16204
+ {
16205
+ type: "text",
16206
+ 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."
16207
+ }
16208
+ ],
16209
+ isError: true
16210
+ };
16211
+ }
16212
+ const resolvedProjectId = projectResolution.projectId;
16213
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
15114
16214
  const { data, error } = await callEdgeFunction(
15115
16215
  "mcp-data",
15116
16216
  {
15117
16217
  action: "mint-connection-nonce",
15118
16218
  platform: platform3,
15119
- ...project_id ? { projectId: project_id, project_id } : {}
16219
+ projectId: resolvedProjectId,
16220
+ project_id: resolvedProjectId
15120
16221
  },
15121
16222
  { timeoutMs: 1e4 }
15122
16223
  );
@@ -15132,6 +16233,17 @@ function registerConnectionTools(server2) {
15132
16233
  isError: true
15133
16234
  };
15134
16235
  }
16236
+ if (data.project_id !== resolvedProjectId) {
16237
+ return {
16238
+ content: [
16239
+ {
16240
+ type: "text",
16241
+ text: "Connection link project attestation failed. No OAuth link was returned."
16242
+ }
16243
+ ],
16244
+ isError: true
16245
+ };
16246
+ }
15135
16247
  if (format === "json") {
15136
16248
  return {
15137
16249
  content: [
@@ -15140,10 +16252,11 @@ function registerConnectionTools(server2) {
15140
16252
  text: JSON.stringify(
15141
16253
  {
15142
16254
  platform: data.platform,
15143
- project_id: project_id ?? null,
16255
+ project_id: resolvedProjectId,
15144
16256
  deep_link: data.deep_link,
15145
16257
  expires_at: data.expires_at,
15146
- next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection."
16258
+ next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection.",
16259
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
15147
16260
  },
15148
16261
  null,
15149
16262
  2
@@ -15159,7 +16272,7 @@ function registerConnectionTools(server2) {
15159
16272
  type: "text",
15160
16273
  text: [
15161
16274
  `${data.platform} connection ready.`,
15162
- ...project_id ? [`Brand/project: ${project_id}`] : [],
16275
+ `Brand/project: ${resolvedProjectId}`,
15163
16276
  "",
15164
16277
  'Ask the user to open this link in a browser and click "Connect" on the platform:',
15165
16278
  ` ${data.deep_link}`,
@@ -15167,7 +16280,8 @@ function registerConnectionTools(server2) {
15167
16280
  `Link expires at: ${data.expires_at} (~2 minutes).`,
15168
16281
  "",
15169
16282
  "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."
16283
+ "This is a one-time browser setup \u2014 not another OAuth flow inside Claude.",
16284
+ ...projectAutoResolvedNote ? ["", `Note: ${projectAutoResolvedNote}`] : []
15171
16285
  ].join("\n")
15172
16286
  }
15173
16287
  ],
@@ -15180,14 +16294,20 @@ function registerConnectionTools(server2) {
15180
16294
  "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
16295
  {
15182
16296
  platform: z33.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
15183
- project_id: z33.string().optional().describe(
16297
+ project_id: z33.string().uuid().optional().describe(
15184
16298
  "Brand/project ID to scope the connection poll. Use the same project_id passed to start_platform_connection."
15185
16299
  ),
15186
16300
  timeout_s: z33.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
15187
16301
  poll_interval_s: z33.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
15188
16302
  response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
15189
16303
  },
15190
- async ({ platform: platform3, project_id, timeout_s, poll_interval_s, response_format }) => {
16304
+ async ({
16305
+ platform: platform3,
16306
+ project_id,
16307
+ timeout_s,
16308
+ poll_interval_s,
16309
+ response_format
16310
+ }) => {
15191
16311
  const format = response_format ?? "text";
15192
16312
  const startedAt = Date.now();
15193
16313
  const timeoutMs = (timeout_s ?? 120) * 1e3;
@@ -15206,6 +16326,18 @@ function registerConnectionTools(server2) {
15206
16326
  isError: true
15207
16327
  };
15208
16328
  }
16329
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
16330
+ if (!resolvedProjectId) {
16331
+ return {
16332
+ content: [
16333
+ {
16334
+ type: "text",
16335
+ text: "A project_id is required to wait for a connection. Use the same project_id used to start the OAuth flow."
16336
+ }
16337
+ ],
16338
+ isError: true
16339
+ };
16340
+ }
15209
16341
  const activeWaits = inFlightWaitsByUser.get(userId) ?? 0;
15210
16342
  if (activeWaits >= MAX_CONCURRENT_WAITS_PER_USER) {
15211
16343
  return {
@@ -15227,12 +16359,62 @@ function registerConnectionTools(server2) {
15227
16359
  "mcp-data",
15228
16360
  {
15229
16361
  action: "connected-accounts",
15230
- ...project_id ? { projectId: project_id, project_id } : {}
16362
+ projectId: resolvedProjectId,
16363
+ project_id: resolvedProjectId
15231
16364
  },
15232
16365
  { timeoutMs: 1e4 }
15233
16366
  );
15234
16367
  if (!error && data?.success) {
15235
- const found = findActiveAccount(data.accounts ?? [], platform3);
16368
+ const foundAccounts = findActiveAccounts(
16369
+ data.accounts ?? [],
16370
+ platform3,
16371
+ resolvedProjectId
16372
+ );
16373
+ if (foundAccounts.length > 1) {
16374
+ if (format === "json") {
16375
+ return {
16376
+ content: [
16377
+ {
16378
+ type: "text",
16379
+ text: JSON.stringify(
16380
+ {
16381
+ connected: true,
16382
+ platform: platform3,
16383
+ project_id: resolvedProjectId,
16384
+ accounts: foundAccounts.map((a) => ({
16385
+ id: a.id,
16386
+ username: a.username,
16387
+ connected_at: a.created_at
16388
+ })),
16389
+ attempts,
16390
+ message: `${platform3} has ${foundAccounts.length} active accounts for this project. Pass the exact account_id to schedule_post.`
16391
+ },
16392
+ null,
16393
+ 2
16394
+ )
16395
+ }
16396
+ ],
16397
+ isError: false
16398
+ };
16399
+ }
16400
+ return {
16401
+ content: [
16402
+ {
16403
+ type: "text",
16404
+ text: [
16405
+ `${platform3} is connected \u2014 ${foundAccounts.length} active accounts found for project ${resolvedProjectId}:`,
16406
+ ...foundAccounts.map(
16407
+ (a) => ` ${a.username || "(unnamed)"} (id=${a.id})`
16408
+ ),
16409
+ "",
16410
+ "Call schedule_post with the exact account_id (or account_ids) for the one you mean."
16411
+ ].join("\n")
16412
+ }
16413
+ ],
16414
+ isError: false
16415
+ };
16416
+ }
16417
+ const found = foundAccounts[0];
15236
16418
  if (found) {
15237
16419
  if (format === "json") {
15238
16420
  return {
@@ -15243,7 +16425,7 @@ function registerConnectionTools(server2) {
15243
16425
  {
15244
16426
  connected: true,
15245
16427
  platform: found.platform,
15246
- project_id: found.project_id ?? project_id ?? null,
16428
+ project_id: resolvedProjectId,
15247
16429
  account_id: found.id,
15248
16430
  username: found.username,
15249
16431
  connected_at: found.created_at,
@@ -15263,7 +16445,7 @@ function registerConnectionTools(server2) {
15263
16445
  type: "text",
15264
16446
  text: [
15265
16447
  `${found.platform} is connected.`,
15266
- ...found.project_id || project_id ? [`Brand/project: ${found.project_id ?? project_id}`] : [],
16448
+ `Brand/project: ${resolvedProjectId}`,
15267
16449
  `Account: ${found.username || "(unnamed)"} (id=${found.id})`,
15268
16450
  `Detected after ${attempts} poll(s) in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s.`,
15269
16451
  "Ready to call `schedule_post`."
@@ -15276,7 +16458,9 @@ function registerConnectionTools(server2) {
15276
16458
  }
15277
16459
  const remaining = deadline - Date.now();
15278
16460
  if (remaining <= 0) break;
15279
- await new Promise((resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining)));
16461
+ await new Promise(
16462
+ (resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining))
16463
+ );
15280
16464
  }
15281
16465
  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
16466
  if (format === "json") {
@@ -15288,7 +16472,7 @@ function registerConnectionTools(server2) {
15288
16472
  {
15289
16473
  connected: false,
15290
16474
  platform: platform3,
15291
- project_id: project_id ?? null,
16475
+ project_id: resolvedProjectId,
15292
16476
  attempts,
15293
16477
  timed_out: true,
15294
16478
  message