@socialneuron/mcp-server 1.8.2 → 1.9.0

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