@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/sn.js CHANGED
@@ -19,7 +19,7 @@ var MCP_VERSION;
19
19
  var init_version = __esm({
20
20
  "src/lib/version.ts"() {
21
21
  "use strict";
22
- MCP_VERSION = "1.8.2";
22
+ MCP_VERSION = "1.9.0";
23
23
  }
24
24
  });
25
25
 
@@ -56,6 +56,209 @@ var init_request_context = __esm({
56
56
  }
57
57
  });
58
58
 
59
+ // src/lib/edge-function.ts
60
+ var edge_function_exports = {};
61
+ __export(edge_function_exports, {
62
+ callEdgeFunction: () => callEdgeFunction
63
+ });
64
+ function safeGatewayError(responseText, status) {
65
+ try {
66
+ const parsed = JSON.parse(responseText);
67
+ const nested = parsed.error && typeof parsed.error === "object" ? parsed.error : null;
68
+ const candidates = [
69
+ nested?.error_type,
70
+ nested?.code,
71
+ parsed.error_type,
72
+ parsed.error_code,
73
+ parsed.code,
74
+ typeof parsed.error === "string" ? parsed.error : null
75
+ ];
76
+ for (const value of candidates) {
77
+ if (typeof value !== "string") continue;
78
+ const normalized = value.trim().toLowerCase();
79
+ if (SAFE_GATEWAY_ERROR_CODES.has(normalized)) return normalized;
80
+ const embedded = [...SAFE_GATEWAY_ERROR_CODES].find((code) => normalized.includes(code));
81
+ if (embedded) return embedded;
82
+ }
83
+ } catch {
84
+ }
85
+ return `Backend request failed (HTTP ${status}).`;
86
+ }
87
+ function safeFailureData(responseText) {
88
+ try {
89
+ const parsed = JSON.parse(responseText);
90
+ const metric = (name) => {
91
+ const value = parsed[name];
92
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
93
+ };
94
+ const billingStatus = typeof parsed.billing_status === "string" && SAFE_BILLING_STATUSES.has(parsed.billing_status) ? parsed.billing_status : void 0;
95
+ const failureReason = parsed.failure_reason === "generation_failed" || parsed.failure_reason === "authentication_failed" || parsed.failure_reason === "cancelled_by_user" ? parsed.failure_reason : void 0;
96
+ const jobStatus = typeof parsed.status === "string" && SAFE_JOB_STATUSES.has(parsed.status) ? parsed.status : void 0;
97
+ const safe = {
98
+ ...jobStatus ? { status: jobStatus } : {},
99
+ ...metric("credits_reserved") !== void 0 ? { credits_reserved: metric("credits_reserved") } : {},
100
+ ...metric("credits_charged") !== void 0 ? { credits_charged: metric("credits_charged") } : {},
101
+ ...metric("credits_refunded") !== void 0 ? { credits_refunded: metric("credits_refunded") } : {},
102
+ ...billingStatus ? { billing_status: billingStatus } : {},
103
+ ...failureReason ? { failure_reason: failureReason } : {}
104
+ };
105
+ return Object.keys(safe).length > 0 ? safe : null;
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+ function getApiKeyOrNull() {
111
+ const envKey = process.env.SOCIALNEURON_API_KEY;
112
+ if (envKey && envKey.trim().length) return envKey.trim();
113
+ const requestToken = getRequestToken();
114
+ if (requestToken) return requestToken;
115
+ return getAuthenticatedApiKey();
116
+ }
117
+ async function callEdgeFunction(functionName, body, options) {
118
+ const supabaseUrl = getSupabaseUrl();
119
+ const apiKey = getApiKeyOrNull();
120
+ const controller = new AbortController();
121
+ const timeoutMs = options?.timeoutMs ?? 6e4;
122
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
123
+ const enrichedBody = { ...body };
124
+ if (!enrichedBody.userId && !enrichedBody.user_id) {
125
+ try {
126
+ const defaultId = await getDefaultUserId();
127
+ enrichedBody.userId = defaultId;
128
+ enrichedBody.user_id = defaultId;
129
+ } catch {
130
+ }
131
+ } else {
132
+ if (enrichedBody.userId && !enrichedBody.user_id) enrichedBody.user_id = enrichedBody.userId;
133
+ if (enrichedBody.user_id && !enrichedBody.userId) enrichedBody.userId = enrichedBody.user_id;
134
+ }
135
+ if (!enrichedBody.projectId && !enrichedBody.project_id) {
136
+ try {
137
+ const { getDefaultProjectId: getDefaultProjectId2 } = await Promise.resolve().then(() => (init_supabase(), supabase_exports));
138
+ const defaultProjectId = await getDefaultProjectId2();
139
+ if (defaultProjectId) {
140
+ enrichedBody.projectId = defaultProjectId;
141
+ enrichedBody.project_id = defaultProjectId;
142
+ }
143
+ } catch {
144
+ }
145
+ }
146
+ let url;
147
+ let method = options?.method ?? "POST";
148
+ let headers;
149
+ let requestBody;
150
+ if (!apiKey) {
151
+ clearTimeout(timer);
152
+ return {
153
+ data: null,
154
+ error: "Not authenticated. Run: npx @socialneuron/mcp-server login \u2014 Requires a paid plan (Starter+). See https://socialneuron.com/pricing"
155
+ };
156
+ }
157
+ url = new URL(`${supabaseUrl}/functions/v1/mcp-gateway`);
158
+ headers = {
159
+ Authorization: `Bearer ${apiKey}`,
160
+ "Content-Type": "application/json"
161
+ };
162
+ requestBody = {
163
+ functionName,
164
+ body: enrichedBody,
165
+ query: options?.query,
166
+ method: method.toUpperCase(),
167
+ timeoutMs
168
+ };
169
+ method = "POST";
170
+ try {
171
+ const response = await fetch(url.toString(), {
172
+ method,
173
+ headers,
174
+ body: method === "GET" ? void 0 : JSON.stringify(requestBody),
175
+ signal: controller.signal
176
+ });
177
+ clearTimeout(timer);
178
+ const responseText = await response.text();
179
+ if (!response.ok) {
180
+ const errorCode = safeGatewayError(responseText, response.status);
181
+ const failureData = safeFailureData(responseText);
182
+ if (response.status === 401) {
183
+ return {
184
+ data: failureData,
185
+ error: `Authentication failed (HTTP 401). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
186
+ };
187
+ }
188
+ if (response.status === 403) {
189
+ return {
190
+ data: failureData,
191
+ error: `Forbidden (HTTP 403): ${errorCode}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
192
+ };
193
+ }
194
+ if (response.status === 429) {
195
+ const retryAfter = response.headers.get("retry-after") || "60";
196
+ return {
197
+ data: failureData,
198
+ error: `Rate limit exceeded (HTTP 429). Wait ${retryAfter}s before retrying. Reduce request frequency or upgrade your plan.`
199
+ };
200
+ }
201
+ return { data: failureData, error: errorCode };
202
+ }
203
+ try {
204
+ const data = JSON.parse(responseText);
205
+ return { data, error: null };
206
+ } catch {
207
+ return { data: { text: responseText }, error: null };
208
+ }
209
+ } catch (err) {
210
+ clearTimeout(timer);
211
+ if (err instanceof Error && err.name === "AbortError") {
212
+ return {
213
+ data: null,
214
+ error: `Edge Function '${functionName}' timed out after ${timeoutMs}ms`
215
+ };
216
+ }
217
+ return { data: null, error: "Network request failed. Please retry." };
218
+ }
219
+ }
220
+ var SAFE_GATEWAY_ERROR_CODES, SAFE_BILLING_STATUSES, SAFE_JOB_STATUSES;
221
+ var init_edge_function = __esm({
222
+ "src/lib/edge-function.ts"() {
223
+ "use strict";
224
+ init_supabase();
225
+ init_request_context();
226
+ SAFE_GATEWAY_ERROR_CODES = /* @__PURE__ */ new Set([
227
+ "daily_limit_reached",
228
+ "insufficient_credits",
229
+ "project_scope_mismatch",
230
+ "schedule_conflict",
231
+ "post_not_found",
232
+ "post_not_reschedulable",
233
+ "post_in_progress",
234
+ "not_cancellable",
235
+ "publishing_in_progress",
236
+ "plan_upgrade_required",
237
+ "rate_limited",
238
+ "validation_error",
239
+ "permission_denied",
240
+ "not_found"
241
+ ]);
242
+ SAFE_BILLING_STATUSES = /* @__PURE__ */ new Set([
243
+ "reserved",
244
+ "charged",
245
+ "refunded",
246
+ "failed_no_charge",
247
+ "refund_pending",
248
+ "not_charged"
249
+ ]);
250
+ SAFE_JOB_STATUSES = /* @__PURE__ */ new Set([
251
+ "queued",
252
+ "pending",
253
+ "processing",
254
+ "completed",
255
+ "failed",
256
+ "cancelled",
257
+ "canceled"
258
+ ]);
259
+ }
260
+ });
261
+
59
262
  // src/cli/credentials.ts
60
263
  var credentials_exports = {};
61
264
  __export(credentials_exports, {
@@ -82,6 +285,10 @@ import {
82
285
  } from "node:fs";
83
286
  import { homedir, platform } from "node:os";
84
287
  import { join } from "node:path";
288
+ function loadNativeKeyring() {
289
+ nativeKeyringModule ??= import("@napi-rs/keyring").catch(() => null);
290
+ return nativeKeyringModule;
291
+ }
85
292
  function assertSafeCredentialPaths() {
86
293
  if (platform() === "win32") return;
87
294
  const uid = process.getuid?.();
@@ -201,49 +408,90 @@ function writeCredentialsFile(data) {
201
408
  }
202
409
  hardenCredentialPermissions();
203
410
  }
204
- function macKeychainRead(service) {
411
+ function isMacKeychainItemMissing(error) {
412
+ const commandError = error;
413
+ if (commandError?.status === 44) return true;
414
+ const detail = `${commandError?.stderr ?? ""}
415
+ ${commandError?.message ?? ""}`;
416
+ return /\berrsecitemnotfound\b/i.test(detail) || /(?:^|:\s*)the specified item could not be found in the keychain\.\s*$/i.test(detail.trim());
417
+ }
418
+ async function inspectMacKeychain(service) {
419
+ const native = await loadNativeKeyring();
420
+ if (native) {
421
+ try {
422
+ const value = new native.Entry(service, KEYCHAIN_ACCOUNT).getPassword();
423
+ if (value) return { status: "found", value };
424
+ } catch {
425
+ }
426
+ }
205
427
  try {
206
428
  const result = execFileSync(
207
429
  "security",
208
430
  ["find-generic-password", "-a", KEYCHAIN_ACCOUNT, "-s", service, "-w"],
209
431
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
210
432
  );
211
- return result.trim() || null;
212
- } catch {
213
- return null;
433
+ const value = result.trim();
434
+ return value ? { status: "found", value } : { status: "missing" };
435
+ } catch (error) {
436
+ return isMacKeychainItemMissing(error) ? { status: "missing" } : { status: "unavailable" };
214
437
  }
215
438
  }
216
- function macKeychainWrite(service, value) {
439
+ async function macKeychainRead(service) {
440
+ const result = await inspectMacKeychain(service);
441
+ return result.status === "found" ? result.value : null;
442
+ }
443
+ async function macKeychainWrite(service, value) {
444
+ const native = await loadNativeKeyring();
445
+ if (!native) return false;
217
446
  try {
218
- execFileSync(
219
- "security",
220
- [
221
- "add-generic-password",
222
- "-a",
223
- KEYCHAIN_ACCOUNT,
224
- "-s",
225
- service,
226
- "-w",
227
- value,
228
- "-U"
229
- // update if exists
230
- ],
231
- { stdio: ["pipe", "pipe", "pipe"] }
232
- );
447
+ new native.Entry(service, KEYCHAIN_ACCOUNT).setPassword(value);
233
448
  return true;
234
449
  } catch {
235
450
  return false;
236
451
  }
237
452
  }
238
- function macKeychainDelete(service) {
453
+ async function macKeychainDelete(service) {
454
+ const native = await loadNativeKeyring();
455
+ let nativeDeleted = false;
456
+ if (native) {
457
+ try {
458
+ nativeDeleted = new native.Entry(service, KEYCHAIN_ACCOUNT).deletePassword();
459
+ } catch {
460
+ }
461
+ }
239
462
  try {
240
463
  execFileSync("security", ["delete-generic-password", "-a", KEYCHAIN_ACCOUNT, "-s", service], {
241
464
  stdio: ["pipe", "pipe", "pipe"]
242
465
  });
243
- return true;
244
- } catch {
245
- return false;
466
+ return "deleted";
467
+ } catch (error) {
468
+ if (isMacKeychainItemMissing(error)) return nativeDeleted ? "deleted" : "missing";
469
+ return "unavailable";
470
+ }
471
+ }
472
+ async function clearMacKeychain(service) {
473
+ for (let attempt = 0; attempt < 4; attempt += 1) {
474
+ const deleted = await macKeychainDelete(service);
475
+ if (deleted === "unavailable") break;
476
+ const remaining = await inspectMacKeychain(service);
477
+ if (remaining.status === "missing") return;
478
+ if (remaining.status === "unavailable") break;
479
+ }
480
+ throw new Error(
481
+ "Unable to verify removal of the existing Social Neuron Keychain credential. Unlock Keychain Access, remove the item, and retry."
482
+ );
483
+ }
484
+ async function verifyMacFileFallback(service) {
485
+ const existing = await inspectMacKeychain(service);
486
+ if (existing.status === "missing") return;
487
+ if (existing.status === "found") {
488
+ throw new Error(
489
+ "An existing Social Neuron Keychain credential could not be replaced. The existing value was retained; unlock or remove it in Keychain Access and retry."
490
+ );
246
491
  }
492
+ throw new Error(
493
+ "Unable to verify absence of an existing Social Neuron Keychain credential. Unlock Keychain Access and retry."
494
+ );
247
495
  }
248
496
  function linuxSecretRead(key) {
249
497
  try {
@@ -284,7 +532,7 @@ async function loadApiKey() {
284
532
  if (envKey) return envKey;
285
533
  const os = platform();
286
534
  if (os === "darwin") {
287
- const key = macKeychainRead(KEYCHAIN_SERVICE_API);
535
+ const key = await macKeychainRead(KEYCHAIN_SERVICE_API);
288
536
  if (key) return key;
289
537
  } else if (os === "linux") {
290
538
  const key = linuxSecretRead("api-key");
@@ -301,13 +549,18 @@ async function loadApiKey() {
301
549
  async function saveApiKey(key) {
302
550
  const os = platform();
303
551
  let saved = false;
552
+ let fallbackCredentials;
304
553
  if (os === "darwin") {
305
- saved = macKeychainWrite(KEYCHAIN_SERVICE_API, key);
554
+ saved = await macKeychainWrite(KEYCHAIN_SERVICE_API, key);
555
+ if (!saved) {
556
+ fallbackCredentials = readCredentialsFile();
557
+ await verifyMacFileFallback(KEYCHAIN_SERVICE_API);
558
+ }
306
559
  } else if (os === "linux") {
307
560
  saved = linuxSecretWrite("api-key", key);
308
561
  }
309
562
  if (!saved) {
310
- const creds = readCredentialsFile();
563
+ const creds = fallbackCredentials ?? readCredentialsFile();
311
564
  creds.apiKey = key;
312
565
  writeCredentialsFile(creds);
313
566
  if (os === "win32") {
@@ -330,8 +583,13 @@ async function saveApiKey(key) {
330
583
  }
331
584
  async function deleteApiKey() {
332
585
  const os = platform();
586
+ let keychainError;
333
587
  if (os === "darwin") {
334
- macKeychainDelete(KEYCHAIN_SERVICE_API);
588
+ try {
589
+ await clearMacKeychain(KEYCHAIN_SERVICE_API);
590
+ } catch (error) {
591
+ keychainError = error;
592
+ }
335
593
  } else if (os === "linux") {
336
594
  linuxSecretDelete("api-key");
337
595
  }
@@ -347,13 +605,14 @@ async function deleteApiKey() {
347
605
  writeCredentialsFile(creds);
348
606
  }
349
607
  }
608
+ if (keychainError) throw keychainError;
350
609
  }
351
610
  async function loadSupabaseUrl() {
352
611
  const envUrl = process.env.SOCIALNEURON_SUPABASE_URL || process.env.SUPABASE_URL;
353
612
  if (envUrl) return envUrl;
354
613
  const os = platform();
355
614
  if (os === "darwin") {
356
- const url = macKeychainRead(KEYCHAIN_SERVICE_URL);
615
+ const url = await macKeychainRead(KEYCHAIN_SERVICE_URL);
357
616
  if (url) return url;
358
617
  } else if (os === "linux") {
359
618
  const url = linuxSecretRead("supabase-url");
@@ -365,18 +624,23 @@ async function loadSupabaseUrl() {
365
624
  async function saveSupabaseUrl(url) {
366
625
  const os = platform();
367
626
  let saved = false;
627
+ let fallbackCredentials;
368
628
  if (os === "darwin") {
369
- saved = macKeychainWrite(KEYCHAIN_SERVICE_URL, url);
629
+ saved = await macKeychainWrite(KEYCHAIN_SERVICE_URL, url);
630
+ if (!saved) {
631
+ fallbackCredentials = readCredentialsFile();
632
+ await verifyMacFileFallback(KEYCHAIN_SERVICE_URL);
633
+ }
370
634
  } else if (os === "linux") {
371
635
  saved = linuxSecretWrite("supabase-url", url);
372
636
  }
373
637
  if (!saved) {
374
- const creds = readCredentialsFile();
638
+ const creds = fallbackCredentials ?? readCredentialsFile();
375
639
  creds.supabaseUrl = url;
376
640
  writeCredentialsFile(creds);
377
641
  }
378
642
  }
379
- var KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE_API, KEYCHAIN_SERVICE_URL, CONFIG_DIR, CREDENTIALS_FILE;
643
+ var KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE_API, KEYCHAIN_SERVICE_URL, CONFIG_DIR, CREDENTIALS_FILE, nativeKeyringModule;
380
644
  var init_credentials = __esm({
381
645
  "src/cli/credentials.ts"() {
382
646
  "use strict";
@@ -529,7 +793,10 @@ __export(supabase_exports, {
529
793
  getSupabaseUrl: () => getSupabaseUrl,
530
794
  initializeAuth: () => initializeAuth,
531
795
  isTelemetryDisabled: () => isTelemetryDisabled,
532
- logMcpToolInvocation: () => logMcpToolInvocation
796
+ listAccessibleProjectsWithAccountStatus: () => listAccessibleProjectsWithAccountStatus,
797
+ logMcpToolInvocation: () => logMcpToolInvocation,
798
+ resolveProjectForConnectedAccountTool: () => resolveProjectForConnectedAccountTool,
799
+ resolveProjectStrict: () => resolveProjectStrict
533
800
  });
534
801
  import { createClient } from "@supabase/supabase-js";
535
802
  import { randomUUID } from "node:crypto";
@@ -568,20 +835,17 @@ async function getDefaultUserId() {
568
835
  if (authenticatedUserId) return authenticatedUserId;
569
836
  const envUserId = process.env.SOCIALNEURON_USER_ID;
570
837
  if (envUserId) return envUserId;
571
- throw new Error("No user ID available. Set SOCIALNEURON_USER_ID or authenticate via API key.");
838
+ throw new Error(
839
+ "No user ID available. Set SOCIALNEURON_USER_ID or authenticate via API key."
840
+ );
572
841
  }
573
842
  async function getDefaultProjectId() {
574
843
  const requestProjectId = getRequestProjectId();
575
844
  if (requestProjectId) return requestProjectId;
576
845
  if (authenticatedProjectId) return authenticatedProjectId;
577
846
  const userId = await getDefaultUserId().catch(() => null);
578
- if (userId) {
579
- const cached = projectIdCache.get(userId);
580
- if (cached) return cached;
581
- }
582
847
  const envProjectId = process.env.SOCIALNEURON_PROJECT_ID;
583
848
  if (envProjectId) {
584
- if (userId) projectIdCache.set(userId, envProjectId);
585
849
  return envProjectId;
586
850
  }
587
851
  if (!userId) return null;
@@ -590,15 +854,126 @@ async function getDefaultProjectId() {
590
854
  const { data: memberships } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId);
591
855
  const orgIds = (memberships ?? []).map((m) => m.organization_id);
592
856
  if (orgIds.length === 0) return null;
593
- const { data } = await supabase.from("projects").select("id").in("organization_id", orgIds).order("created_at", { ascending: false }).limit(1).maybeSingle();
594
- if (data?.id) {
595
- projectIdCache.set(userId, data.id);
596
- return data.id;
597
- }
857
+ const { data } = await supabase.from("projects").select("id").in("organization_id", orgIds).order("created_at", { ascending: false }).limit(2);
858
+ if (data?.length === 1) return data[0].id;
598
859
  } catch {
599
860
  }
600
861
  return null;
601
862
  }
863
+ function normalizePlatformFilter(platform3) {
864
+ if (!platform3) return null;
865
+ const values = Array.isArray(platform3) ? platform3 : [platform3];
866
+ const set = new Set(values.filter(Boolean).map((p) => p.toLowerCase()));
867
+ return set.size > 0 ? set : null;
868
+ }
869
+ async function listAccessibleProjectsWithAccountStatusDirect(userId, platform3) {
870
+ try {
871
+ const supabase = getSupabaseClient();
872
+ const { data: memberships } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId);
873
+ const orgIds = (memberships ?? []).map((m) => m.organization_id);
874
+ if (orgIds.length === 0) return [];
875
+ const { data: projects } = await supabase.from("projects").select("id, name").in("organization_id", orgIds).order("created_at", { ascending: false });
876
+ const projectRows = projects ?? [];
877
+ if (projectRows.length === 0) return [];
878
+ const projectIds = projectRows.map((p) => p.id);
879
+ const { data: accounts } = await supabase.from("connected_accounts").select("project_id, status, platform").eq("user_id", userId).in("project_id", projectIds);
880
+ const anyAccountByProject = /* @__PURE__ */ new Set();
881
+ const platformsByProject = /* @__PURE__ */ new Map();
882
+ for (const row of accounts ?? []) {
883
+ if (row.status !== "active" && row.status !== "expires_soon") continue;
884
+ if (!row.project_id) continue;
885
+ anyAccountByProject.add(row.project_id);
886
+ if (row.platform) {
887
+ const set = platformsByProject.get(row.project_id) ?? /* @__PURE__ */ new Set();
888
+ set.add(row.platform.toLowerCase());
889
+ platformsByProject.set(row.project_id, set);
890
+ }
891
+ }
892
+ const requestedPlatforms = normalizePlatformFilter(platform3);
893
+ return projectRows.map((p) => {
894
+ const platforms = Array.from(platformsByProject.get(p.id) ?? []);
895
+ const hasConnectedAccounts = requestedPlatforms ? platforms.some((pl) => requestedPlatforms.has(pl)) : anyAccountByProject.has(p.id);
896
+ return { id: p.id, name: p.name, hasConnectedAccounts, platforms };
897
+ });
898
+ } catch {
899
+ return [];
900
+ }
901
+ }
902
+ async function listAccessibleProjectsWithAccountStatusViaEdgeFunction(userId, platform3) {
903
+ try {
904
+ const { callEdgeFunction: callEdgeFunction2 } = await Promise.resolve().then(() => (init_edge_function(), edge_function_exports));
905
+ const { data, error } = await callEdgeFunction2(
906
+ "mcp-data",
907
+ { action: "projects", userId, user_id: userId },
908
+ { timeoutMs: 1e4 }
909
+ );
910
+ if (error || !data?.success || !Array.isArray(data.projects)) return [];
911
+ const requestedPlatforms = normalizePlatformFilter(platform3);
912
+ return data.projects.map((p) => {
913
+ const platforms = (p.platforms ?? []).map((pl) => pl.toLowerCase());
914
+ const hasConnectedAccounts = requestedPlatforms ? platforms.some((pl) => requestedPlatforms.has(pl)) : Boolean(p.hasConnectedAccounts);
915
+ return { id: p.id, name: p.name, hasConnectedAccounts, platforms };
916
+ });
917
+ } catch {
918
+ return [];
919
+ }
920
+ }
921
+ async function listAccessibleProjectsWithAccountStatus(userId, platform3) {
922
+ if (getServiceKeyOrNull()) {
923
+ return listAccessibleProjectsWithAccountStatusDirect(userId, platform3);
924
+ }
925
+ return listAccessibleProjectsWithAccountStatusViaEdgeFunction(
926
+ userId,
927
+ platform3
928
+ );
929
+ }
930
+ async function resolveProjectForConnectedAccountTool(explicitProjectId, platform3) {
931
+ if (explicitProjectId) return { projectId: explicitProjectId };
932
+ const defaultProjectId = await getDefaultProjectId();
933
+ if (defaultProjectId) return { projectId: defaultProjectId };
934
+ const genericError = "project_id is required. Configure an explicit project or use an API key scoped to exactly one project.";
935
+ const userId = await getDefaultUserId().catch(() => null);
936
+ if (!userId) return { error: genericError };
937
+ const projects = await listAccessibleProjectsWithAccountStatus(
938
+ userId,
939
+ platform3
940
+ );
941
+ if (projects.length === 0) return { error: genericError };
942
+ const withAccounts = projects.filter((p) => p.hasConnectedAccounts);
943
+ if (withAccounts.length === 1) {
944
+ const chosen = withAccounts[0];
945
+ const platformNote = platform3 ? ` for ${Array.isArray(platform3) ? platform3.join("/") : platform3}` : "";
946
+ return {
947
+ projectId: chosen.id,
948
+ autoResolvedNote: `project_id was not provided; auto-resolved to "${chosen.name}" (${chosen.id}) \u2014 the only one of your ${projects.length} project(s) with an active connected account${platformNote}.`,
949
+ projects
950
+ };
951
+ }
952
+ const projectList = projects.map(
953
+ (p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
954
+ ).join("; ");
955
+ return {
956
+ error: `project_id is required \u2014 your account has ${projects.length} projects and the target could not be auto-resolved. Pass the exact project_id from this list: ${projectList}.`,
957
+ projects
958
+ };
959
+ }
960
+ async function resolveProjectStrict(explicitProjectId) {
961
+ if (explicitProjectId) return { projectId: explicitProjectId };
962
+ const defaultProjectId = await getDefaultProjectId();
963
+ if (defaultProjectId) return { projectId: defaultProjectId };
964
+ const genericError = "project_id is required. Configure an explicit project or use an API key scoped to exactly one project.";
965
+ const userId = await getDefaultUserId().catch(() => null);
966
+ if (!userId) return { error: genericError };
967
+ const projects = await listAccessibleProjectsWithAccountStatus(userId);
968
+ if (projects.length === 0) return { error: genericError };
969
+ const projectList = projects.map(
970
+ (p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
971
+ ).join("; ");
972
+ return {
973
+ error: `project_id is required \u2014 your account has ${projects.length} projects. Pass the exact project_id from this list: ${projectList}.`,
974
+ projects
975
+ };
976
+ }
602
977
  async function initializeAuth() {
603
978
  const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
604
979
  const apiKey = await loadApiKey2();
@@ -624,8 +999,11 @@ async function initializeAuth() {
624
999
  }
625
1000
  if (authenticatedExpiresAt) {
626
1001
  const expiresMs = new Date(authenticatedExpiresAt).getTime();
627
- const daysLeft = Math.ceil((expiresMs - Date.now()) / (1e3 * 60 * 60 * 24));
628
- if (!_quietAuth) console.error("[MCP] Key expires: " + authenticatedExpiresAt);
1002
+ const daysLeft = Math.ceil(
1003
+ (expiresMs - Date.now()) / (1e3 * 60 * 60 * 24)
1004
+ );
1005
+ if (!_quietAuth)
1006
+ console.error("[MCP] Key expires: " + authenticatedExpiresAt);
629
1007
  if (daysLeft <= 7) {
630
1008
  console.error(
631
1009
  `[MCP] Warning: API key expires in ${daysLeft} day(s). Run: npx @socialneuron/mcp-server login`
@@ -704,7 +1082,7 @@ async function logMcpToolInvocation(args) {
704
1082
  Promise.resolve().then(() => (init_posthog(), posthog_exports)).then(({ captureToolEvent: captureToolEvent2 }) => captureToolEvent2(args)).catch(() => {
705
1083
  });
706
1084
  }
707
- var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, authenticatedProjectId, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY, projectIdCache;
1085
+ var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, authenticatedProjectId, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY;
708
1086
  var init_supabase = __esm({
709
1087
  "src/lib/supabase.ts"() {
710
1088
  "use strict";
@@ -722,7 +1100,6 @@ var init_supabase = __esm({
722
1100
  MCP_RUN_ID = randomUUID();
723
1101
  CLOUD_SUPABASE_URL = "https://rhukkjscgzauutioyeei.supabase.co";
724
1102
  CLOUD_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJodWtranNjZ3phdXV0aW95ZWVpIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ4NjM4ODYsImV4cCI6MjA4MDQzOTg4Nn0.JVtrviGvN0HaSh0JFS5KNl5FAB5ffG5Y1IMZsQFUrNQ";
725
- projectIdCache = /* @__PURE__ */ new Map();
726
1103
  }
727
1104
  });
728
1105
 
@@ -877,250 +1254,47 @@ function classifyError(err) {
877
1254
  message,
878
1255
  errorType: "RATE_LIMIT",
879
1256
  retryable: true,
880
- hint: "Wait a moment and retry."
881
- };
882
- }
883
- if (lower.includes("not found") || lower.includes("404") || lower.includes("no job found")) {
884
- return { message, errorType: "NOT_FOUND", retryable: false };
885
- }
886
- if (lower.includes("missing required") || lower.includes("invalid") || lower.includes("cannot delete builtin")) {
887
- return { message, errorType: "VALIDATION", retryable: false };
888
- }
889
- return { message, errorType: "INTERNAL", retryable: true };
890
- }
891
- async function withSnErrorHandling(command, asJson, fn, replMode = false) {
892
- try {
893
- await fn();
894
- } catch (err) {
895
- const classified = classifyError(err);
896
- if (asJson) {
897
- const response = {
898
- ok: false,
899
- command,
900
- error: classified.message,
901
- errorType: classified.errorType,
902
- retryable: classified.retryable,
903
- ...classified.hint ? { hint: classified.hint } : {},
904
- schema_version: "1"
905
- };
906
- process.stdout.write(JSON.stringify(response, null, 2) + "\n");
907
- } else {
908
- process.stderr.write(`Error [${command}]: ${classified.message}
909
- `);
910
- if (classified.hint) process.stderr.write(`Hint: ${classified.hint}
911
- `);
912
- }
913
- if (!replMode) {
914
- process.exit(1);
915
- }
916
- }
917
- }
918
- var init_error_handling = __esm({
919
- "src/cli/error-handling.ts"() {
920
- "use strict";
921
- }
922
- });
923
-
924
- // src/lib/edge-function.ts
925
- var edge_function_exports = {};
926
- __export(edge_function_exports, {
927
- callEdgeFunction: () => callEdgeFunction
928
- });
929
- function safeGatewayError(responseText, status) {
930
- try {
931
- const parsed = JSON.parse(responseText);
932
- const nested = parsed.error && typeof parsed.error === "object" ? parsed.error : null;
933
- const candidates = [
934
- nested?.error_type,
935
- nested?.code,
936
- parsed.error_type,
937
- parsed.error_code,
938
- parsed.code,
939
- typeof parsed.error === "string" ? parsed.error : null
940
- ];
941
- for (const value of candidates) {
942
- if (typeof value !== "string") continue;
943
- const normalized = value.trim().toLowerCase();
944
- if (SAFE_GATEWAY_ERROR_CODES.has(normalized)) return normalized;
945
- const embedded = [...SAFE_GATEWAY_ERROR_CODES].find((code) => normalized.includes(code));
946
- if (embedded) return embedded;
947
- }
948
- } catch {
949
- }
950
- return `Backend request failed (HTTP ${status}).`;
951
- }
952
- function safeFailureData(responseText) {
953
- try {
954
- const parsed = JSON.parse(responseText);
955
- const metric = (name) => {
956
- const value = parsed[name];
957
- return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
958
- };
959
- const billingStatus = typeof parsed.billing_status === "string" && SAFE_BILLING_STATUSES.has(parsed.billing_status) ? parsed.billing_status : void 0;
960
- const failureReason = parsed.failure_reason === "generation_failed" || parsed.failure_reason === "authentication_failed" || parsed.failure_reason === "cancelled_by_user" ? parsed.failure_reason : void 0;
961
- const jobStatus = typeof parsed.status === "string" && SAFE_JOB_STATUSES.has(parsed.status) ? parsed.status : void 0;
962
- const safe = {
963
- ...jobStatus ? { status: jobStatus } : {},
964
- ...metric("credits_reserved") !== void 0 ? { credits_reserved: metric("credits_reserved") } : {},
965
- ...metric("credits_charged") !== void 0 ? { credits_charged: metric("credits_charged") } : {},
966
- ...metric("credits_refunded") !== void 0 ? { credits_refunded: metric("credits_refunded") } : {},
967
- ...billingStatus ? { billing_status: billingStatus } : {},
968
- ...failureReason ? { failure_reason: failureReason } : {}
969
- };
970
- return Object.keys(safe).length > 0 ? safe : null;
971
- } catch {
972
- return null;
973
- }
974
- }
975
- function getApiKeyOrNull() {
976
- const envKey = process.env.SOCIALNEURON_API_KEY;
977
- if (envKey && envKey.trim().length) return envKey.trim();
978
- const requestToken = getRequestToken();
979
- if (requestToken) return requestToken;
980
- return getAuthenticatedApiKey();
981
- }
982
- async function callEdgeFunction(functionName, body, options) {
983
- const supabaseUrl = getSupabaseUrl();
984
- const apiKey = getApiKeyOrNull();
985
- const controller = new AbortController();
986
- const timeoutMs = options?.timeoutMs ?? 6e4;
987
- const timer = setTimeout(() => controller.abort(), timeoutMs);
988
- const enrichedBody = { ...body };
989
- if (!enrichedBody.userId && !enrichedBody.user_id) {
990
- try {
991
- const defaultId = await getDefaultUserId();
992
- enrichedBody.userId = defaultId;
993
- enrichedBody.user_id = defaultId;
994
- } catch {
995
- }
996
- } else {
997
- if (enrichedBody.userId && !enrichedBody.user_id) enrichedBody.user_id = enrichedBody.userId;
998
- if (enrichedBody.user_id && !enrichedBody.userId) enrichedBody.userId = enrichedBody.user_id;
999
- }
1000
- if (!enrichedBody.projectId && !enrichedBody.project_id) {
1001
- try {
1002
- const { getDefaultProjectId: getDefaultProjectId2 } = await Promise.resolve().then(() => (init_supabase(), supabase_exports));
1003
- const defaultProjectId = await getDefaultProjectId2();
1004
- if (defaultProjectId) {
1005
- enrichedBody.projectId = defaultProjectId;
1006
- enrichedBody.project_id = defaultProjectId;
1007
- }
1008
- } catch {
1009
- }
1010
- }
1011
- let url;
1012
- let method = options?.method ?? "POST";
1013
- let headers;
1014
- let requestBody;
1015
- if (!apiKey) {
1016
- clearTimeout(timer);
1017
- return {
1018
- data: null,
1019
- error: "Not authenticated. Run: npx @socialneuron/mcp-server login \u2014 Requires a paid plan (Starter+). See https://socialneuron.com/pricing"
1020
- };
1021
- }
1022
- url = new URL(`${supabaseUrl}/functions/v1/mcp-gateway`);
1023
- headers = {
1024
- Authorization: `Bearer ${apiKey}`,
1025
- "Content-Type": "application/json"
1026
- };
1027
- requestBody = {
1028
- functionName,
1029
- body: enrichedBody,
1030
- query: options?.query,
1031
- method: method.toUpperCase(),
1032
- timeoutMs
1033
- };
1034
- method = "POST";
1035
- try {
1036
- const response = await fetch(url.toString(), {
1037
- method,
1038
- headers,
1039
- body: method === "GET" ? void 0 : JSON.stringify(requestBody),
1040
- signal: controller.signal
1041
- });
1042
- clearTimeout(timer);
1043
- const responseText = await response.text();
1044
- if (!response.ok) {
1045
- const errorCode = safeGatewayError(responseText, response.status);
1046
- const failureData = safeFailureData(responseText);
1047
- if (response.status === 401) {
1048
- return {
1049
- data: failureData,
1050
- error: `Authentication failed (HTTP 401). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
1051
- };
1052
- }
1053
- if (response.status === 403) {
1054
- return {
1055
- data: failureData,
1056
- error: `Forbidden (HTTP 403): ${errorCode}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
1057
- };
1058
- }
1059
- if (response.status === 429) {
1060
- const retryAfter = response.headers.get("retry-after") || "60";
1061
- return {
1062
- data: failureData,
1063
- error: `Rate limit exceeded (HTTP 429). Wait ${retryAfter}s before retrying. Reduce request frequency or upgrade your plan.`
1064
- };
1065
- }
1066
- return { data: failureData, error: errorCode };
1067
- }
1068
- try {
1069
- const data = JSON.parse(responseText);
1070
- return { data, error: null };
1071
- } catch {
1072
- return { data: { text: responseText }, error: null };
1073
- }
1257
+ hint: "Wait a moment and retry."
1258
+ };
1259
+ }
1260
+ if (lower.includes("not found") || lower.includes("404") || lower.includes("no job found")) {
1261
+ return { message, errorType: "NOT_FOUND", retryable: false };
1262
+ }
1263
+ if (lower.includes("missing required") || lower.includes("invalid") || lower.includes("cannot delete builtin")) {
1264
+ return { message, errorType: "VALIDATION", retryable: false };
1265
+ }
1266
+ return { message, errorType: "INTERNAL", retryable: true };
1267
+ }
1268
+ async function withSnErrorHandling(command, asJson, fn, replMode = false) {
1269
+ try {
1270
+ await fn();
1074
1271
  } catch (err) {
1075
- clearTimeout(timer);
1076
- if (err instanceof Error && err.name === "AbortError") {
1077
- return {
1078
- data: null,
1079
- error: `Edge Function '${functionName}' timed out after ${timeoutMs}ms`
1272
+ const classified = classifyError(err);
1273
+ if (asJson) {
1274
+ const response = {
1275
+ ok: false,
1276
+ command,
1277
+ error: classified.message,
1278
+ errorType: classified.errorType,
1279
+ retryable: classified.retryable,
1280
+ ...classified.hint ? { hint: classified.hint } : {},
1281
+ schema_version: "1"
1080
1282
  };
1283
+ process.stdout.write(JSON.stringify(response, null, 2) + "\n");
1284
+ } else {
1285
+ process.stderr.write(`Error [${command}]: ${classified.message}
1286
+ `);
1287
+ if (classified.hint) process.stderr.write(`Hint: ${classified.hint}
1288
+ `);
1289
+ }
1290
+ if (!replMode) {
1291
+ process.exit(1);
1081
1292
  }
1082
- return { data: null, error: "Network request failed. Please retry." };
1083
1293
  }
1084
1294
  }
1085
- var SAFE_GATEWAY_ERROR_CODES, SAFE_BILLING_STATUSES, SAFE_JOB_STATUSES;
1086
- var init_edge_function = __esm({
1087
- "src/lib/edge-function.ts"() {
1295
+ var init_error_handling = __esm({
1296
+ "src/cli/error-handling.ts"() {
1088
1297
  "use strict";
1089
- init_supabase();
1090
- init_request_context();
1091
- SAFE_GATEWAY_ERROR_CODES = /* @__PURE__ */ new Set([
1092
- "daily_limit_reached",
1093
- "insufficient_credits",
1094
- "project_scope_mismatch",
1095
- "schedule_conflict",
1096
- "post_not_found",
1097
- "post_not_reschedulable",
1098
- "post_in_progress",
1099
- "not_cancellable",
1100
- "publishing_in_progress",
1101
- "plan_upgrade_required",
1102
- "rate_limited",
1103
- "validation_error",
1104
- "permission_denied",
1105
- "not_found"
1106
- ]);
1107
- SAFE_BILLING_STATUSES = /* @__PURE__ */ new Set([
1108
- "reserved",
1109
- "charged",
1110
- "refunded",
1111
- "failed_no_charge",
1112
- "refund_pending",
1113
- "not_charged"
1114
- ]);
1115
- SAFE_JOB_STATUSES = /* @__PURE__ */ new Set([
1116
- "queued",
1117
- "pending",
1118
- "processing",
1119
- "completed",
1120
- "failed",
1121
- "cancelled",
1122
- "canceled"
1123
- ]);
1124
1298
  }
1125
1299
  });
1126
1300
 
@@ -4377,7 +4551,10 @@ function registerContentTools(server) {
4377
4551
  isError: true
4378
4552
  };
4379
4553
  }
4380
- const rateLimit = checkRateLimit("generation", `generate_video:${userId}`);
4554
+ const rateLimit = checkRateLimit(
4555
+ "generation",
4556
+ `generate_video:${userId}`
4557
+ );
4381
4558
  if (!rateLimit.allowed) {
4382
4559
  return {
4383
4560
  content: [
@@ -4500,7 +4677,14 @@ function registerContentTools(server) {
4500
4677
  project_id: z2.string().optional().describe("Project ID to associate the generated image with."),
4501
4678
  response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
4502
4679
  },
4503
- async ({ prompt: prompt2, model, aspect_ratio, image_url, project_id, response_format }) => {
4680
+ async ({
4681
+ prompt: prompt2,
4682
+ model,
4683
+ aspect_ratio,
4684
+ image_url,
4685
+ project_id,
4686
+ response_format
4687
+ }) => {
4504
4688
  const format = response_format ?? "text";
4505
4689
  const userId = await getDefaultUserId();
4506
4690
  const assetBudget = checkAssetBudget();
@@ -4518,7 +4702,10 @@ function registerContentTools(server) {
4518
4702
  isError: true
4519
4703
  };
4520
4704
  }
4521
- const rateLimit = checkRateLimit("generation", `generate_image:${userId}`);
4705
+ const rateLimit = checkRateLimit(
4706
+ "generation",
4707
+ `generate_image:${userId}`
4708
+ );
4522
4709
  if (!rateLimit.allowed) {
4523
4710
  return {
4524
4711
  content: [
@@ -4649,6 +4836,15 @@ function registerContentTools(server) {
4649
4836
  isError: true
4650
4837
  };
4651
4838
  }
4839
+ let projectsDisclosure;
4840
+ const ownScopedProjectId = await getDefaultProjectId();
4841
+ if (!ownScopedProjectId) {
4842
+ const ownUserId = await getDefaultUserId().catch(() => null);
4843
+ if (ownUserId) {
4844
+ const list = await listAccessibleProjectsWithAccountStatus(ownUserId);
4845
+ if (list.length > 0) projectsDisclosure = list;
4846
+ }
4847
+ }
4652
4848
  if (job.external_id && (job.status === "pending" || job.status === "processing")) {
4653
4849
  const { data: liveStatus } = await callEdgeFunction(
4654
4850
  "kie-task-status",
@@ -4672,7 +4868,9 @@ function registerContentTools(server) {
4672
4868
  if (livePayload.error) {
4673
4869
  lines2.push(`Error: ${livePayload.error}`);
4674
4870
  }
4675
- const fallbackDisclosure2 = buildFallbackDisclosureLine(job.result_metadata);
4871
+ const fallbackDisclosure2 = buildFallbackDisclosureLine(
4872
+ job.result_metadata
4873
+ );
4676
4874
  if (fallbackDisclosure2) lines2.push(fallbackDisclosure2);
4677
4875
  lines2.push(`Credits: ${job.credits_cost}`);
4678
4876
  if (job.billing_status) {
@@ -4681,6 +4879,14 @@ function registerContentTools(server) {
4681
4879
  );
4682
4880
  }
4683
4881
  lines2.push(`Created: ${job.created_at}`);
4882
+ if (projectsDisclosure) {
4883
+ lines2.push(
4884
+ "",
4885
+ `Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
4886
+ (p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
4887
+ ).join("; ")}`
4888
+ );
4889
+ }
4684
4890
  if (format === "json") {
4685
4891
  return {
4686
4892
  content: [
@@ -4697,7 +4903,8 @@ function registerContentTools(server) {
4697
4903
  // `job` + `liveStatus`; do not duplicate them here.
4698
4904
  asEnvelope2({
4699
4905
  ...liveStatus,
4700
- ...livePayload
4906
+ ...livePayload,
4907
+ ...projectsDisclosure ? { projects: projectsDisclosure } : {}
4701
4908
  }),
4702
4909
  null,
4703
4910
  2
@@ -4740,7 +4947,9 @@ function registerContentTools(server) {
4740
4947
  if (job.error_message) {
4741
4948
  lines.push(`Error: ${job.error_message}`);
4742
4949
  }
4743
- const fallbackDisclosure = buildFallbackDisclosureLine(job.result_metadata);
4950
+ const fallbackDisclosure = buildFallbackDisclosureLine(
4951
+ job.result_metadata
4952
+ );
4744
4953
  if (fallbackDisclosure) lines.push(fallbackDisclosure);
4745
4954
  lines.push(`Credits: ${job.credits_cost}`);
4746
4955
  if (job.billing_status) {
@@ -4752,10 +4961,19 @@ function registerContentTools(server) {
4752
4961
  if (job.completed_at) {
4753
4962
  lines.push(`Completed: ${job.completed_at}`);
4754
4963
  }
4964
+ if (projectsDisclosure) {
4965
+ lines.push(
4966
+ "",
4967
+ `Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
4968
+ (p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
4969
+ ).join("; ")}`
4970
+ );
4971
+ }
4755
4972
  if (format === "json") {
4756
4973
  const enriched = {
4757
4974
  ...job,
4758
- ...buildCheckStatusPayload(job)
4975
+ ...buildCheckStatusPayload(job),
4976
+ ...projectsDisclosure ? { projects: projectsDisclosure } : {}
4759
4977
  };
4760
4978
  return {
4761
4979
  content: [
@@ -5499,6 +5717,122 @@ var init_ssrf = __esm({
5499
5717
  }
5500
5718
  });
5501
5719
 
5720
+ // src/lib/connected-account-routing.ts
5721
+ function canonicalPlatform(value) {
5722
+ const normalized = value.trim().toLowerCase();
5723
+ return normalized === "x" ? "twitter" : normalized;
5724
+ }
5725
+ function providerPlatform(value) {
5726
+ return PLATFORM_CASE_MAP[canonicalPlatform(value)] ?? value;
5727
+ }
5728
+ function isUsable(account) {
5729
+ const status = account.effective_status ?? account.status;
5730
+ return status === "active" || status === "expires_soon";
5731
+ }
5732
+ function normalizeRequestedIds(requested) {
5733
+ const ids = /* @__PURE__ */ new Map();
5734
+ for (const [platform3, accountId] of Object.entries(requested ?? {})) {
5735
+ const canonical = canonicalPlatform(platform3);
5736
+ const existing = ids.get(canonical);
5737
+ if (existing && existing !== accountId) {
5738
+ return {
5739
+ error: `Conflicting account IDs were supplied for ${platform3} and its platform alias.`
5740
+ };
5741
+ }
5742
+ ids.set(canonical, accountId);
5743
+ }
5744
+ return { ids };
5745
+ }
5746
+ async function resolveConnectedAccountRouting(input) {
5747
+ const normalizedRequested = normalizeRequestedIds(input.requestedAccountIds);
5748
+ if (normalizedRequested.error) return { error: normalizedRequested.error };
5749
+ const targetPlatforms = new Set(input.platforms.map(canonicalPlatform));
5750
+ for (const requestedPlatform of normalizedRequested.ids?.keys() ?? []) {
5751
+ if (!targetPlatforms.has(requestedPlatform)) {
5752
+ return {
5753
+ error: `An account ID was supplied for untargeted platform ${requestedPlatform}.`
5754
+ };
5755
+ }
5756
+ }
5757
+ const { data, error } = await callEdgeFunction(
5758
+ "mcp-data",
5759
+ {
5760
+ action: "connected-accounts",
5761
+ projectId: input.projectId,
5762
+ project_id: input.projectId
5763
+ },
5764
+ { timeoutMs: 1e4 }
5765
+ );
5766
+ if (error || !Array.isArray(data?.accounts)) {
5767
+ return {
5768
+ error: `Connected-account verification failed: ${error ?? data?.error ?? "no account inventory returned"}.`
5769
+ };
5770
+ }
5771
+ const connectedAccountIds = {};
5772
+ for (const platform3 of input.platforms) {
5773
+ const canonical = canonicalPlatform(platform3);
5774
+ const displayPlatform = providerPlatform(platform3);
5775
+ const platformAccounts = data.accounts.filter(
5776
+ (account) => canonicalPlatform(account.platform) === canonical && account.project_id === input.projectId && isUsable(account)
5777
+ );
5778
+ const requestedId = normalizedRequested.ids?.get(canonical);
5779
+ let selected;
5780
+ if (requestedId) {
5781
+ selected = data.accounts.find((account) => account.id === requestedId);
5782
+ if (!selected) {
5783
+ return {
5784
+ error: `${displayPlatform}: account ${requestedId} is not available for project_id ${input.projectId}.`
5785
+ };
5786
+ }
5787
+ if (canonicalPlatform(selected.platform) !== canonical) {
5788
+ return {
5789
+ error: `${displayPlatform}: account ${requestedId} belongs to ${selected.platform}.`
5790
+ };
5791
+ }
5792
+ if (selected.project_id !== input.projectId) {
5793
+ return {
5794
+ error: `${displayPlatform}: account ${requestedId} is not bound to project_id ${input.projectId}.`
5795
+ };
5796
+ }
5797
+ if (!isUsable(selected)) {
5798
+ return {
5799
+ error: `${displayPlatform}: account ${requestedId} is ${selected.effective_status ?? selected.status}.`
5800
+ };
5801
+ }
5802
+ } else if (platformAccounts.length === 1) {
5803
+ selected = platformAccounts[0];
5804
+ } else if (platformAccounts.length === 0) {
5805
+ return {
5806
+ error: `${displayPlatform}: no active account is bound to project_id ${input.projectId}.`
5807
+ };
5808
+ } else {
5809
+ return {
5810
+ error: `${displayPlatform}: multiple active accounts are bound to project_id ${input.projectId}; pass the exact account ID returned by list_connected_accounts.`
5811
+ };
5812
+ }
5813
+ connectedAccountIds[displayPlatform] = selected.id;
5814
+ }
5815
+ return { connectedAccountIds };
5816
+ }
5817
+ var PLATFORM_CASE_MAP;
5818
+ var init_connected_account_routing = __esm({
5819
+ "src/lib/connected-account-routing.ts"() {
5820
+ "use strict";
5821
+ init_edge_function();
5822
+ PLATFORM_CASE_MAP = {
5823
+ youtube: "YouTube",
5824
+ tiktok: "TikTok",
5825
+ instagram: "Instagram",
5826
+ twitter: "Twitter",
5827
+ x: "Twitter",
5828
+ linkedin: "LinkedIn",
5829
+ facebook: "Facebook",
5830
+ threads: "Threads",
5831
+ bluesky: "Bluesky"
5832
+ };
5833
+ }
5834
+ });
5835
+
5502
5836
  // src/tools/distribution.ts
5503
5837
  import { z as z3 } from "zod";
5504
5838
  import { createHash as createHash2 } from "node:crypto";
@@ -5611,21 +5945,6 @@ async function validatePublishMediaUrl(url) {
5611
5945
  function accountEffectiveStatus(account) {
5612
5946
  return account.effective_status || account.status;
5613
5947
  }
5614
- function isUsableAccount(account) {
5615
- const status = accountEffectiveStatus(account);
5616
- return status === "active" || status === "expires_soon";
5617
- }
5618
- function formatAccountChoice(account) {
5619
- const name = account.username ? `@${account.username}` : "unnamed";
5620
- const project = account.project_id ? `project_id=${account.project_id}` : "project_id=unassigned";
5621
- const refresh = account.has_refresh_token ? "OAuth 2.0 refresh" : "no refresh token";
5622
- return `${account.id} (${name}, ${project}, ${accountEffectiveStatus(account)}, ${refresh})`;
5623
- }
5624
- function requestedAccountIdForPlatform(accountId, accountIds, platform3) {
5625
- if (accountId) return accountId;
5626
- if (!accountIds) return void 0;
5627
- return accountIds[platform3] || accountIds[platform3.toLowerCase()];
5628
- }
5629
5948
  async function rehostExternalUrl(mediaUrl, projectId) {
5630
5949
  const ssrf = await validateUrlForSSRF(mediaUrl);
5631
5950
  if (!ssrf.isValid) {
@@ -5761,17 +6080,17 @@ function registerDistributionTools(server) {
5761
6080
  schedule_at: z3.string().optional().describe(
5762
6081
  'ISO 8601 UTC datetime for scheduled posting (e.g. "2026-03-20T14:00:00Z"). Omit to post immediately. Must be in the future.'
5763
6082
  ),
5764
- project_id: z3.string().optional().describe(
6083
+ project_id: z3.string().uuid().optional().describe(
5765
6084
  "Social Neuron brand/project ID to associate this post with. Provide this when the account has multiple brands so brand voice and connected account routing stay scoped to the right brand."
5766
6085
  ),
5767
6086
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
5768
6087
  attribution: z3.boolean().optional().describe(
5769
6088
  'If true, appends "Created with Social Neuron" to the caption. Default: false.'
5770
6089
  ),
5771
- account_id: z3.string().optional().describe(
5772
- "Connected account ID to post from. 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."
6090
+ account_id: z3.string().uuid().optional().describe(
6091
+ "Connected account ID to post from. Optional when the resolved project has exactly one active account for the target platform \u2014 it is auto-bound. Required (with a clear error listing candidates) when multiple accounts exist for the same platform. Use list_connected_accounts to find the right ID. The account must be active and bound to the exact project_id."
5773
6092
  ),
5774
- account_ids: z3.record(z3.string(), z3.string()).optional().describe(
6093
+ account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
5775
6094
  'Per-platform account IDs when posting to multiple platforms. Example: {"twitter": "abc123", "instagram": "def456"}. Use list_connected_accounts with the same project_id to find IDs.'
5776
6095
  ),
5777
6096
  auto_rehost: z3.boolean().optional().describe(
@@ -5815,6 +6134,45 @@ function registerDistributionTools(server) {
5815
6134
  isError: true
5816
6135
  };
5817
6136
  }
6137
+ const projectResolution = await resolveProjectForConnectedAccountTool(
6138
+ project_id,
6139
+ platforms
6140
+ );
6141
+ if (!projectResolution.projectId) {
6142
+ return {
6143
+ content: [
6144
+ {
6145
+ type: "text",
6146
+ text: projectResolution.error ?? "A project_id is required for publishing. Configure an explicit project or use an API key that is scoped to exactly one project."
6147
+ }
6148
+ ],
6149
+ isError: true
6150
+ };
6151
+ }
6152
+ const resolvedProjectId = projectResolution.projectId;
6153
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
6154
+ if (account_id && account_ids) {
6155
+ return {
6156
+ content: [
6157
+ {
6158
+ type: "text",
6159
+ text: "Pass either account_id or account_ids, not both."
6160
+ }
6161
+ ],
6162
+ isError: true
6163
+ };
6164
+ }
6165
+ if (account_id && platforms.length !== 1) {
6166
+ return {
6167
+ content: [
6168
+ {
6169
+ type: "text",
6170
+ text: "account_id is valid only for a single target platform. Use account_ids for multi-platform publishing."
6171
+ }
6172
+ ],
6173
+ isError: true
6174
+ };
6175
+ }
5818
6176
  const userId = await getDefaultUserId();
5819
6177
  const rateLimit = checkRateLimit("posting", `schedule_post:${userId}`);
5820
6178
  if (!rateLimit.allowed) {
@@ -5919,11 +6277,16 @@ function registerDistributionTools(server) {
5919
6277
  }
5920
6278
  const resolvedJobs = resolved;
5921
6279
  resolvedMediaUrls = resolvedJobs.map((item) => item.url);
5922
- resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map((item) => item.trustedR2);
6280
+ resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map(
6281
+ (item) => item.trustedR2
6282
+ );
5923
6283
  }
5924
6284
  const shouldRehost = auto_rehost !== false;
5925
6285
  if (shouldRehost && resolvedMediaUrl && !resolvedMediaUrlIsTrustedR2) {
5926
- const rehost = await rehostExternalUrl(resolvedMediaUrl, project_id);
6286
+ const rehost = await rehostExternalUrl(
6287
+ resolvedMediaUrl,
6288
+ resolvedProjectId
6289
+ );
5927
6290
  if ("error" in rehost) {
5928
6291
  return {
5929
6292
  content: [
@@ -5941,7 +6304,7 @@ function registerDistributionTools(server) {
5941
6304
  if (shouldRehost && resolvedMediaUrls && resolvedMediaUrls.length > 0) {
5942
6305
  const rehosted = await Promise.all(
5943
6306
  resolvedMediaUrls.map(
5944
- (u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, project_id)
6307
+ (u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, resolvedProjectId)
5945
6308
  )
5946
6309
  );
5947
6310
  const failIdx = rehosted.findIndex((r) => "error" in r);
@@ -6008,7 +6371,7 @@ function registerDistributionTools(server) {
6008
6371
  }
6009
6372
  }
6010
6373
  const normalizedPlatforms = platforms.map(
6011
- (p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
6374
+ (p) => PLATFORM_CASE_MAP2[p.toLowerCase()] || p
6012
6375
  );
6013
6376
  const blockedPlatforms = normalizedPlatforms.filter(
6014
6377
  (p) => MCP_NOT_LIVE_FOR_POSTING.has(p)
@@ -6024,92 +6387,27 @@ function registerDistributionTools(server) {
6024
6387
  isError: true
6025
6388
  };
6026
6389
  }
6027
- const { data: accountsData } = await callEdgeFunction(
6028
- "mcp-data",
6029
- {
6030
- action: "connected-accounts",
6031
- ...project_id ? { projectId: project_id, project_id } : {}
6032
- },
6033
- { timeoutMs: 1e4 }
6034
- );
6035
- if (accountsData?.accounts) {
6036
- const accounts = accountsData.accounts;
6037
- const issues = [];
6038
- for (const platform3 of normalizedPlatforms) {
6039
- const platformAccounts = accounts.filter(
6040
- (a) => a.platform.toLowerCase() === platform3.toLowerCase() && isUsableAccount(a)
6041
- );
6042
- const requestedAccountId = requestedAccountIdForPlatform(
6043
- account_id,
6044
- account_ids,
6045
- platform3
6046
- );
6047
- if (requestedAccountId) {
6048
- const selected = accounts.find((a) => a.id === requestedAccountId);
6049
- if (!selected) {
6050
- issues.push(
6051
- `${platform3}: Account "${requestedAccountId}" is not available${project_id ? ` for project_id ${project_id}` : ""}. Call \`list_connected_accounts\`${project_id ? " with the same project_id" : ""} and choose one of the returned IDs.`
6052
- );
6053
- } else if (selected.platform.toLowerCase() !== platform3.toLowerCase()) {
6054
- issues.push(
6055
- `${platform3}: Account "${requestedAccountId}" belongs to ${selected.platform}, not ${platform3}.`
6056
- );
6057
- } else if (!isUsableAccount(selected)) {
6058
- issues.push(
6059
- `${platform3}: Account "${selected.username || selected.id}" is ${accountEffectiveStatus(selected)}. Reconnect at socialneuron.com/settings/connections.`
6060
- );
6061
- } else if (project_id && selected.project_id && selected.project_id !== project_id) {
6062
- issues.push(
6063
- `${platform3}: Account "${selected.username || selected.id}" is bound to project_id ${selected.project_id}, not ${project_id}. Use the selected brand's account or reconnect/assign it in Settings > Integrations.`
6064
- );
6065
- }
6066
- continue;
6067
- }
6068
- if (platformAccounts.length === 0) {
6069
- issues.push(
6070
- `${platform3}: not connected yet. This is a one-time browser setup on socialneuron.com \u2014 NOT another OAuth in Claude. Call \`start_platform_connection\` with platform="${platform3.toLowerCase()}"${project_id ? ` and project_id="${project_id}"` : ""} to get a deep link, ask the user to open it in their browser and approve on the platform, then call \`wait_for_connection\` before retrying schedule_post.`
6071
- );
6072
- } else if (platformAccounts.length > 1) {
6073
- const accountList = platformAccounts.map((a) => ` - ${formatAccountChoice(a)}`).join("\n");
6074
- issues.push(
6075
- `${platform3}: Multiple accounts found. Specify account_id or account_ids to choose:
6076
- ${accountList}`
6077
- );
6078
- } else if (platformAccounts.length === 1) {
6079
- const acct = platformAccounts[0];
6080
- if (acct.expires_at && new Date(acct.expires_at) < /* @__PURE__ */ new Date() && !acct.has_refresh_token) {
6081
- issues.push(
6082
- `${platform3}: Account "${acct.username || acct.id}" has expired OAuth and no refresh token. Reconnect at socialneuron.com/settings/connections.`
6083
- );
6084
- }
6085
- }
6086
- }
6087
- if (issues.length > 0) {
6088
- return {
6089
- content: [
6090
- {
6091
- type: "text",
6092
- text: `Cannot post \u2014 account issues found:
6093
-
6094
- ${issues.join("\n\n")}`
6095
- }
6096
- ],
6097
- isError: true
6098
- };
6099
- }
6100
- }
6101
- let connectedAccountIds;
6390
+ let requestedAccountIds;
6102
6391
  if (account_id) {
6103
- connectedAccountIds = {};
6104
- for (const p of normalizedPlatforms) {
6105
- connectedAccountIds[p] = account_id;
6106
- }
6392
+ requestedAccountIds = { [normalizedPlatforms[0]]: account_id };
6107
6393
  } else if (account_ids) {
6108
- connectedAccountIds = {};
6109
- for (const [key, val] of Object.entries(account_ids)) {
6110
- const normalizedKey = PLATFORM_CASE_MAP[key.toLowerCase()] || key;
6111
- connectedAccountIds[normalizedKey] = val;
6112
- }
6394
+ requestedAccountIds = account_ids;
6395
+ }
6396
+ const routing = await resolveConnectedAccountRouting({
6397
+ projectId: resolvedProjectId,
6398
+ platforms: normalizedPlatforms,
6399
+ requestedAccountIds
6400
+ });
6401
+ if (routing.error || !routing.connectedAccountIds) {
6402
+ return {
6403
+ content: [
6404
+ {
6405
+ type: "text",
6406
+ text: `Cannot post \u2014 ${routing.error ?? "exact connected-account routing could not be established."}`
6407
+ }
6408
+ ],
6409
+ isError: true
6410
+ };
6113
6411
  }
6114
6412
  let finalCaption = caption;
6115
6413
  if (attribution && finalCaption) {
@@ -6163,8 +6461,9 @@ Created with Social Neuron`;
6163
6461
  title,
6164
6462
  hashtags,
6165
6463
  scheduledAt: schedule_at,
6166
- projectId: project_id,
6167
- ...connectedAccountIds ? { connectedAccountIds } : {},
6464
+ projectId: resolvedProjectId,
6465
+ project_id: resolvedProjectId,
6466
+ connectedAccountIds: routing.connectedAccountIds,
6168
6467
  ...normalizedPlatformMetadata ? {
6169
6468
  platformMetadata: convertPlatformMetadata(
6170
6469
  normalizedPlatformMetadata
@@ -6199,10 +6498,14 @@ Created with Social Neuron`;
6199
6498
  isError: true
6200
6499
  };
6201
6500
  }
6501
+ const responseData = projectAutoResolvedNote ? { ...data, project_auto_resolved: projectAutoResolvedNote } : data;
6202
6502
  const lines = [
6203
6503
  data.success ? "Post scheduled successfully." : "Post scheduling had errors.",
6204
6504
  `Scheduled for: ${data.scheduledAt}`
6205
6505
  ];
6506
+ if (projectAutoResolvedNote) {
6507
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
6508
+ }
6206
6509
  if (tiktokAutoInboxApplied) {
6207
6510
  lines.push(
6208
6511
  "",
@@ -6220,7 +6523,7 @@ Created with Social Neuron`;
6220
6523
  }
6221
6524
  }
6222
6525
  if (format === "json") {
6223
- const structuredContent = asEnvelope3(data);
6526
+ const structuredContent = asEnvelope3(responseData);
6224
6527
  return {
6225
6528
  structuredContent,
6226
6529
  content: [
@@ -6233,7 +6536,7 @@ Created with Social Neuron`;
6233
6536
  };
6234
6537
  }
6235
6538
  return {
6236
- structuredContent: asEnvelope3(data),
6539
+ structuredContent: asEnvelope3(responseData),
6237
6540
  content: [{ type: "text", text: lines.join("\n") }],
6238
6541
  isError: !data.success
6239
6542
  };
@@ -6247,7 +6550,9 @@ Created with Social Neuron`;
6247
6550
  project_id: z3.string().uuid().optional().describe(
6248
6551
  "Brand/project ID that owns the post. Defaults to the authenticated key's project or the account default."
6249
6552
  ),
6250
- scheduled_at: z3.string().datetime({ offset: true }).describe("New future publish time as an ISO 8601 datetime with timezone."),
6553
+ scheduled_at: z3.string().datetime({ offset: true }).describe(
6554
+ "New future publish time as an ISO 8601 datetime with timezone."
6555
+ ),
6251
6556
  expected_scheduled_at: z3.string().datetime({ offset: true }).optional().describe(
6252
6557
  "Optional current schedule timestamp. If it changed since you read it, the update is rejected instead of silently overwriting it."
6253
6558
  ),
@@ -6303,7 +6608,11 @@ Created with Social Neuron`;
6303
6608
  projectId: resolvedProjectId,
6304
6609
  project_id: resolvedProjectId,
6305
6610
  scheduled_at: next.toISOString(),
6306
- ...expected_scheduled_at ? { expected_scheduled_at: new Date(expected_scheduled_at).toISOString() } : {}
6611
+ ...expected_scheduled_at ? {
6612
+ expected_scheduled_at: new Date(
6613
+ expected_scheduled_at
6614
+ ).toISOString()
6615
+ } : {}
6307
6616
  });
6308
6617
  if (error || !result?.success) {
6309
6618
  const code = result?.error ?? error ?? "reschedule_failed";
@@ -6336,17 +6645,34 @@ Created with Social Neuron`;
6336
6645
  "list_connected_accounts",
6337
6646
  "Check which social platforms have active OAuth connections for posting. Call this before schedule_post to verify credentials. Pass project_id to list the accounts for a specific brand/project, then pass the returned account id as account_id/account_ids when posting. If a platform is missing or expired, the user needs to reconnect at socialneuron.com/settings/connections.",
6338
6647
  {
6339
- project_id: z3.string().optional().describe(
6648
+ project_id: z3.string().uuid().optional().describe(
6340
6649
  "Brand/project ID to scope connected accounts. Use the same project_id when calling schedule_post."
6341
6650
  ),
6342
- include_all: z3.boolean().optional().describe("If true, include expired or inactive accounts as well as usable accounts."),
6651
+ include_all: z3.boolean().optional().describe(
6652
+ "If true, include expired or inactive accounts as well as usable accounts."
6653
+ ),
6343
6654
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6344
6655
  },
6345
6656
  async ({ project_id, include_all, response_format }) => {
6346
6657
  const format = response_format ?? "text";
6658
+ const projectResolution = await resolveProjectForConnectedAccountTool(project_id);
6659
+ if (!projectResolution.projectId) {
6660
+ return {
6661
+ content: [
6662
+ {
6663
+ type: "text",
6664
+ text: projectResolution.error ?? "A project_id is required to list connected accounts. Configure an explicit project or use an API key scoped to exactly one project."
6665
+ }
6666
+ ],
6667
+ isError: true
6668
+ };
6669
+ }
6670
+ const resolvedProjectId = projectResolution.projectId;
6671
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
6347
6672
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
6348
6673
  action: "connected-accounts",
6349
- ...project_id ? { projectId: project_id, project_id } : {},
6674
+ projectId: resolvedProjectId,
6675
+ project_id: resolvedProjectId,
6350
6676
  ...include_all ? { includeAll: true } : {}
6351
6677
  });
6352
6678
  if (efError || !result?.success) {
@@ -6360,10 +6686,27 @@ Created with Social Neuron`;
6360
6686
  isError: true
6361
6687
  };
6362
6688
  }
6363
- const accounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
6689
+ const parsedAccounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
6690
+ if (parsedAccounts.some(
6691
+ (account) => account.project_id !== resolvedProjectId
6692
+ )) {
6693
+ return {
6694
+ content: [
6695
+ {
6696
+ type: "text",
6697
+ text: "Connected-account project attestation failed. No account inventory was returned."
6698
+ }
6699
+ ],
6700
+ isError: true
6701
+ };
6702
+ }
6703
+ const accounts = parsedAccounts;
6364
6704
  if (accounts.length === 0) {
6365
6705
  if (format === "json") {
6366
- const structuredContent = asEnvelope3({ accounts: [] });
6706
+ const structuredContent = asEnvelope3({
6707
+ accounts: [],
6708
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
6709
+ });
6367
6710
  return {
6368
6711
  structuredContent,
6369
6712
  content: [
@@ -6378,13 +6721,15 @@ Created with Social Neuron`;
6378
6721
  content: [
6379
6722
  {
6380
6723
  type: "text",
6381
- text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections."
6724
+ text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections." + (projectAutoResolvedNote ? `
6725
+
6726
+ Note: ${projectAutoResolvedNote}` : "")
6382
6727
  }
6383
6728
  ]
6384
6729
  };
6385
6730
  }
6386
6731
  const lines = [
6387
- `${accounts.length} connected account(s)${project_id ? ` for project ${project_id}` : ""}:`,
6732
+ `${accounts.length} connected account(s) for project ${resolvedProjectId}:`,
6388
6733
  ""
6389
6734
  ];
6390
6735
  for (const account of accounts) {
@@ -6396,8 +6741,14 @@ Created with Social Neuron`;
6396
6741
  ` ${platformLower}: ${name} | id=${account.id} | ${project} | status=${status} (connected ${account.created_at.split("T")[0]})`
6397
6742
  );
6398
6743
  }
6744
+ if (projectAutoResolvedNote) {
6745
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
6746
+ }
6399
6747
  if (format === "json") {
6400
- const structuredContent = asEnvelope3({ accounts });
6748
+ const structuredContent = asEnvelope3({
6749
+ accounts,
6750
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
6751
+ });
6401
6752
  return {
6402
6753
  structuredContent,
6403
6754
  content: [
@@ -6592,7 +6943,10 @@ Created with Social Neuron`;
6592
6943
  if (!Number.isFinite(startDate.getTime())) {
6593
6944
  return {
6594
6945
  content: [
6595
- { type: "text", text: "start_after must be a valid ISO datetime." }
6946
+ {
6947
+ type: "text",
6948
+ text: "start_after must be a valid ISO datetime."
6949
+ }
6596
6950
  ],
6597
6951
  isError: true
6598
6952
  };
@@ -6693,11 +7047,14 @@ Created with Social Neuron`;
6693
7047
  "Schedule all posts in a content plan. Optionally auto-assigns time slots and runs quality checks before scheduling. Supports dry-run mode.",
6694
7048
  {
6695
7049
  plan: z3.object({
7050
+ project_id: z3.string().uuid().optional(),
7051
+ account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe("Exact connected-account ID per platform."),
6696
7052
  posts: z3.array(
6697
7053
  z3.object({
6698
7054
  id: z3.string(),
6699
7055
  caption: z3.string(),
6700
7056
  platform: z3.string(),
7057
+ connected_account_id: z3.string().uuid().optional(),
6701
7058
  title: z3.string().optional(),
6702
7059
  media_url: z3.string().optional(),
6703
7060
  schedule_at: z3.string().optional(),
@@ -6705,6 +7062,12 @@ Created with Social Neuron`;
6705
7062
  })
6706
7063
  )
6707
7064
  }).passthrough().optional(),
7065
+ project_id: z3.string().uuid().optional().describe(
7066
+ "Exact brand/project ID. Defaults only when the authenticated user has one project."
7067
+ ),
7068
+ account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
7069
+ "Exact connected-account ID per platform for every post in the plan."
7070
+ ),
6708
7071
  plan_id: z3.string().uuid().optional().describe("Persisted content plan ID from content_plans table"),
6709
7072
  auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
6710
7073
  dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
@@ -6721,6 +7084,8 @@ Created with Social Neuron`;
6721
7084
  async ({
6722
7085
  plan,
6723
7086
  plan_id,
7087
+ project_id,
7088
+ account_ids,
6724
7089
  auto_slot,
6725
7090
  dry_run,
6726
7091
  response_format,
@@ -6730,9 +7095,11 @@ Created with Social Neuron`;
6730
7095
  idempotency_seed
6731
7096
  }) => {
6732
7097
  try {
7098
+ const effectiveBatchSize = batch_size ?? 4;
6733
7099
  let workingPlan = plan;
6734
7100
  let effectivePlanId = plan_id;
6735
- let effectiveProjectId;
7101
+ let effectiveProjectId = project_id;
7102
+ let projectAutoResolvedNote;
6736
7103
  let approvalSummary;
6737
7104
  if (!workingPlan && plan_id) {
6738
7105
  const { data: planResult, error: planError } = await callEdgeFunction("mcp-data", { action: "get-content-plan", plan_id });
@@ -6779,7 +7146,18 @@ Created with Social Neuron`;
6779
7146
  posts: postsFromPayload
6780
7147
  };
6781
7148
  effectivePlanId = stored.id;
6782
- effectiveProjectId = stored.project_id ?? void 0;
7149
+ if (effectiveProjectId && stored.project_id && effectiveProjectId !== stored.project_id) {
7150
+ return {
7151
+ content: [
7152
+ {
7153
+ type: "text",
7154
+ text: `project_id ${effectiveProjectId} does not own plan ${plan_id}.`
7155
+ }
7156
+ ],
7157
+ isError: true
7158
+ };
7159
+ }
7160
+ effectiveProjectId = stored.project_id ?? effectiveProjectId;
6783
7161
  }
6784
7162
  if (!workingPlan) {
6785
7163
  return {
@@ -6792,10 +7170,35 @@ Created with Social Neuron`;
6792
7170
  isError: true
6793
7171
  };
6794
7172
  }
7173
+ const planProjectId = workingPlan.project_id;
7174
+ if (effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0 && effectiveProjectId !== planProjectId) {
7175
+ return {
7176
+ content: [
7177
+ {
7178
+ type: "text",
7179
+ text: `Conflicting project_id values were supplied for the plan (${planProjectId}) and request (${effectiveProjectId}).`
7180
+ }
7181
+ ],
7182
+ isError: true
7183
+ };
7184
+ }
7185
+ if (!effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0) {
7186
+ effectiveProjectId = planProjectId;
7187
+ }
6795
7188
  if (!effectiveProjectId) {
6796
- const planProjectId = workingPlan.project_id;
6797
- if (typeof planProjectId === "string" && planProjectId.length > 0) {
6798
- effectiveProjectId = planProjectId;
7189
+ const projectResolution = await resolveProjectForConnectedAccountTool();
7190
+ effectiveProjectId = projectResolution.projectId;
7191
+ projectAutoResolvedNote = projectResolution.autoResolvedNote;
7192
+ if (!effectiveProjectId) {
7193
+ return {
7194
+ content: [
7195
+ {
7196
+ type: "text",
7197
+ text: projectResolution.error ?? "A project_id is required to schedule a content plan. Configure an explicit project or use an API key scoped to exactly one project."
7198
+ }
7199
+ ],
7200
+ isError: true
7201
+ };
6799
7202
  }
6800
7203
  }
6801
7204
  if (effectivePlanId) {
@@ -7014,6 +7417,61 @@ Created with Social Neuron`;
7014
7417
  isError: false
7015
7418
  };
7016
7419
  }
7420
+ const embeddedAccountIds = workingPlan.account_ids ?? {};
7421
+ for (const [platform3, accountId] of Object.entries(account_ids ?? {})) {
7422
+ if (embeddedAccountIds[platform3] && embeddedAccountIds[platform3] !== accountId) {
7423
+ return {
7424
+ content: [
7425
+ {
7426
+ type: "text",
7427
+ text: `Conflicting account_ids values were supplied for ${platform3}.`
7428
+ }
7429
+ ],
7430
+ isError: true
7431
+ };
7432
+ }
7433
+ }
7434
+ const requestedPlanAccountIds = {
7435
+ ...embeddedAccountIds,
7436
+ ...account_ids ?? {}
7437
+ };
7438
+ for (const post of workingPlan.posts) {
7439
+ if (!post.connected_account_id) continue;
7440
+ const key = post.platform.toLowerCase() === "x" ? "twitter" : post.platform.toLowerCase();
7441
+ const existing = requestedPlanAccountIds[key];
7442
+ if (existing && existing !== post.connected_account_id) {
7443
+ return {
7444
+ content: [
7445
+ {
7446
+ type: "text",
7447
+ text: `Plan contains conflicting connected_account_id values for ${post.platform}. Use separate plans when scheduling the same platform through different accounts.`
7448
+ }
7449
+ ],
7450
+ isError: true
7451
+ };
7452
+ }
7453
+ requestedPlanAccountIds[key] = post.connected_account_id;
7454
+ }
7455
+ const planPlatforms = Array.from(
7456
+ new Set(workingPlan.posts.map((post) => post.platform))
7457
+ );
7458
+ const planRouting = await resolveConnectedAccountRouting({
7459
+ projectId: effectiveProjectId,
7460
+ platforms: planPlatforms,
7461
+ requestedAccountIds: requestedPlanAccountIds
7462
+ });
7463
+ if (planRouting.error || !planRouting.connectedAccountIds) {
7464
+ return {
7465
+ content: [
7466
+ {
7467
+ type: "text",
7468
+ text: `Cannot schedule plan \u2014 ${planRouting.error ?? "exact connected-account routing could not be established."}`
7469
+ }
7470
+ ],
7471
+ isError: true
7472
+ };
7473
+ }
7474
+ const verifiedPlanAccountIds = planRouting.connectedAccountIds;
7017
7475
  let scheduled = 0;
7018
7476
  let failed = 0;
7019
7477
  const results = [];
@@ -7042,7 +7500,7 @@ Created with Social Neuron`;
7042
7500
  retryable: false
7043
7501
  };
7044
7502
  }
7045
- const normalizedPlatform = PLATFORM_CASE_MAP[post.platform.toLowerCase()] ?? post.platform;
7503
+ const normalizedPlatform = PLATFORM_CASE_MAP2[post.platform.toLowerCase()] ?? post.platform;
7046
7504
  const idempotencyKey = buildIdempotencyKey(post);
7047
7505
  const { data, error } = await callEdgeFunction(
7048
7506
  "schedule-post",
@@ -7053,6 +7511,13 @@ Created with Social Neuron`;
7053
7511
  mediaUrl: post.media_url,
7054
7512
  scheduledAt: post.schedule_at,
7055
7513
  hashtags: post.hashtags,
7514
+ ...effectiveProjectId ? {
7515
+ projectId: effectiveProjectId,
7516
+ project_id: effectiveProjectId
7517
+ } : {},
7518
+ connectedAccountIds: {
7519
+ [normalizedPlatform]: verifiedPlanAccountIds[normalizedPlatform]
7520
+ },
7056
7521
  ...effectivePlanId ? { planId: effectivePlanId } : {},
7057
7522
  idempotencyKey
7058
7523
  },
@@ -7111,7 +7576,7 @@ Created with Social Neuron`;
7111
7576
  const platformBatches = Array.from(grouped.entries()).map(
7112
7577
  async ([platform3, platformPosts]) => {
7113
7578
  const platformResults = [];
7114
- const batches = chunk(platformPosts, batch_size);
7579
+ const batches = chunk(platformPosts, effectiveBatchSize);
7115
7580
  for (const batch of batches) {
7116
7581
  const settled = await Promise.allSettled(
7117
7582
  batch.map((post) => scheduleOne(post))
@@ -7172,7 +7637,8 @@ Created with Social Neuron`;
7172
7637
  total_posts: workingPlan.posts.length,
7173
7638
  scheduled,
7174
7639
  failed
7175
- }
7640
+ },
7641
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
7176
7642
  }),
7177
7643
  null,
7178
7644
  2
@@ -7196,6 +7662,9 @@ Created with Social Neuron`;
7196
7662
  lines.push(
7197
7663
  `Scheduled: ${scheduled}/${workingPlan.posts.length} | Failed: ${failed}/${workingPlan.posts.length}`
7198
7664
  );
7665
+ if (projectAutoResolvedNote) {
7666
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
7667
+ }
7199
7668
  return {
7200
7669
  content: [{ type: "text", text: lines.join("\n") }],
7201
7670
  isError: failed > 0
@@ -7215,7 +7684,7 @@ Created with Social Neuron`;
7215
7684
  }
7216
7685
  );
7217
7686
  }
7218
- var PLATFORM_CASE_MAP, TIKTOK_AUDIT_APPROVED, MCP_NOT_LIVE_FOR_POSTING, VIDEO_FILE_EXTENSIONS, IMAGE_FILE_EXTENSIONS;
7687
+ var PLATFORM_CASE_MAP2, TIKTOK_AUDIT_APPROVED, MCP_NOT_LIVE_FOR_POSTING, VIDEO_FILE_EXTENSIONS, IMAGE_FILE_EXTENSIONS;
7219
7688
  var init_distribution = __esm({
7220
7689
  "src/tools/distribution.ts"() {
7221
7690
  "use strict";
@@ -7226,11 +7695,17 @@ var init_distribution = __esm({
7226
7695
  init_supabase();
7227
7696
  init_quality();
7228
7697
  init_version();
7229
- PLATFORM_CASE_MAP = {
7698
+ init_connected_account_routing();
7699
+ PLATFORM_CASE_MAP2 = {
7230
7700
  youtube: "YouTube",
7231
7701
  tiktok: "TikTok",
7232
7702
  instagram: "Instagram",
7233
7703
  twitter: "Twitter",
7704
+ // 'x' is the platform's current branding but connected_account_routing.ts
7705
+ // (and the DB convention) still key on 'Twitter' — keep both aliases
7706
+ // resolving to the same case so schedule_content_plan's platform:'x' posts
7707
+ // don't fall through to an undefined binding (F8, 2026-07-15).
7708
+ x: "Twitter",
7234
7709
  linkedin: "LinkedIn",
7235
7710
  facebook: "Facebook",
7236
7711
  threads: "Threads",
@@ -7772,15 +8247,35 @@ function registerAnalyticsTools(server) {
7772
8247
  content_id: z5.string().uuid().optional().describe(
7773
8248
  "Filter to a specific content_history ID to see performance of one piece of content."
7774
8249
  ),
7775
- project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
8250
+ project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
7776
8251
  limit: z5.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
7777
8252
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7778
8253
  },
7779
- async ({ platform: platform3, days, content_id, project_id, limit, response_format }) => {
8254
+ async ({
8255
+ platform: platform3,
8256
+ days,
8257
+ content_id,
8258
+ project_id,
8259
+ limit,
8260
+ response_format
8261
+ }) => {
7780
8262
  const format = response_format ?? "text";
7781
8263
  const lookbackDays = days ?? 30;
7782
8264
  const maxPosts = limit ?? 20;
7783
- const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
8265
+ const projectResolution = await resolveProjectStrict(project_id);
8266
+ if (!projectResolution.projectId) {
8267
+ return {
8268
+ content: [
8269
+ {
8270
+ type: "text",
8271
+ text: projectResolution.error ?? "A project_id is required to fetch analytics. Configure an explicit project or use an API key scoped to exactly one project."
8272
+ }
8273
+ ],
8274
+ isError: true
8275
+ };
8276
+ }
8277
+ const resolvedProjectId = projectResolution.projectId;
8278
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
7784
8279
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
7785
8280
  action: "analytics",
7786
8281
  platform: platform3,
@@ -7791,11 +8286,17 @@ function registerAnalyticsTools(server) {
7791
8286
  limit: Math.min(maxPosts * 5, 100),
7792
8287
  latestOnly: true,
7793
8288
  contentId: content_id,
7794
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
8289
+ projectId: resolvedProjectId,
8290
+ project_id: resolvedProjectId
7795
8291
  });
7796
8292
  if (efError) {
7797
8293
  return {
7798
- content: [{ type: "text", text: `Failed to fetch analytics: ${efError}` }],
8294
+ content: [
8295
+ {
8296
+ type: "text",
8297
+ text: `Failed to fetch analytics: ${efError}`
8298
+ }
8299
+ ],
7799
8300
  isError: true
7800
8301
  };
7801
8302
  }
@@ -7815,7 +8316,8 @@ function registerAnalyticsTools(server) {
7815
8316
  totalViews: 0,
7816
8317
  totalEngagement: 0,
7817
8318
  postCount: 0,
7818
- posts: []
8319
+ posts: [],
8320
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
7819
8321
  });
7820
8322
  return {
7821
8323
  structuredContent,
@@ -7831,7 +8333,9 @@ function registerAnalyticsTools(server) {
7831
8333
  content: [
7832
8334
  {
7833
8335
  type: "text",
7834
- text: `No analytics data found for the last ${lookbackDays} days${platform3 ? ` on ${platform3}` : ""}.`
8336
+ text: `No analytics data found for the last ${lookbackDays} days${platform3 ? ` on ${platform3}` : ""}.` + (projectAutoResolvedNote ? `
8337
+
8338
+ Note: ${projectAutoResolvedNote}` : "")
7835
8339
  }
7836
8340
  ]
7837
8341
  };
@@ -7863,20 +8367,27 @@ function registerAnalyticsTools(server) {
7863
8367
  postCount: posts.length,
7864
8368
  posts
7865
8369
  };
7866
- return formatAnalytics(summary, lookbackDays, format);
8370
+ return formatAnalytics(
8371
+ summary,
8372
+ lookbackDays,
8373
+ format,
8374
+ projectAutoResolvedNote
8375
+ );
7867
8376
  }
7868
8377
  );
7869
8378
  server.tool(
7870
8379
  "refresh_platform_analytics",
7871
8380
  "Queue analytics refresh jobs for posts from the last 7 days in one project across its connected platforms. Call this before fetch_analytics if you need fresh data. Returns immediately \u2014 data updates asynchronously over the next 1-5 minutes.",
7872
8381
  {
7873
- project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
8382
+ project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
7874
8383
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7875
8384
  },
7876
8385
  async ({ project_id, response_format }) => {
7877
8386
  const format = response_format ?? "text";
7878
8387
  const userId = await getDefaultUserId();
7879
- const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
8388
+ const projectResolution = await resolveProjectStrict(project_id);
8389
+ const resolvedProjectId = projectResolution.projectId;
8390
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
7880
8391
  const rateLimit = checkRateLimit(
7881
8392
  "posting",
7882
8393
  `refresh_platform_analytics:${userId}:${resolvedProjectId ?? "default"}`
@@ -7892,25 +8403,48 @@ function registerAnalyticsTools(server) {
7892
8403
  isError: true
7893
8404
  };
7894
8405
  }
8406
+ if (!resolvedProjectId) {
8407
+ return {
8408
+ content: [
8409
+ {
8410
+ type: "text",
8411
+ text: projectResolution.error ?? "A project_id is required to refresh analytics. Configure an explicit project or use an API key scoped to exactly one project."
8412
+ }
8413
+ ],
8414
+ isError: true
8415
+ };
8416
+ }
7895
8417
  const { data, error } = await callEdgeFunction("fetch-analytics", {
7896
8418
  userId,
7897
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
8419
+ projectId: resolvedProjectId,
8420
+ project_id: resolvedProjectId
7898
8421
  });
7899
8422
  if (error) {
7900
8423
  return {
7901
- content: [{ type: "text", text: `Error refreshing analytics: ${error}` }],
8424
+ content: [
8425
+ {
8426
+ type: "text",
8427
+ text: `Error refreshing analytics: ${error}`
8428
+ }
8429
+ ],
7902
8430
  isError: true
7903
8431
  };
7904
8432
  }
7905
8433
  const result = data;
7906
8434
  if (!result.success) {
7907
8435
  return {
7908
- content: [{ type: "text", text: "Analytics refresh failed." }],
8436
+ content: [
8437
+ { type: "text", text: "Analytics refresh failed." }
8438
+ ],
7909
8439
  isError: true
7910
8440
  };
7911
8441
  }
7912
- const queued = (result.results ?? []).filter((r) => r.status === "queued").length;
7913
- const errored = (result.results ?? []).filter((r) => r.status === "error").length;
8442
+ const queued = (result.results ?? []).filter(
8443
+ (r) => r.status === "queued"
8444
+ ).length;
8445
+ const errored = (result.results ?? []).filter(
8446
+ (r) => r.status === "error"
8447
+ ).length;
7914
8448
  const lines = [
7915
8449
  `Analytics refresh triggered successfully.`,
7916
8450
  ` Posts processed: ${result.postsProcessed}`,
@@ -7919,13 +8453,17 @@ function registerAnalyticsTools(server) {
7919
8453
  if (errored > 0) {
7920
8454
  lines.push(` Errors: ${errored}`);
7921
8455
  }
8456
+ if (projectAutoResolvedNote) {
8457
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
8458
+ }
7922
8459
  if (format === "json") {
7923
8460
  const structuredContent = asEnvelope4({
7924
8461
  success: true,
7925
8462
  postsProcessed: result.postsProcessed,
7926
8463
  queued,
7927
8464
  errored,
7928
- projectId: resolvedProjectId ?? null
8465
+ projectId: resolvedProjectId ?? null,
8466
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
7929
8467
  });
7930
8468
  return {
7931
8469
  structuredContent,
@@ -7941,12 +8479,21 @@ function registerAnalyticsTools(server) {
7941
8479
  }
7942
8480
  );
7943
8481
  }
7944
- function formatAnalytics(summary, days, format) {
7945
- const structuredContent = asEnvelope4({ ...summary, days });
8482
+ function formatAnalytics(summary, days, format, projectAutoResolvedNote) {
8483
+ const structuredContent = asEnvelope4({
8484
+ ...summary,
8485
+ days,
8486
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
8487
+ });
7946
8488
  if (format === "json") {
7947
8489
  return {
7948
8490
  structuredContent,
7949
- content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }]
8491
+ content: [
8492
+ {
8493
+ type: "text",
8494
+ text: JSON.stringify(structuredContent, null, 2)
8495
+ }
8496
+ ]
7950
8497
  };
7951
8498
  }
7952
8499
  const lines = [
@@ -7970,6 +8517,9 @@ function formatAnalytics(summary, days, format) {
7970
8517
  lines.push(line);
7971
8518
  }
7972
8519
  }
8520
+ if (projectAutoResolvedNote) {
8521
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
8522
+ }
7973
8523
  return {
7974
8524
  structuredContent,
7975
8525
  content: [{ type: "text", text: lines.join("\n") }]
@@ -9399,15 +9949,64 @@ function registerYouTubeAnalyticsTools(server) {
9399
9949
  start_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Start date in YYYY-MM-DD format."),
9400
9950
  end_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("End date in YYYY-MM-DD format."),
9401
9951
  video_id: z10.string().optional().describe('YouTube video ID. Required when action is "video".'),
9402
- max_results: z10.number().min(1).max(50).optional().describe('Max videos to return for "topVideos" action. Defaults to 10.'),
9952
+ max_results: z10.number().min(1).max(50).optional().describe(
9953
+ 'Max videos to return for "topVideos" action. Defaults to 10.'
9954
+ ),
9955
+ connected_account_id: z10.string().uuid().optional().describe(
9956
+ "Exact YouTube connected-account ID from list_connections. Optional when exactly one active YouTube account is bound to the resolved project \u2014 auto-resolved. Required (with a clear list of candidates) when the project has multiple YouTube accounts."
9957
+ ),
9958
+ project_id: z10.string().uuid().optional().describe(
9959
+ "Exact brand/project ID. Defaults only when the authenticated user has one project."
9960
+ ),
9403
9961
  response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9404
9962
  },
9405
- async ({ action, start_date, end_date, video_id, max_results, response_format }) => {
9963
+ async ({
9964
+ action,
9965
+ start_date,
9966
+ end_date,
9967
+ video_id,
9968
+ max_results,
9969
+ connected_account_id,
9970
+ project_id,
9971
+ response_format
9972
+ }) => {
9406
9973
  const format = response_format ?? "text";
9407
9974
  if (action === "video" && !video_id) {
9408
9975
  return {
9409
9976
  content: [
9410
- { type: "text", text: 'Error: video_id is required when action is "video".' }
9977
+ {
9978
+ type: "text",
9979
+ text: 'Error: video_id is required when action is "video".'
9980
+ }
9981
+ ],
9982
+ isError: true
9983
+ };
9984
+ }
9985
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
9986
+ if (!resolvedProjectId) {
9987
+ return {
9988
+ content: [
9989
+ {
9990
+ type: "text",
9991
+ text: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
9992
+ }
9993
+ ],
9994
+ isError: true
9995
+ };
9996
+ }
9997
+ const routing = await resolveConnectedAccountRouting({
9998
+ projectId: resolvedProjectId,
9999
+ platforms: ["youtube"],
10000
+ requestedAccountIds: connected_account_id ? { youtube: connected_account_id } : void 0
10001
+ });
10002
+ const resolvedAccountId = routing.connectedAccountIds?.YouTube;
10003
+ if (routing.error || !resolvedAccountId) {
10004
+ return {
10005
+ content: [
10006
+ {
10007
+ type: "text",
10008
+ text: routing.error ?? "YouTube: exact connected-account routing could not be established."
10009
+ }
9411
10010
  ],
9412
10011
  isError: true
9413
10012
  };
@@ -9417,11 +10016,19 @@ function registerYouTubeAnalyticsTools(server) {
9417
10016
  startDate: start_date,
9418
10017
  endDate: end_date,
9419
10018
  videoId: video_id,
9420
- maxResults: max_results ?? 10
10019
+ maxResults: max_results ?? 10,
10020
+ projectId: resolvedProjectId,
10021
+ project_id: resolvedProjectId,
10022
+ connectedAccountId: resolvedAccountId
9421
10023
  });
9422
10024
  if (error) {
9423
10025
  return {
9424
- content: [{ type: "text", text: `YouTube Analytics error: ${error}` }],
10026
+ content: [
10027
+ {
10028
+ type: "text",
10029
+ text: `YouTube Analytics error: ${error}`
10030
+ }
10031
+ ],
9425
10032
  isError: true
9426
10033
  };
9427
10034
  }
@@ -9434,7 +10041,12 @@ function registerYouTubeAnalyticsTools(server) {
9434
10041
  {
9435
10042
  type: "text",
9436
10043
  text: JSON.stringify(
9437
- asEnvelope7({ action, startDate: start_date, endDate: end_date, analytics: a }),
10044
+ asEnvelope7({
10045
+ action,
10046
+ startDate: start_date,
10047
+ endDate: end_date,
10048
+ analytics: a
10049
+ }),
9438
10050
  null,
9439
10051
  2
9440
10052
  )
@@ -9461,7 +10073,10 @@ function registerYouTubeAnalyticsTools(server) {
9461
10073
  if (days.length === 0) {
9462
10074
  return {
9463
10075
  content: [
9464
- { type: "text", text: "No daily analytics data found for this period." }
10076
+ {
10077
+ type: "text",
10078
+ text: "No daily analytics data found for this period."
10079
+ }
9465
10080
  ]
9466
10081
  };
9467
10082
  }
@@ -9484,7 +10099,10 @@ function registerYouTubeAnalyticsTools(server) {
9484
10099
  ]
9485
10100
  };
9486
10101
  }
9487
- const lines = [`YouTube Daily Analytics (${start_date} to ${end_date}):`, ""];
10102
+ const lines = [
10103
+ `YouTube Daily Analytics (${start_date} to ${end_date}):`,
10104
+ ""
10105
+ ];
9488
10106
  for (const d of days) {
9489
10107
  lines.push(
9490
10108
  ` ${d.date}: ${d.views.toLocaleString()} views, ${d.watchTimeMinutes.toLocaleString()} min watch, +${d.subscribersGained} subs, ${d.likes} likes, ${d.comments} comments`
@@ -9531,7 +10149,12 @@ function registerYouTubeAnalyticsTools(server) {
9531
10149
  const videos = result.topVideos ?? [];
9532
10150
  if (videos.length === 0) {
9533
10151
  return {
9534
- content: [{ type: "text", text: "No top videos found for this period." }]
10152
+ content: [
10153
+ {
10154
+ type: "text",
10155
+ text: "No top videos found for this period."
10156
+ }
10157
+ ]
9535
10158
  };
9536
10159
  }
9537
10160
  if (format === "json") {
@@ -9553,7 +10176,10 @@ function registerYouTubeAnalyticsTools(server) {
9553
10176
  ]
9554
10177
  };
9555
10178
  }
9556
- const lines = [`Top ${videos.length} YouTube Videos (${start_date} to ${end_date}):`, ""];
10179
+ const lines = [
10180
+ `Top ${videos.length} YouTube Videos (${start_date} to ${end_date}):`,
10181
+ ""
10182
+ ];
9557
10183
  for (let i = 0; i < videos.length; i++) {
9558
10184
  const v = videos[i];
9559
10185
  lines.push(
@@ -9566,11 +10192,18 @@ function registerYouTubeAnalyticsTools(server) {
9566
10192
  }
9567
10193
  if (format === "json") {
9568
10194
  return {
9569
- content: [{ type: "text", text: JSON.stringify(asEnvelope7(result), null, 2) }]
10195
+ content: [
10196
+ {
10197
+ type: "text",
10198
+ text: JSON.stringify(asEnvelope7(result), null, 2)
10199
+ }
10200
+ ]
9570
10201
  };
9571
10202
  }
9572
10203
  return {
9573
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
10204
+ content: [
10205
+ { type: "text", text: JSON.stringify(result, null, 2) }
10206
+ ]
9574
10207
  };
9575
10208
  }
9576
10209
  );
@@ -9579,6 +10212,8 @@ var init_youtube_analytics = __esm({
9579
10212
  "src/tools/youtube-analytics.ts"() {
9580
10213
  "use strict";
9581
10214
  init_edge_function();
10215
+ init_supabase();
10216
+ init_connected_account_routing();
9582
10217
  init_version();
9583
10218
  }
9584
10219
  });
@@ -9594,6 +10229,29 @@ function asEnvelope8(data) {
9594
10229
  data
9595
10230
  };
9596
10231
  }
10232
+ async function exactYouTubeRoute(projectId, connectedAccountId) {
10233
+ const resolvedProjectId = projectId ?? await getDefaultProjectId() ?? void 0;
10234
+ if (!resolvedProjectId) {
10235
+ return {
10236
+ error: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
10237
+ };
10238
+ }
10239
+ const routing = await resolveConnectedAccountRouting({
10240
+ projectId: resolvedProjectId,
10241
+ platforms: ["youtube"],
10242
+ requestedAccountIds: connectedAccountId ? { youtube: connectedAccountId } : void 0
10243
+ });
10244
+ const resolvedAccountId = routing.connectedAccountIds?.YouTube;
10245
+ if (routing.error || !resolvedAccountId) {
10246
+ return {
10247
+ error: routing.error ?? "YouTube: exact connected-account routing could not be established."
10248
+ };
10249
+ }
10250
+ return {
10251
+ projectId: resolvedProjectId,
10252
+ connectedAccountId: resolvedAccountId
10253
+ };
10254
+ }
9597
10255
  function registerCommentsTools(server) {
9598
10256
  server.tool(
9599
10257
  "list_comments",
@@ -9606,19 +10264,42 @@ function registerCommentsTools(server) {
9606
10264
  page_token: z11.string().optional().describe(
9607
10265
  "Pagination cursor from previous list_comments response nextPageToken field. Omit for first page of results."
9608
10266
  ),
10267
+ connected_account_id: z11.string().uuid().optional().describe(
10268
+ "Exact YouTube connected-account ID from list_connections. Optional when exactly one active YouTube account is bound to the resolved project \u2014 auto-resolved. Required (with a clear list of candidates) when the project has multiple YouTube accounts."
10269
+ ),
10270
+ project_id: PROJECT_ID_SCHEMA,
9609
10271
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9610
10272
  },
9611
- async ({ video_id, max_results, page_token, response_format }) => {
10273
+ async ({
10274
+ video_id,
10275
+ max_results,
10276
+ page_token,
10277
+ connected_account_id,
10278
+ project_id,
10279
+ response_format
10280
+ }) => {
9612
10281
  const format = response_format ?? "text";
10282
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
10283
+ if ("error" in route) {
10284
+ return {
10285
+ content: [{ type: "text", text: route.error }],
10286
+ isError: true
10287
+ };
10288
+ }
9613
10289
  const { data, error } = await callEdgeFunction("youtube-comments", {
9614
10290
  action: "list",
9615
10291
  videoId: video_id,
9616
10292
  maxResults: max_results ?? 50,
9617
- pageToken: page_token
10293
+ pageToken: page_token,
10294
+ projectId: route.projectId,
10295
+ project_id: route.projectId,
10296
+ connectedAccountId: route.connectedAccountId
9618
10297
  });
9619
10298
  if (error) {
9620
10299
  return {
9621
- content: [{ type: "text", text: `Error listing comments: ${error}` }],
10300
+ content: [
10301
+ { type: "text", text: `Error listing comments: ${error}` }
10302
+ ],
9622
10303
  isError: true
9623
10304
  };
9624
10305
  }
@@ -9630,7 +10311,10 @@ function registerCommentsTools(server) {
9630
10311
  {
9631
10312
  type: "text",
9632
10313
  text: JSON.stringify(
9633
- asEnvelope8({ comments, nextPageToken: result.nextPageToken ?? null }),
10314
+ asEnvelope8({
10315
+ comments,
10316
+ nextPageToken: result.nextPageToken ?? null
10317
+ }),
9634
10318
  null,
9635
10319
  2
9636
10320
  )
@@ -9667,12 +10351,31 @@ function registerCommentsTools(server) {
9667
10351
  "reply_to_comment",
9668
10352
  "Reply to a YouTube comment. Get the parent_id from list_comments results. Reply appears as the authenticated channel. Use for community engagement after checking list_comments for questions or feedback.",
9669
10353
  {
9670
- parent_id: z11.string().describe("The ID of the parent comment to reply to (from list_comments)."),
10354
+ parent_id: z11.string().describe(
10355
+ "The ID of the parent comment to reply to (from list_comments)."
10356
+ ),
9671
10357
  text: z11.string().min(1).describe("The reply text."),
10358
+ connected_account_id: z11.string().uuid().optional().describe(
10359
+ "Exact YouTube connected-account ID from list_connections. Optional when exactly one active YouTube account is bound to the resolved project \u2014 auto-resolved. Required (with a clear list of candidates) when the project has multiple YouTube accounts."
10360
+ ),
10361
+ project_id: PROJECT_ID_SCHEMA,
9672
10362
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9673
10363
  },
9674
- async ({ parent_id, text, response_format }) => {
10364
+ async ({
10365
+ parent_id,
10366
+ text,
10367
+ connected_account_id,
10368
+ project_id,
10369
+ response_format
10370
+ }) => {
9675
10371
  const format = response_format ?? "text";
10372
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
10373
+ if ("error" in route) {
10374
+ return {
10375
+ content: [{ type: "text", text: route.error }],
10376
+ isError: true
10377
+ };
10378
+ }
9676
10379
  const userId = await getDefaultUserId();
9677
10380
  const rateLimit = checkRateLimit("posting", `reply_to_comment:${userId}`);
9678
10381
  if (!rateLimit.allowed) {
@@ -9689,18 +10392,31 @@ function registerCommentsTools(server) {
9689
10392
  const { data, error } = await callEdgeFunction("youtube-comments", {
9690
10393
  action: "reply",
9691
10394
  parentId: parent_id,
9692
- text
10395
+ text,
10396
+ projectId: route.projectId,
10397
+ project_id: route.projectId,
10398
+ connectedAccountId: route.connectedAccountId
9693
10399
  });
9694
10400
  if (error) {
9695
10401
  return {
9696
- content: [{ type: "text", text: `Error replying to comment: ${error}` }],
10402
+ content: [
10403
+ {
10404
+ type: "text",
10405
+ text: `Error replying to comment: ${error}`
10406
+ }
10407
+ ],
9697
10408
  isError: true
9698
10409
  };
9699
10410
  }
9700
10411
  const result = data;
9701
10412
  if (format === "json") {
9702
10413
  return {
9703
- content: [{ type: "text", text: JSON.stringify(asEnvelope8(result), null, 2) }]
10414
+ content: [
10415
+ {
10416
+ type: "text",
10417
+ text: JSON.stringify(asEnvelope8(result), null, 2)
10418
+ }
10419
+ ]
9704
10420
  };
9705
10421
  }
9706
10422
  return {
@@ -9721,10 +10437,27 @@ function registerCommentsTools(server) {
9721
10437
  {
9722
10438
  video_id: z11.string().describe("The YouTube video ID to comment on."),
9723
10439
  text: z11.string().min(1).describe("The comment text."),
10440
+ connected_account_id: z11.string().uuid().optional().describe(
10441
+ "Exact YouTube connected-account ID from list_connections. Optional when exactly one active YouTube account is bound to the resolved project \u2014 auto-resolved. Required (with a clear list of candidates) when the project has multiple YouTube accounts."
10442
+ ),
10443
+ project_id: PROJECT_ID_SCHEMA,
9724
10444
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9725
10445
  },
9726
- async ({ video_id, text, response_format }) => {
10446
+ async ({
10447
+ video_id,
10448
+ text,
10449
+ connected_account_id,
10450
+ project_id,
10451
+ response_format
10452
+ }) => {
9727
10453
  const format = response_format ?? "text";
10454
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
10455
+ if ("error" in route) {
10456
+ return {
10457
+ content: [{ type: "text", text: route.error }],
10458
+ isError: true
10459
+ };
10460
+ }
9728
10461
  const userId = await getDefaultUserId();
9729
10462
  const rateLimit = checkRateLimit("posting", `post_comment:${userId}`);
9730
10463
  if (!rateLimit.allowed) {
@@ -9741,18 +10474,28 @@ function registerCommentsTools(server) {
9741
10474
  const { data, error } = await callEdgeFunction("youtube-comments", {
9742
10475
  action: "post",
9743
10476
  videoId: video_id,
9744
- text
10477
+ text,
10478
+ projectId: route.projectId,
10479
+ project_id: route.projectId,
10480
+ connectedAccountId: route.connectedAccountId
9745
10481
  });
9746
10482
  if (error) {
9747
10483
  return {
9748
- content: [{ type: "text", text: `Error posting comment: ${error}` }],
10484
+ content: [
10485
+ { type: "text", text: `Error posting comment: ${error}` }
10486
+ ],
9749
10487
  isError: true
9750
10488
  };
9751
10489
  }
9752
10490
  const result = data;
9753
10491
  if (format === "json") {
9754
10492
  return {
9755
- content: [{ type: "text", text: JSON.stringify(asEnvelope8(result), null, 2) }]
10493
+ content: [
10494
+ {
10495
+ type: "text",
10496
+ text: JSON.stringify(asEnvelope8(result), null, 2)
10497
+ }
10498
+ ]
9756
10499
  };
9757
10500
  }
9758
10501
  return {
@@ -9773,10 +10516,27 @@ function registerCommentsTools(server) {
9773
10516
  {
9774
10517
  comment_id: z11.string().describe("The comment ID to moderate."),
9775
10518
  moderation_status: z11.enum(["published", "rejected"]).describe('"published" to approve, "rejected" to hide.'),
10519
+ connected_account_id: z11.string().uuid().optional().describe(
10520
+ "Exact YouTube connected-account ID from list_connections. Optional when exactly one active YouTube account is bound to the resolved project \u2014 auto-resolved. Required (with a clear list of candidates) when the project has multiple YouTube accounts."
10521
+ ),
10522
+ project_id: PROJECT_ID_SCHEMA,
9776
10523
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9777
10524
  },
9778
- async ({ comment_id, moderation_status, response_format }) => {
10525
+ async ({
10526
+ comment_id,
10527
+ moderation_status,
10528
+ connected_account_id,
10529
+ project_id,
10530
+ response_format
10531
+ }) => {
9779
10532
  const format = response_format ?? "text";
10533
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
10534
+ if ("error" in route) {
10535
+ return {
10536
+ content: [{ type: "text", text: route.error }],
10537
+ isError: true
10538
+ };
10539
+ }
9780
10540
  const userId = await getDefaultUserId();
9781
10541
  const rateLimit = checkRateLimit("posting", `moderate_comment:${userId}`);
9782
10542
  if (!rateLimit.allowed) {
@@ -9793,11 +10553,19 @@ function registerCommentsTools(server) {
9793
10553
  const { error } = await callEdgeFunction("youtube-comments", {
9794
10554
  action: "moderate",
9795
10555
  commentId: comment_id,
9796
- moderationStatus: moderation_status
10556
+ moderationStatus: moderation_status,
10557
+ projectId: route.projectId,
10558
+ project_id: route.projectId,
10559
+ connectedAccountId: route.connectedAccountId
9797
10560
  });
9798
10561
  if (error) {
9799
10562
  return {
9800
- content: [{ type: "text", text: `Error moderating comment: ${error}` }],
10563
+ content: [
10564
+ {
10565
+ type: "text",
10566
+ text: `Error moderating comment: ${error}`
10567
+ }
10568
+ ],
9801
10569
  isError: true
9802
10570
  };
9803
10571
  }
@@ -9834,10 +10602,26 @@ function registerCommentsTools(server) {
9834
10602
  "Delete a YouTube comment. Only works for comments owned by the authenticated channel.",
9835
10603
  {
9836
10604
  comment_id: z11.string().describe("The comment ID to delete."),
10605
+ connected_account_id: z11.string().uuid().optional().describe(
10606
+ "Exact YouTube connected-account ID from list_connections. Optional when exactly one active YouTube account is bound to the resolved project \u2014 auto-resolved. Required (with a clear list of candidates) when the project has multiple YouTube accounts."
10607
+ ),
10608
+ project_id: PROJECT_ID_SCHEMA,
9837
10609
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9838
10610
  },
9839
- async ({ comment_id, response_format }) => {
10611
+ async ({
10612
+ comment_id,
10613
+ connected_account_id,
10614
+ project_id,
10615
+ response_format
10616
+ }) => {
9840
10617
  const format = response_format ?? "text";
10618
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
10619
+ if ("error" in route) {
10620
+ return {
10621
+ content: [{ type: "text", text: route.error }],
10622
+ isError: true
10623
+ };
10624
+ }
9841
10625
  const userId = await getDefaultUserId();
9842
10626
  const rateLimit = checkRateLimit("posting", `delete_comment:${userId}`);
9843
10627
  if (!rateLimit.allowed) {
@@ -9853,11 +10637,16 @@ function registerCommentsTools(server) {
9853
10637
  }
9854
10638
  const { error } = await callEdgeFunction("youtube-comments", {
9855
10639
  action: "delete",
9856
- commentId: comment_id
10640
+ commentId: comment_id,
10641
+ projectId: route.projectId,
10642
+ project_id: route.projectId,
10643
+ connectedAccountId: route.connectedAccountId
9857
10644
  });
9858
10645
  if (error) {
9859
10646
  return {
9860
- content: [{ type: "text", text: `Error deleting comment: ${error}` }],
10647
+ content: [
10648
+ { type: "text", text: `Error deleting comment: ${error}` }
10649
+ ],
9861
10650
  isError: true
9862
10651
  };
9863
10652
  }
@@ -9866,24 +10655,38 @@ function registerCommentsTools(server) {
9866
10655
  content: [
9867
10656
  {
9868
10657
  type: "text",
9869
- text: JSON.stringify(asEnvelope8({ success: true, commentId: comment_id }), null, 2)
10658
+ text: JSON.stringify(
10659
+ asEnvelope8({ success: true, commentId: comment_id }),
10660
+ null,
10661
+ 2
10662
+ )
9870
10663
  }
9871
10664
  ]
9872
10665
  };
9873
10666
  }
9874
10667
  return {
9875
- content: [{ type: "text", text: `Comment ${comment_id} deleted successfully.` }]
10668
+ content: [
10669
+ {
10670
+ type: "text",
10671
+ text: `Comment ${comment_id} deleted successfully.`
10672
+ }
10673
+ ]
9876
10674
  };
9877
10675
  }
9878
10676
  );
9879
10677
  }
10678
+ var PROJECT_ID_SCHEMA;
9880
10679
  var init_comments = __esm({
9881
10680
  "src/tools/comments.ts"() {
9882
10681
  "use strict";
9883
10682
  init_edge_function();
9884
10683
  init_rate_limit();
9885
10684
  init_supabase();
10685
+ init_connected_account_routing();
9886
10686
  init_version();
10687
+ PROJECT_ID_SCHEMA = z11.string().uuid().optional().describe(
10688
+ "Exact brand/project ID. Defaults only when the authenticated user has one project."
10689
+ );
9887
10690
  }
9888
10691
  });
9889
10692
 
@@ -12876,7 +13679,10 @@ var init_discovery2 = __esm({
12876
13679
  import { z as z24 } from "zod";
12877
13680
  import { randomUUID as randomUUID3 } from "node:crypto";
12878
13681
  function asEnvelope20(data) {
12879
- return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
13682
+ return {
13683
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
13684
+ data
13685
+ };
12880
13686
  }
12881
13687
  function registerPipelineTools(server) {
12882
13688
  server.tool(
@@ -12899,7 +13705,10 @@ function registerPipelineTools(server) {
12899
13705
  action: "pipeline-readiness",
12900
13706
  platforms,
12901
13707
  estimated_posts,
12902
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
13708
+ ...resolvedProjectId ? {
13709
+ projectId: resolvedProjectId,
13710
+ project_id: resolvedProjectId
13711
+ } : {}
12903
13712
  },
12904
13713
  { timeoutMs: 15e3 }
12905
13714
  );
@@ -12917,16 +13726,24 @@ function registerPipelineTools(server) {
12917
13726
  const blockers = [];
12918
13727
  const warnings = [];
12919
13728
  if (!isUnlimited && credits < estimatedCost) {
12920
- blockers.push(`Insufficient credits: ${credits} available, ~${estimatedCost} needed`);
13729
+ blockers.push(
13730
+ `Insufficient credits: ${credits} available, ~${estimatedCost} needed`
13731
+ );
12921
13732
  }
12922
13733
  if (missingPlatforms.length > 0) {
12923
- blockers.push(`Missing connected accounts: ${missingPlatforms.join(", ")}`);
13734
+ blockers.push(
13735
+ `Missing connected accounts: ${missingPlatforms.join(", ")}`
13736
+ );
12924
13737
  }
12925
13738
  if (!hasBrand) {
12926
- warnings.push("No brand profile found. Content will use generic voice.");
13739
+ warnings.push(
13740
+ "No brand profile found. Content will use generic voice."
13741
+ );
12927
13742
  }
12928
13743
  if (pendingApprovals > 0) {
12929
- warnings.push(`${pendingApprovals} pending approval(s) from previous runs.`);
13744
+ warnings.push(
13745
+ `${pendingApprovals} pending approval(s) from previous runs.`
13746
+ );
12930
13747
  }
12931
13748
  if (!insightsFresh) {
12932
13749
  warnings.push(
@@ -12941,7 +13758,10 @@ function registerPipelineTools(server) {
12941
13758
  estimated_cost: estimatedCost,
12942
13759
  sufficient: credits >= estimatedCost
12943
13760
  },
12944
- connected_accounts: { platforms: connectedPlatforms, missing: missingPlatforms },
13761
+ connected_accounts: {
13762
+ platforms: connectedPlatforms,
13763
+ missing: missingPlatforms
13764
+ },
12945
13765
  brand_profile: { exists: hasBrand },
12946
13766
  pending_approvals: { count: pendingApprovals },
12947
13767
  insights_available: {
@@ -12955,11 +13775,18 @@ function registerPipelineTools(server) {
12955
13775
  };
12956
13776
  if (format === "json") {
12957
13777
  return {
12958
- content: [{ type: "text", text: JSON.stringify(asEnvelope20(result), null, 2) }]
13778
+ content: [
13779
+ {
13780
+ type: "text",
13781
+ text: JSON.stringify(asEnvelope20(result), null, 2)
13782
+ }
13783
+ ]
12959
13784
  };
12960
13785
  }
12961
13786
  const lines = [];
12962
- lines.push(`Pipeline Readiness: ${result.ready ? "READY" : "NOT READY"}`);
13787
+ lines.push(
13788
+ `Pipeline Readiness: ${result.ready ? "READY" : "NOT READY"}`
13789
+ );
12963
13790
  lines.push("=".repeat(40));
12964
13791
  lines.push(
12965
13792
  `Credits: ${credits} available, ~${estimatedCost} needed \u2014 ${credits >= estimatedCost ? "OK" : "BLOCKED"}`
@@ -12967,7 +13794,9 @@ function registerPipelineTools(server) {
12967
13794
  lines.push(
12968
13795
  `Accounts: ${connectedPlatforms.length} connected${missingPlatforms.length > 0 ? ` (missing: ${missingPlatforms.join(", ")})` : " \u2014 OK"}`
12969
13796
  );
12970
- lines.push(`Brand: ${hasBrand ? "OK" : "Missing (will use generic voice)"}`);
13797
+ lines.push(
13798
+ `Brand: ${hasBrand ? "OK" : "Missing (will use generic voice)"}`
13799
+ );
12971
13800
  lines.push(`Pending Approvals: ${pendingApprovals}`);
12972
13801
  lines.push(
12973
13802
  `Insights: ${insightsFresh ? "Fresh" : insightAge === null ? "None available" : `${insightAge} days old`}`
@@ -12986,7 +13815,12 @@ function registerPipelineTools(server) {
12986
13815
  } catch (err) {
12987
13816
  const message = sanitizeError(err);
12988
13817
  return {
12989
- content: [{ type: "text", text: `Readiness check failed: ${message}` }],
13818
+ content: [
13819
+ {
13820
+ type: "text",
13821
+ text: `Readiness check failed: ${message}`
13822
+ }
13823
+ ],
12990
13824
  isError: true
12991
13825
  };
12992
13826
  }
@@ -13000,6 +13834,9 @@ function registerPipelineTools(server) {
13000
13834
  topic: z24.string().optional().describe("Content topic (required if no source_url)"),
13001
13835
  source_url: z24.string().optional().describe("URL to extract content from"),
13002
13836
  platforms: z24.array(PLATFORM_ENUM2).min(1).describe("Target platforms"),
13837
+ account_ids: z24.record(z24.string(), z24.string().uuid()).optional().describe(
13838
+ "Exact connected-account ID per target platform. Required when a project has multiple accounts on one platform."
13839
+ ),
13003
13840
  days: z24.number().min(1).max(7).default(5).describe("Days to plan"),
13004
13841
  posts_per_day: z24.number().min(1).max(3).default(1).describe("Posts per platform per day"),
13005
13842
  approval_mode: z24.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe(
@@ -13021,6 +13858,7 @@ function registerPipelineTools(server) {
13021
13858
  topic,
13022
13859
  source_url,
13023
13860
  platforms,
13861
+ account_ids,
13024
13862
  days,
13025
13863
  posts_per_day,
13026
13864
  approval_mode,
@@ -13038,7 +13876,12 @@ function registerPipelineTools(server) {
13038
13876
  let creditsUsed = 0;
13039
13877
  if (!topic && !source_url) {
13040
13878
  return {
13041
- content: [{ type: "text", text: "Either topic or source_url is required." }],
13879
+ content: [
13880
+ {
13881
+ type: "text",
13882
+ text: "Either topic or source_url is required."
13883
+ }
13884
+ ],
13042
13885
  isError: true
13043
13886
  };
13044
13887
  }
@@ -13068,6 +13911,35 @@ function registerPipelineTools(server) {
13068
13911
  }
13069
13912
  try {
13070
13913
  const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
13914
+ if (schedulingRequested && !resolvedProjectId) {
13915
+ return {
13916
+ content: [
13917
+ {
13918
+ type: "text",
13919
+ text: "A project_id is required to schedule pipeline output. Configure an explicit project or use an API key scoped to exactly one project."
13920
+ }
13921
+ ],
13922
+ isError: true
13923
+ };
13924
+ }
13925
+ if (schedulingRequested) {
13926
+ const preflightRouting = await resolveConnectedAccountRouting({
13927
+ projectId: resolvedProjectId,
13928
+ platforms,
13929
+ requestedAccountIds: account_ids
13930
+ });
13931
+ if (preflightRouting.error || !preflightRouting.connectedAccountIds) {
13932
+ return {
13933
+ content: [
13934
+ {
13935
+ type: "text",
13936
+ text: `Cannot run publishing pipeline \u2014 ${preflightRouting.error ?? "exact connected-account routing could not be established."} (checked before any credits were spent.)`
13937
+ }
13938
+ ],
13939
+ isError: true
13940
+ };
13941
+ }
13942
+ }
13071
13943
  const estimatedCost = BASE_PLAN_CREDITS + (source_url ? SOURCE_EXTRACTION_CREDITS : 0);
13072
13944
  const { data: budgetData } = await callEdgeFunction(
13073
13945
  "mcp-data",
@@ -13121,7 +13993,13 @@ function registerPipelineTools(server) {
13121
13993
  "social-neuron-ai",
13122
13994
  {
13123
13995
  type: "generation",
13124
- prompt: buildPlanPrompt(resolvedTopic, platforms, days, posts_per_day, source_url),
13996
+ prompt: buildPlanPrompt(
13997
+ resolvedTopic,
13998
+ platforms,
13999
+ days,
14000
+ posts_per_day,
14001
+ source_url
14002
+ ),
13125
14003
  model: "gemini-2.5-flash",
13126
14004
  responseFormat: "json",
13127
14005
  ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
@@ -13129,7 +14007,10 @@ function registerPipelineTools(server) {
13129
14007
  { timeoutMs: 6e4 }
13130
14008
  );
13131
14009
  if (planError || !planData) {
13132
- errors.push({ stage: "planning", message: planError ?? "No AI response" });
14010
+ errors.push({
14011
+ stage: "planning",
14012
+ message: planError ?? "No AI response"
14013
+ });
13133
14014
  await callEdgeFunction(
13134
14015
  "mcp-data",
13135
14016
  {
@@ -13146,7 +14027,10 @@ function registerPipelineTools(server) {
13146
14027
  );
13147
14028
  return {
13148
14029
  content: [
13149
- { type: "text", text: `Planning failed: ${planError ?? "No AI response"}` }
14030
+ {
14031
+ type: "text",
14032
+ text: `Planning failed: ${planError ?? "No AI response"}`
14033
+ }
13150
14034
  ],
13151
14035
  isError: true
13152
14036
  };
@@ -13176,20 +14060,22 @@ function registerPipelineTools(server) {
13176
14060
  const postsArray = extractJsonArray(rawText);
13177
14061
  const requestedPlatformSet = new Set(platforms);
13178
14062
  const maxPosts = platforms.length * days * posts_per_day;
13179
- const parsedPosts = (postsArray ?? []).map((p) => ({
13180
- id: String(p.id ?? randomUUID3().slice(0, 8)),
13181
- day: Number(p.day ?? 1),
13182
- date: String(p.date ?? ""),
13183
- platform: String(p.platform ?? ""),
13184
- content_type: p.content_type ?? "caption",
13185
- caption: String(p.caption ?? ""),
13186
- title: p.title ? String(p.title) : void 0,
13187
- hashtags: Array.isArray(p.hashtags) ? p.hashtags.map(String) : void 0,
13188
- hook: String(p.hook ?? ""),
13189
- angle: String(p.angle ?? ""),
13190
- visual_direction: p.visual_direction ? String(p.visual_direction) : void 0,
13191
- media_type: p.media_type ? String(p.media_type) : void 0
13192
- }));
14063
+ const parsedPosts = (postsArray ?? []).map(
14064
+ (p) => ({
14065
+ id: String(p.id ?? randomUUID3().slice(0, 8)),
14066
+ day: Number(p.day ?? 1),
14067
+ date: String(p.date ?? ""),
14068
+ platform: String(p.platform ?? ""),
14069
+ content_type: p.content_type ?? "caption",
14070
+ caption: String(p.caption ?? ""),
14071
+ title: p.title ? String(p.title) : void 0,
14072
+ hashtags: Array.isArray(p.hashtags) ? p.hashtags.map(String) : void 0,
14073
+ hook: String(p.hook ?? ""),
14074
+ angle: String(p.angle ?? ""),
14075
+ visual_direction: p.visual_direction ? String(p.visual_direction) : void 0,
14076
+ media_type: p.media_type ? String(p.media_type) : void 0
14077
+ })
14078
+ );
13193
14079
  const platformFilteredPosts = parsedPosts.filter(
13194
14080
  (post) => requestedPlatformSet.has(post.platform)
13195
14081
  );
@@ -13272,7 +14158,10 @@ function registerPipelineTools(server) {
13272
14158
  estimated_credits: estimatedCost,
13273
14159
  generated_at: (/* @__PURE__ */ new Date()).toISOString()
13274
14160
  },
13275
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
14161
+ ...resolvedProjectId ? {
14162
+ projectId: resolvedProjectId,
14163
+ project_id: resolvedProjectId
14164
+ } : {}
13276
14165
  },
13277
14166
  { timeoutMs: 1e4 }
13278
14167
  );
@@ -13325,6 +14214,22 @@ function registerPipelineTools(server) {
13325
14214
  }
13326
14215
  }
13327
14216
  let postsScheduled = 0;
14217
+ const pipelineRouting = schedulingRequested && postsApproved > 0 ? await resolveConnectedAccountRouting({
14218
+ projectId: resolvedProjectId,
14219
+ platforms,
14220
+ requestedAccountIds: account_ids
14221
+ }) : void 0;
14222
+ if (pipelineRouting?.error || schedulingRequested && postsApproved > 0 && !pipelineRouting?.connectedAccountIds) {
14223
+ return {
14224
+ content: [
14225
+ {
14226
+ type: "text",
14227
+ text: `Cannot run publishing pipeline \u2014 ${pipelineRouting?.error ?? "exact connected-account routing could not be established."}`
14228
+ }
14229
+ ],
14230
+ isError: true
14231
+ };
14232
+ }
13328
14233
  if (!dry_run && !skipSet.has("schedule") && postsApproved > 0) {
13329
14234
  const approvedPosts = posts.filter((p) => p.status === "approved");
13330
14235
  const scheduleBase = /* @__PURE__ */ new Date();
@@ -13332,10 +14237,27 @@ function registerPipelineTools(server) {
13332
14237
  const scheduleBaseMs = scheduleBase.getTime();
13333
14238
  for (const post of approvedPosts) {
13334
14239
  if (creditsUsed >= creditLimit) {
13335
- errors.push({ stage: "schedule", message: "Credit limit reached" });
14240
+ errors.push({
14241
+ stage: "schedule",
14242
+ message: "Credit limit reached"
14243
+ });
13336
14244
  break;
13337
14245
  }
13338
- const scheduledAt = post.schedule_at ?? new Date(scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5).toISOString();
14246
+ const scheduledAt = post.schedule_at ?? new Date(
14247
+ scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5
14248
+ ).toISOString();
14249
+ const route = Object.entries(
14250
+ pipelineRouting.connectedAccountIds
14251
+ ).find(
14252
+ ([platform3]) => platform3.toLowerCase() === post.platform.toLowerCase()
14253
+ );
14254
+ if (!route) {
14255
+ errors.push({
14256
+ stage: "schedule",
14257
+ message: `No verified connected account route for ${post.platform}`
14258
+ });
14259
+ continue;
14260
+ }
13339
14261
  try {
13340
14262
  const { error: schedError } = await callEdgeFunction(
13341
14263
  "schedule-post",
@@ -13348,7 +14270,11 @@ function registerPipelineTools(server) {
13348
14270
  scheduledAt,
13349
14271
  planId,
13350
14272
  idempotencyKey: `pipeline-${planId}-${post.id}`,
13351
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
14273
+ ...resolvedProjectId ? {
14274
+ projectId: resolvedProjectId,
14275
+ project_id: resolvedProjectId
14276
+ } : {},
14277
+ connectedAccountIds: { [route[0]]: route[1] }
13352
14278
  },
13353
14279
  { timeoutMs: 15e3 }
13354
14280
  );
@@ -13414,12 +14340,17 @@ function registerPipelineTools(server) {
13414
14340
  if (response_format === "json") {
13415
14341
  return {
13416
14342
  content: [
13417
- { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
14343
+ {
14344
+ type: "text",
14345
+ text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
14346
+ }
13418
14347
  ]
13419
14348
  };
13420
14349
  }
13421
14350
  const lines = [];
13422
- lines.push(`Pipeline ${pipelineId.slice(0, 8)}... ${finalStatus.toUpperCase()}`);
14351
+ lines.push(
14352
+ `Pipeline ${pipelineId.slice(0, 8)}... ${finalStatus.toUpperCase()}`
14353
+ );
13423
14354
  lines.push("=".repeat(40));
13424
14355
  lines.push(`Posts generated: ${posts.length}`);
13425
14356
  lines.push(`Posts approved: ${postsApproved}`);
@@ -13458,7 +14389,9 @@ function registerPipelineTools(server) {
13458
14389
  } catch {
13459
14390
  }
13460
14391
  return {
13461
- content: [{ type: "text", text: `Pipeline failed: ${message}` }],
14392
+ content: [
14393
+ { type: "text", text: `Pipeline failed: ${message}` }
14394
+ ],
13462
14395
  isError: true
13463
14396
  };
13464
14397
  }
@@ -13495,7 +14428,12 @@ function registerPipelineTools(server) {
13495
14428
  }
13496
14429
  if (format === "json") {
13497
14430
  return {
13498
- content: [{ type: "text", text: JSON.stringify(asEnvelope20(data), null, 2) }]
14431
+ content: [
14432
+ {
14433
+ type: "text",
14434
+ text: JSON.stringify(asEnvelope20(data), null, 2)
14435
+ }
14436
+ ]
13499
14437
  };
13500
14438
  }
13501
14439
  const lines = [];
@@ -13535,10 +14473,19 @@ function registerPipelineTools(server) {
13535
14473
  },
13536
14474
  async ({ plan_id, quality_threshold, response_format }) => {
13537
14475
  try {
13538
- const { data: loadResult, error: loadError } = await callEdgeFunction("mcp-data", { action: "auto-approve-plan", plan_id }, { timeoutMs: 1e4 });
14476
+ const { data: loadResult, error: loadError } = await callEdgeFunction(
14477
+ "mcp-data",
14478
+ { action: "auto-approve-plan", plan_id },
14479
+ { timeoutMs: 1e4 }
14480
+ );
13539
14481
  if (loadError) {
13540
14482
  return {
13541
- content: [{ type: "text", text: `Failed to load plan: ${loadError}` }],
14483
+ content: [
14484
+ {
14485
+ type: "text",
14486
+ text: `Failed to load plan: ${loadError}`
14487
+ }
14488
+ ],
13542
14489
  isError: true
13543
14490
  };
13544
14491
  }
@@ -13546,7 +14493,10 @@ function registerPipelineTools(server) {
13546
14493
  if (!stored?.plan_payload) {
13547
14494
  return {
13548
14495
  content: [
13549
- { type: "text", text: `No content plan found for plan_id=${plan_id}` }
14496
+ {
14497
+ type: "text",
14498
+ text: `No content plan found for plan_id=${plan_id}`
14499
+ }
13550
14500
  ],
13551
14501
  isError: true
13552
14502
  };
@@ -13573,7 +14523,11 @@ function registerPipelineTools(server) {
13573
14523
  blockers: []
13574
14524
  };
13575
14525
  autoApproved++;
13576
- details.push({ post_id: post.id, action: "approved", score: quality.total });
14526
+ details.push({
14527
+ post_id: post.id,
14528
+ action: "approved",
14529
+ score: quality.total
14530
+ });
13577
14531
  } else if (quality.total >= quality_threshold - 5) {
13578
14532
  post.status = "needs_edit";
13579
14533
  post.quality = {
@@ -13583,7 +14537,11 @@ function registerPipelineTools(server) {
13583
14537
  blockers: quality.blockers
13584
14538
  };
13585
14539
  flagged++;
13586
- details.push({ post_id: post.id, action: "flagged", score: quality.total });
14540
+ details.push({
14541
+ post_id: post.id,
14542
+ action: "flagged",
14543
+ score: quality.total
14544
+ });
13587
14545
  } else {
13588
14546
  post.status = "rejected";
13589
14547
  post.quality = {
@@ -13593,7 +14551,11 @@ function registerPipelineTools(server) {
13593
14551
  blockers: quality.blockers
13594
14552
  };
13595
14553
  rejected++;
13596
- details.push({ post_id: post.id, action: "rejected", score: quality.total });
14554
+ details.push({
14555
+ post_id: post.id,
14556
+ action: "rejected",
14557
+ score: quality.total
14558
+ });
13597
14559
  }
13598
14560
  }
13599
14561
  const newStatus = flagged === 0 && rejected === 0 ? "approved" : "in_review";
@@ -13629,7 +14591,10 @@ function registerPipelineTools(server) {
13629
14591
  if (response_format === "json") {
13630
14592
  return {
13631
14593
  content: [
13632
- { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
14594
+ {
14595
+ type: "text",
14596
+ text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
14597
+ }
13633
14598
  ]
13634
14599
  };
13635
14600
  }
@@ -13650,7 +14615,9 @@ function registerPipelineTools(server) {
13650
14615
  } catch (err) {
13651
14616
  const message = sanitizeError(err);
13652
14617
  return {
13653
- content: [{ type: "text", text: `Auto-approve failed: ${message}` }],
14618
+ content: [
14619
+ { type: "text", text: `Auto-approve failed: ${message}` }
14620
+ ],
13654
14621
  isError: true
13655
14622
  };
13656
14623
  }
@@ -13684,6 +14651,7 @@ var init_pipeline = __esm({
13684
14651
  init_quality();
13685
14652
  init_version();
13686
14653
  init_parse_utils();
14654
+ init_connected_account_routing();
13687
14655
  PLATFORM_ENUM2 = z24.enum([
13688
14656
  "youtube",
13689
14657
  "tiktok",
@@ -16348,28 +17316,33 @@ var init_analytics_pulse = __esm({
16348
17316
 
16349
17317
  // src/tools/connections.ts
16350
17318
  import { z as z33 } from "zod";
16351
- function findActiveAccount(accounts, platform3) {
17319
+ function findActiveAccounts(accounts, platform3, projectId) {
16352
17320
  const target = platform3.toLowerCase();
16353
- return accounts.find((a) => {
17321
+ return accounts.filter((a) => {
16354
17322
  const effectiveStatus = a.effective_status || a.status;
16355
- return a.platform.toLowerCase() === target && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
16356
- }) ?? null;
17323
+ return a.platform.toLowerCase() === target && a.project_id === projectId && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
17324
+ });
16357
17325
  }
16358
17326
  function registerConnectionTools(server) {
16359
17327
  server.tool(
16360
17328
  "start_platform_connection",
16361
17329
  "Begin connecting a social platform (Instagram, TikTok, YouTube, etc.). Returns a single-use deep link the user opens in a browser to complete the one-time OAuth handshake on socialneuron.com. This is NOT another OAuth in Claude \u2014 platform connections require a browser session because the social platforms (Meta, Google, TikTok) only accept callbacks on socialneuron.com. After the user clicks the link and approves on the platform, call `wait_for_connection` to detect completion. Link expires in 2 minutes; mint a new one if needed. Use `list_connected_accounts` first to check whether the platform is already connected before calling this.",
16362
17330
  {
16363
- platform: z33.enum(PLATFORM_ENUM4).describe("Platform to connect. Lower-case: instagram, tiktok, youtube, etc."),
16364
- project_id: z33.string().optional().describe(
16365
- "Brand/project ID to bind the new social account to. Use this when connecting a brand-specific account."
17331
+ platform: z33.enum(PLATFORM_ENUM4).describe(
17332
+ "Platform to connect. Lower-case: instagram, tiktok, youtube, etc."
17333
+ ),
17334
+ project_id: z33.string().uuid().optional().describe(
17335
+ "Brand/project ID to bind the new social account to. Required when the account has multiple brands."
16366
17336
  ),
16367
17337
  response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
16368
17338
  },
16369
17339
  async ({ platform: platform3, project_id, response_format }) => {
16370
17340
  const format = response_format ?? "text";
16371
17341
  const userId = await getDefaultUserId();
16372
- const rl = checkRateLimit("posting", `start_platform_connection:${userId}`);
17342
+ const rl = checkRateLimit(
17343
+ "posting",
17344
+ `start_platform_connection:${userId}`
17345
+ );
16373
17346
  if (!rl.allowed) {
16374
17347
  return {
16375
17348
  content: [
@@ -16381,12 +17354,27 @@ function registerConnectionTools(server) {
16381
17354
  isError: true
16382
17355
  };
16383
17356
  }
17357
+ const projectResolution = await resolveProjectStrict(project_id);
17358
+ if (!projectResolution.projectId) {
17359
+ return {
17360
+ content: [
17361
+ {
17362
+ type: "text",
17363
+ text: projectResolution.error ?? "A project_id is required to connect a platform. Configure an explicit project or use an API key scoped to exactly one project."
17364
+ }
17365
+ ],
17366
+ isError: true
17367
+ };
17368
+ }
17369
+ const resolvedProjectId = projectResolution.projectId;
17370
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
16384
17371
  const { data, error } = await callEdgeFunction(
16385
17372
  "mcp-data",
16386
17373
  {
16387
17374
  action: "mint-connection-nonce",
16388
17375
  platform: platform3,
16389
- ...project_id ? { projectId: project_id, project_id } : {}
17376
+ projectId: resolvedProjectId,
17377
+ project_id: resolvedProjectId
16390
17378
  },
16391
17379
  { timeoutMs: 1e4 }
16392
17380
  );
@@ -16402,6 +17390,17 @@ function registerConnectionTools(server) {
16402
17390
  isError: true
16403
17391
  };
16404
17392
  }
17393
+ if (data.project_id !== resolvedProjectId) {
17394
+ return {
17395
+ content: [
17396
+ {
17397
+ type: "text",
17398
+ text: "Connection link project attestation failed. No OAuth link was returned."
17399
+ }
17400
+ ],
17401
+ isError: true
17402
+ };
17403
+ }
16405
17404
  if (format === "json") {
16406
17405
  return {
16407
17406
  content: [
@@ -16410,10 +17409,11 @@ function registerConnectionTools(server) {
16410
17409
  text: JSON.stringify(
16411
17410
  {
16412
17411
  platform: data.platform,
16413
- project_id: project_id ?? null,
17412
+ project_id: resolvedProjectId,
16414
17413
  deep_link: data.deep_link,
16415
17414
  expires_at: data.expires_at,
16416
- next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection."
17415
+ next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection.",
17416
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
16417
17417
  },
16418
17418
  null,
16419
17419
  2
@@ -16429,7 +17429,7 @@ function registerConnectionTools(server) {
16429
17429
  type: "text",
16430
17430
  text: [
16431
17431
  `${data.platform} connection ready.`,
16432
- ...project_id ? [`Brand/project: ${project_id}`] : [],
17432
+ `Brand/project: ${resolvedProjectId}`,
16433
17433
  "",
16434
17434
  'Ask the user to open this link in a browser and click "Connect" on the platform:',
16435
17435
  ` ${data.deep_link}`,
@@ -16437,7 +17437,8 @@ function registerConnectionTools(server) {
16437
17437
  `Link expires at: ${data.expires_at} (~2 minutes).`,
16438
17438
  "",
16439
17439
  "After they approve, call `wait_for_connection` with the same platform to confirm.",
16440
- "This is a one-time browser setup \u2014 not another OAuth flow inside Claude."
17440
+ "This is a one-time browser setup \u2014 not another OAuth flow inside Claude.",
17441
+ ...projectAutoResolvedNote ? ["", `Note: ${projectAutoResolvedNote}`] : []
16441
17442
  ].join("\n")
16442
17443
  }
16443
17444
  ],
@@ -16450,14 +17451,20 @@ function registerConnectionTools(server) {
16450
17451
  "Poll until a platform connection becomes active. Use after `start_platform_connection` while the user completes the browser OAuth flow. Returns when the account row appears with status=active, or when the timeout elapses. Default timeout 120s, max 600s.",
16451
17452
  {
16452
17453
  platform: z33.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
16453
- project_id: z33.string().optional().describe(
17454
+ project_id: z33.string().uuid().optional().describe(
16454
17455
  "Brand/project ID to scope the connection poll. Use the same project_id passed to start_platform_connection."
16455
17456
  ),
16456
17457
  timeout_s: z33.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
16457
17458
  poll_interval_s: z33.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
16458
17459
  response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
16459
17460
  },
16460
- async ({ platform: platform3, project_id, timeout_s, poll_interval_s, response_format }) => {
17461
+ async ({
17462
+ platform: platform3,
17463
+ project_id,
17464
+ timeout_s,
17465
+ poll_interval_s,
17466
+ response_format
17467
+ }) => {
16461
17468
  const format = response_format ?? "text";
16462
17469
  const startedAt = Date.now();
16463
17470
  const timeoutMs = (timeout_s ?? 120) * 1e3;
@@ -16476,6 +17483,18 @@ function registerConnectionTools(server) {
16476
17483
  isError: true
16477
17484
  };
16478
17485
  }
17486
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
17487
+ if (!resolvedProjectId) {
17488
+ return {
17489
+ content: [
17490
+ {
17491
+ type: "text",
17492
+ text: "A project_id is required to wait for a connection. Use the same project_id used to start the OAuth flow."
17493
+ }
17494
+ ],
17495
+ isError: true
17496
+ };
17497
+ }
16479
17498
  const activeWaits = inFlightWaitsByUser.get(userId) ?? 0;
16480
17499
  if (activeWaits >= MAX_CONCURRENT_WAITS_PER_USER) {
16481
17500
  return {
@@ -16497,12 +17516,62 @@ function registerConnectionTools(server) {
16497
17516
  "mcp-data",
16498
17517
  {
16499
17518
  action: "connected-accounts",
16500
- ...project_id ? { projectId: project_id, project_id } : {}
17519
+ projectId: resolvedProjectId,
17520
+ project_id: resolvedProjectId
16501
17521
  },
16502
17522
  { timeoutMs: 1e4 }
16503
17523
  );
16504
17524
  if (!error && data?.success) {
16505
- const found = findActiveAccount(data.accounts ?? [], platform3);
17525
+ const foundAccounts = findActiveAccounts(
17526
+ data.accounts ?? [],
17527
+ platform3,
17528
+ resolvedProjectId
17529
+ );
17530
+ if (foundAccounts.length > 1) {
17531
+ if (format === "json") {
17532
+ return {
17533
+ content: [
17534
+ {
17535
+ type: "text",
17536
+ text: JSON.stringify(
17537
+ {
17538
+ connected: true,
17539
+ platform: platform3,
17540
+ project_id: resolvedProjectId,
17541
+ accounts: foundAccounts.map((a) => ({
17542
+ id: a.id,
17543
+ username: a.username,
17544
+ connected_at: a.created_at
17545
+ })),
17546
+ attempts,
17547
+ message: `${platform3} has ${foundAccounts.length} active accounts for this project. Pass the exact account_id to schedule_post.`
17548
+ },
17549
+ null,
17550
+ 2
17551
+ )
17552
+ }
17553
+ ],
17554
+ isError: false
17555
+ };
17556
+ }
17557
+ return {
17558
+ content: [
17559
+ {
17560
+ type: "text",
17561
+ text: [
17562
+ `${platform3} is connected \u2014 ${foundAccounts.length} active accounts found for project ${resolvedProjectId}:`,
17563
+ ...foundAccounts.map(
17564
+ (a) => ` ${a.username || "(unnamed)"} (id=${a.id})`
17565
+ ),
17566
+ "",
17567
+ "Call schedule_post with the exact account_id (or account_ids) for the one you mean."
17568
+ ].join("\n")
17569
+ }
17570
+ ],
17571
+ isError: false
17572
+ };
17573
+ }
17574
+ const found = foundAccounts[0];
16506
17575
  if (found) {
16507
17576
  if (format === "json") {
16508
17577
  return {
@@ -16513,7 +17582,7 @@ function registerConnectionTools(server) {
16513
17582
  {
16514
17583
  connected: true,
16515
17584
  platform: found.platform,
16516
- project_id: found.project_id ?? project_id ?? null,
17585
+ project_id: resolvedProjectId,
16517
17586
  account_id: found.id,
16518
17587
  username: found.username,
16519
17588
  connected_at: found.created_at,
@@ -16533,7 +17602,7 @@ function registerConnectionTools(server) {
16533
17602
  type: "text",
16534
17603
  text: [
16535
17604
  `${found.platform} is connected.`,
16536
- ...found.project_id || project_id ? [`Brand/project: ${found.project_id ?? project_id}`] : [],
17605
+ `Brand/project: ${resolvedProjectId}`,
16537
17606
  `Account: ${found.username || "(unnamed)"} (id=${found.id})`,
16538
17607
  `Detected after ${attempts} poll(s) in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s.`,
16539
17608
  "Ready to call `schedule_post`."
@@ -16546,7 +17615,9 @@ function registerConnectionTools(server) {
16546
17615
  }
16547
17616
  const remaining = deadline - Date.now();
16548
17617
  if (remaining <= 0) break;
16549
- await new Promise((resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining)));
17618
+ await new Promise(
17619
+ (resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining))
17620
+ );
16550
17621
  }
16551
17622
  const message = `${platform3} did not connect within ${timeout_s ?? 120}s (${attempts} polls). The user may not have completed the browser OAuth yet, or the link expired. Mint a new link with \`start_platform_connection\` and try again, or have the user go directly to socialneuron.com/settings/connections.`;
16552
17623
  if (format === "json") {
@@ -16558,7 +17629,7 @@ function registerConnectionTools(server) {
16558
17629
  {
16559
17630
  connected: false,
16560
17631
  platform: platform3,
16561
- project_id: project_id ?? null,
17632
+ project_id: resolvedProjectId,
16562
17633
  attempts,
16563
17634
  timed_out: true,
16564
17635
  message