@socialneuron/mcp-server 1.8.2 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.1";
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,50 +408,91 @@ 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";
246
470
  }
247
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
+ );
491
+ }
492
+ throw new Error(
493
+ "Unable to verify absence of an existing Social Neuron Keychain credential. Unlock Keychain Access and retry."
494
+ );
495
+ }
248
496
  function linuxSecretRead(key) {
249
497
  try {
250
498
  const result = execFileSync(
@@ -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
 
@@ -858,269 +1235,66 @@ function classifyError(err) {
858
1235
  const lower = message.toLowerCase();
859
1236
  if (lower.includes("no authentication") || lower.includes("unauthorized") || lower.includes("api key invalid") || lower.includes("expired") || lower.includes("not logged in") || lower.includes("invalid api key") || lower.includes("invalid signature")) {
860
1237
  return {
861
- message,
862
- errorType: "AUTH",
863
- retryable: false,
864
- hint: "Run: npx @socialneuron/mcp-server login \u2014 Requires a paid plan (Starter+). See https://socialneuron.com/pricing"
865
- };
866
- }
867
- if (lower.includes("econnrefused") || lower.includes("etimedout") || lower.includes("fetch failed") || lower.includes("aborterror") || lower.includes("network") || lower.includes("dns") || lower.includes("temporary issue")) {
868
- return {
869
- message,
870
- errorType: "NETWORK",
871
- retryable: true,
872
- hint: "Check your network connection and try again."
873
- };
874
- }
875
- if (lower.includes("rate limit") || lower.includes("429") || lower.includes("too many requests")) {
876
- return {
877
- message,
878
- errorType: "RATE_LIMIT",
879
- 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;
1238
+ message,
1239
+ errorType: "AUTH",
1240
+ retryable: false,
1241
+ hint: "Run: npx @socialneuron/mcp-server login \u2014 Requires a paid plan (Starter+). See https://socialneuron.com/pricing"
1242
+ };
999
1243
  }
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
- }
1244
+ if (lower.includes("econnrefused") || lower.includes("etimedout") || lower.includes("fetch failed") || lower.includes("aborterror") || lower.includes("network") || lower.includes("dns") || lower.includes("temporary issue")) {
1245
+ return {
1246
+ message,
1247
+ errorType: "NETWORK",
1248
+ retryable: true,
1249
+ hint: "Check your network connection and try again."
1250
+ };
1010
1251
  }
1011
- let url;
1012
- let method = options?.method ?? "POST";
1013
- let headers;
1014
- let requestBody;
1015
- if (!apiKey) {
1016
- clearTimeout(timer);
1252
+ if (lower.includes("rate limit") || lower.includes("429") || lower.includes("too many requests")) {
1017
1253
  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"
1254
+ message,
1255
+ errorType: "RATE_LIMIT",
1256
+ retryable: true,
1257
+ hint: "Wait a moment and retry."
1020
1258
  };
1021
1259
  }
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";
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) {
1035
1269
  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
- }
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
 
@@ -4173,6 +4347,92 @@ var init_ideation = __esm({
4173
4347
  }
4174
4348
  });
4175
4349
 
4350
+ // src/lib/sanitize-error.ts
4351
+ function redactSensitiveIdentifiers(message) {
4352
+ return message.replace(EMAIL_PATTERN, "[redacted-email]").replace(UUID_PATTERN, "[redacted-id]");
4353
+ }
4354
+ function sanitizeError(error) {
4355
+ const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
4356
+ for (const [pattern, userMessage] of ERROR_PATTERNS) {
4357
+ if (pattern.test(msg)) {
4358
+ return userMessage;
4359
+ }
4360
+ }
4361
+ return "An unexpected error occurred. Please try again.";
4362
+ }
4363
+ var ERROR_PATTERNS, UUID_PATTERN, EMAIL_PATTERN;
4364
+ var init_sanitize_error = __esm({
4365
+ "src/lib/sanitize-error.ts"() {
4366
+ "use strict";
4367
+ ERROR_PATTERNS = [
4368
+ // Postgres / PostgREST
4369
+ [
4370
+ /PGRST301|permission denied/i,
4371
+ "Access denied. Check your account permissions."
4372
+ ],
4373
+ [
4374
+ /42P01|does not exist/i,
4375
+ "Service temporarily unavailable. Please try again."
4376
+ ],
4377
+ [
4378
+ /23505|unique.*constraint|duplicate key/i,
4379
+ "A duplicate record already exists."
4380
+ ],
4381
+ [/23503|foreign key/i, "Referenced record not found."],
4382
+ // Gemini / Google AI
4383
+ [
4384
+ /google.*api.*key|googleapis\.com.*40[13]/i,
4385
+ "Content generation failed. Please try again."
4386
+ ],
4387
+ [
4388
+ /RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
4389
+ "AI service rate limit reached. Please wait and retry."
4390
+ ],
4391
+ [
4392
+ /SAFETY|prompt.*blocked|content.*filter/i,
4393
+ "Content was blocked by the AI safety filter. Try rephrasing."
4394
+ ],
4395
+ [
4396
+ /gemini.*error|generativelanguage/i,
4397
+ "Content generation failed. Please try again."
4398
+ ],
4399
+ // Kie.ai
4400
+ [/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
4401
+ // Stripe
4402
+ [
4403
+ /stripe.*api|sk_live_|sk_test_/i,
4404
+ "Payment processing error. Please try again."
4405
+ ],
4406
+ // Network / fetch
4407
+ [
4408
+ /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
4409
+ "External service unavailable. Please try again."
4410
+ ],
4411
+ [
4412
+ /fetch failed|network error|abort.*timeout/i,
4413
+ "Network request failed. Please try again."
4414
+ ],
4415
+ [/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
4416
+ // Supabase Edge Function internals
4417
+ [
4418
+ /FunctionsHttpError|non-2xx status/i,
4419
+ "Backend service error. Please try again."
4420
+ ],
4421
+ [
4422
+ /JWT|token.*expired|token.*invalid/i,
4423
+ "Authentication expired. Please re-authenticate."
4424
+ ],
4425
+ // Generic sensitive patterns (API keys, URLs with secrets)
4426
+ [
4427
+ /[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i,
4428
+ "An internal error occurred. Please try again."
4429
+ ]
4430
+ ];
4431
+ UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
4432
+ EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
4433
+ }
4434
+ });
4435
+
4176
4436
  // src/lib/checkStatusShape.ts
4177
4437
  function buildCheckStatusPayload(job, liveStatus) {
4178
4438
  const status = liveStatus?.status ?? job.status;
@@ -4268,9 +4528,22 @@ function checkCreditBudget(estimatedCost) {
4268
4528
  }
4269
4529
  const used = getCreditsUsed();
4270
4530
  if (used + estimatedCost > MAX_CREDITS_PER_RUN) {
4531
+ const message = `Per-run credit cap reached \u2014 this is a local spend guard, not a server rate limit, so retrying will not help. The SOCIALNEURON_MAX_CREDITS_PER_RUN environment variable is set to ${MAX_CREDITS_PER_RUN} credits. This run has already spent ${used} credits and this call is estimated at ~${estimatedCost} more (${used} + ${estimatedCost} = ${used + estimatedCost} > ${MAX_CREDITS_PER_RUN}). To proceed, raise or unset SOCIALNEURON_MAX_CREDITS_PER_RUN.`;
4271
4532
  return {
4272
4533
  ok: false,
4273
- message: `Credit budget exceeded for this MCP run. Used=${used}, next~=${estimatedCost}, limit=${MAX_CREDITS_PER_RUN}.`
4534
+ message,
4535
+ error: toolError("billing_error", message, {
4536
+ recover_with: [
4537
+ "Raise SOCIALNEURON_MAX_CREDITS_PER_RUN to a higher credit ceiling.",
4538
+ "Unset SOCIALNEURON_MAX_CREDITS_PER_RUN to remove the per-run cap."
4539
+ ],
4540
+ details: {
4541
+ env_var: "SOCIALNEURON_MAX_CREDITS_PER_RUN",
4542
+ credits_used_this_run: used,
4543
+ estimated_call_cost: estimatedCost,
4544
+ max_credits_per_run: MAX_CREDITS_PER_RUN
4545
+ }
4546
+ })
4274
4547
  };
4275
4548
  }
4276
4549
  return { ok: true };
@@ -4293,8 +4566,15 @@ var init_budget = __esm({
4293
4566
  "src/lib/budget.ts"() {
4294
4567
  "use strict";
4295
4568
  init_request_context();
4296
- MAX_CREDITS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
4297
- MAX_ASSETS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0));
4569
+ init_tool_error();
4570
+ MAX_CREDITS_PER_RUN = Math.max(
4571
+ 0,
4572
+ Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0)
4573
+ );
4574
+ MAX_ASSETS_PER_RUN = Math.max(
4575
+ 0,
4576
+ Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0)
4577
+ );
4298
4578
  _globalCreditsUsed = 0;
4299
4579
  _globalAssetsGenerated = 0;
4300
4580
  }
@@ -4321,7 +4601,7 @@ function asEnvelope2(data) {
4321
4601
  function registerContentTools(server) {
4322
4602
  server.tool(
4323
4603
  "generate_video",
4324
- "Start an async AI video generation job \u2014 returns a job_id immediately. Poll with check_status every 10-30s until complete. Base credit costs (reference config; dynamic models scale with duration/audio/resolution): seedance-2-fast 264 (8s, best quality/credit) \xB7 kling-3 100 (5s no-audio) \xB7 grok-imagine 30 (6s, cheapest) \xB7 veo3-fast 65 (8s) \xB7 kling-3-pro 135 (5s no-audio) \xB7 seedance-2 328 (8s 720p) \xB7 veo3-quality 1000 (8s) \xB7 wan-2.6 105 (5s) \xB7 gemini-omni-video 126 (multi-reference composites) \xB7 hailuo-02-standard 180 / seedance-1.5-pro 150 / kling 170 (legacy/budget fallbacks). Costs are estimated pre-check and reconciled post-response from the actual charge. audio adds ~1.5x on kling-3/kling-3-pro and ~2.65x on kling (enable_audio defaults to FALSE). Check get_credit_balance first for expensive generations.",
4604
+ "Start an async AI video generation job \u2014 returns a job_id immediately. Poll with check_status every 10-30s until complete. Base credit costs (reference config; dynamic models scale with duration/audio/resolution): seedance-2-fast 264 (8s, best quality/credit) \xB7 kling-3 100 (5s no-audio) \xB7 grok-imagine 30 (6s, cheapest) \xB7 veo3-fast 65 (8s) \xB7 kling-3-pro 135 (5s no-audio) \xB7 seedance-2 328 (8s 720p) \xB7 veo3-quality 1000 (8s) \xB7 wan-2.6 105 (5s) \xB7 gemini-omni-video 126 (multi-reference composites) \xB7 hailuo-02-standard 180 / seedance-1.5-pro 150 / kling 170 (legacy/budget fallbacks). Costs are estimated pre-check and reconciled post-response from the actual charge. audio adds ~1.5x on kling-3/kling-3-pro and ~2.65x on kling; enable_audio defaults to FALSE EXCEPT the seedance-2 family (seedance-2-fast, seedance-2) where audio is ON by default and its cost is already included. Check get_credit_balance first for expensive generations.",
4325
4605
  {
4326
4606
  prompt: z2.string().max(2500).describe(
4327
4607
  'Video prompt \u2014 be specific about visual style, camera movement, lighting, and mood. Example: "Aerial drone shot of coastal cliffs at golden hour, slow dolly forward, cinematic 24fps, warm color grading." Vague prompts produce generic results.'
@@ -4336,7 +4616,7 @@ function registerContentTools(server) {
4336
4616
  "Video aspect ratio. 16:9 for YouTube/landscape, 9:16 for TikTok/Reels/Shorts, 1:1 for Instagram feed/square. Defaults to 16:9."
4337
4617
  ),
4338
4618
  enable_audio: z2.boolean().optional().describe(
4339
- "Enable native audio generation. DEFAULTS TO FALSE (cost control). Cost multiplier when true: kling 2.6 ~2.65x (17->45 cr/sec), kling-3 1.5x (20->30 cr/sec), kling-3-pro ~1.5x (27->40 cr/sec). seedance-2/-fast generate audio natively regardless. 5+ languages."
4619
+ "Enable native audio generation. For most models this DEFAULTS TO FALSE (cost control). EXCEPTION: the seedance-2 family (seedance-2-fast, seedance-2) DEFAULTS TO TRUE \u2014 these generate audio natively and their credit cost already includes it, so audio is on unless you explicitly pass false. Cost multiplier when true on other models: kling 2.6 ~2.65x (17->45 cr/sec), kling-3 1.5x (20->30 cr/sec), kling-3-pro ~1.5x (27->40 cr/sec). 5+ languages."
4340
4620
  ),
4341
4621
  image_url: z2.string().optional().describe(
4342
4622
  "Start frame image URL for image-to-video (Kling 3.0 frame control)."
@@ -4372,12 +4652,12 @@ function registerContentTools(server) {
4372
4652
  const estimatedCost = VIDEO_CREDIT_ESTIMATES[model] ?? 120;
4373
4653
  const budgetCheck = checkCreditBudget(estimatedCost);
4374
4654
  if (!budgetCheck.ok) {
4375
- return {
4376
- content: [{ type: "text", text: budgetCheck.message }],
4377
- isError: true
4378
- };
4655
+ return budgetCheck.error;
4379
4656
  }
4380
- const rateLimit = checkRateLimit("generation", `generate_video:${userId}`);
4657
+ const rateLimit = checkRateLimit(
4658
+ "generation",
4659
+ `generate_video:${userId}`
4660
+ );
4381
4661
  if (!rateLimit.allowed) {
4382
4662
  return {
4383
4663
  content: [
@@ -4396,9 +4676,12 @@ function registerContentTools(server) {
4396
4676
  model,
4397
4677
  duration: duration ?? 5,
4398
4678
  aspectRatio: aspect_ratio ?? "16:9",
4399
- // Default FALSE (2026-07-13): the old `?? true` default silently multiplied
4400
- // kling-family costs (2.65x on kling 2.6) for callers that never asked for audio.
4401
- enableAudio: enable_audio ?? false,
4679
+ // Default FALSE for most models (2026-07-13): the old `?? true` default silently
4680
+ // multiplied kling-family costs (2.65x on kling 2.6) for callers that never asked
4681
+ // for audio. Exception (2026-07-17): the seedance-2 family bills audio into its base
4682
+ // cost and generates it natively, so a false default there shipped silent no-audio
4683
+ // mp4s — those models default TRUE when the caller omits enable_audio.
4684
+ enableAudio: enable_audio ?? AUDIO_NATIVE_DEFAULT_MODELS.has(model),
4402
4685
  ...image_url && { imageUrl: image_url },
4403
4686
  ...end_frame_url && { endFrameUrl: end_frame_url },
4404
4687
  // The server reads projectId and enforces
@@ -4500,7 +4783,14 @@ function registerContentTools(server) {
4500
4783
  project_id: z2.string().optional().describe("Project ID to associate the generated image with."),
4501
4784
  response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
4502
4785
  },
4503
- async ({ prompt: prompt2, model, aspect_ratio, image_url, project_id, response_format }) => {
4786
+ async ({
4787
+ prompt: prompt2,
4788
+ model,
4789
+ aspect_ratio,
4790
+ image_url,
4791
+ project_id,
4792
+ response_format
4793
+ }) => {
4504
4794
  const format = response_format ?? "text";
4505
4795
  const userId = await getDefaultUserId();
4506
4796
  const assetBudget = checkAssetBudget();
@@ -4513,12 +4803,12 @@ function registerContentTools(server) {
4513
4803
  const estimatedCost = IMAGE_CREDIT_ESTIMATES[model] ?? 30;
4514
4804
  const budgetCheck = checkCreditBudget(estimatedCost);
4515
4805
  if (!budgetCheck.ok) {
4516
- return {
4517
- content: [{ type: "text", text: budgetCheck.message }],
4518
- isError: true
4519
- };
4806
+ return budgetCheck.error;
4520
4807
  }
4521
- const rateLimit = checkRateLimit("generation", `generate_image:${userId}`);
4808
+ const rateLimit = checkRateLimit(
4809
+ "generation",
4810
+ `generate_image:${userId}`
4811
+ );
4522
4812
  if (!rateLimit.allowed) {
4523
4813
  return {
4524
4814
  content: [
@@ -4649,6 +4939,15 @@ function registerContentTools(server) {
4649
4939
  isError: true
4650
4940
  };
4651
4941
  }
4942
+ let projectsDisclosure;
4943
+ const ownScopedProjectId = await getDefaultProjectId();
4944
+ if (!ownScopedProjectId) {
4945
+ const ownUserId = await getDefaultUserId().catch(() => null);
4946
+ if (ownUserId) {
4947
+ const list = await listAccessibleProjectsWithAccountStatus(ownUserId);
4948
+ if (list.length > 0) projectsDisclosure = list;
4949
+ }
4950
+ }
4652
4951
  if (job.external_id && (job.status === "pending" || job.status === "processing")) {
4653
4952
  const { data: liveStatus } = await callEdgeFunction(
4654
4953
  "kie-task-status",
@@ -4672,7 +4971,9 @@ function registerContentTools(server) {
4672
4971
  if (livePayload.error) {
4673
4972
  lines2.push(`Error: ${livePayload.error}`);
4674
4973
  }
4675
- const fallbackDisclosure2 = buildFallbackDisclosureLine(job.result_metadata);
4974
+ const fallbackDisclosure2 = buildFallbackDisclosureLine(
4975
+ job.result_metadata
4976
+ );
4676
4977
  if (fallbackDisclosure2) lines2.push(fallbackDisclosure2);
4677
4978
  lines2.push(`Credits: ${job.credits_cost}`);
4678
4979
  if (job.billing_status) {
@@ -4680,7 +4981,15 @@ function registerContentTools(server) {
4680
4981
  `Billing: ${job.billing_status} (charged ${job.credits_charged ?? 0}, refunded ${job.credits_refunded ?? 0})`
4681
4982
  );
4682
4983
  }
4683
- lines2.push(`Created: ${job.created_at}`);
4984
+ lines2.push(`Created: ${job.created_at}`);
4985
+ if (projectsDisclosure) {
4986
+ lines2.push(
4987
+ "",
4988
+ `Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
4989
+ (p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
4990
+ ).join("; ")}`
4991
+ );
4992
+ }
4684
4993
  if (format === "json") {
4685
4994
  return {
4686
4995
  content: [
@@ -4697,7 +5006,8 @@ function registerContentTools(server) {
4697
5006
  // `job` + `liveStatus`; do not duplicate them here.
4698
5007
  asEnvelope2({
4699
5008
  ...liveStatus,
4700
- ...livePayload
5009
+ ...livePayload,
5010
+ ...projectsDisclosure ? { projects: projectsDisclosure } : {}
4701
5011
  }),
4702
5012
  null,
4703
5013
  2
@@ -4738,9 +5048,11 @@ function registerContentTools(server) {
4738
5048
  );
4739
5049
  }
4740
5050
  if (job.error_message) {
4741
- lines.push(`Error: ${job.error_message}`);
5051
+ lines.push(`Error: ${redactSensitiveIdentifiers(job.error_message)}`);
4742
5052
  }
4743
- const fallbackDisclosure = buildFallbackDisclosureLine(job.result_metadata);
5053
+ const fallbackDisclosure = buildFallbackDisclosureLine(
5054
+ job.result_metadata
5055
+ );
4744
5056
  if (fallbackDisclosure) lines.push(fallbackDisclosure);
4745
5057
  lines.push(`Credits: ${job.credits_cost}`);
4746
5058
  if (job.billing_status) {
@@ -4752,10 +5064,19 @@ function registerContentTools(server) {
4752
5064
  if (job.completed_at) {
4753
5065
  lines.push(`Completed: ${job.completed_at}`);
4754
5066
  }
5067
+ if (projectsDisclosure) {
5068
+ lines.push(
5069
+ "",
5070
+ `Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
5071
+ (p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
5072
+ ).join("; ")}`
5073
+ );
5074
+ }
4755
5075
  if (format === "json") {
4756
5076
  const enriched = {
4757
5077
  ...job,
4758
- ...buildCheckStatusPayload(job)
5078
+ ...buildCheckStatusPayload(job),
5079
+ ...projectsDisclosure ? { projects: projectsDisclosure } : {}
4759
5080
  };
4760
5081
  return {
4761
5082
  content: [
@@ -4881,10 +5202,7 @@ Return ONLY valid JSON in this exact format:
4881
5202
  const estimatedCost = 10;
4882
5203
  const budgetCheck = checkCreditBudget(estimatedCost);
4883
5204
  if (!budgetCheck.ok) {
4884
- return {
4885
- content: [{ type: "text", text: budgetCheck.message }],
4886
- isError: true
4887
- };
5205
+ return budgetCheck.error;
4888
5206
  }
4889
5207
  const { data, error } = await callEdgeFunction(
4890
5208
  "social-neuron-ai",
@@ -4971,10 +5289,7 @@ Return ONLY valid JSON in this exact format:
4971
5289
  const estimatedCost = 15;
4972
5290
  const budgetCheck = checkCreditBudget(estimatedCost);
4973
5291
  if (!budgetCheck.ok) {
4974
- return {
4975
- content: [{ type: "text", text: budgetCheck.message }],
4976
- isError: true
4977
- };
5292
+ return budgetCheck.error;
4978
5293
  }
4979
5294
  const rateLimit = checkRateLimit(
4980
5295
  "generation",
@@ -5137,10 +5452,7 @@ Return ONLY valid JSON in this exact format:
5137
5452
  const estimatedCost = 10 + slideCount * 2;
5138
5453
  const budgetCheck = checkCreditBudget(estimatedCost);
5139
5454
  if (!budgetCheck.ok) {
5140
- return {
5141
- content: [{ type: "text", text: budgetCheck.message }],
5142
- isError: true
5143
- };
5455
+ return budgetCheck.error;
5144
5456
  }
5145
5457
  const userId = await getDefaultUserId();
5146
5458
  const rateLimit = checkRateLimit(
@@ -5243,13 +5555,14 @@ Return ONLY valid JSON in this exact format:
5243
5555
  }
5244
5556
  );
5245
5557
  }
5246
- var VIDEO_CREDIT_ESTIMATES, VIDEO_MODEL_ENUM, IMAGE_CREDIT_ESTIMATES;
5558
+ var VIDEO_CREDIT_ESTIMATES, AUDIO_NATIVE_DEFAULT_MODELS, VIDEO_MODEL_ENUM, IMAGE_CREDIT_ESTIMATES;
5247
5559
  var init_content2 = __esm({
5248
5560
  "src/tools/content.ts"() {
5249
5561
  "use strict";
5250
5562
  init_edge_function();
5251
5563
  init_rate_limit();
5252
5564
  init_supabase();
5565
+ init_sanitize_error();
5253
5566
  init_version();
5254
5567
  init_checkStatusShape();
5255
5568
  init_budget();
@@ -5267,6 +5580,10 @@ var init_content2 = __esm({
5267
5580
  "seedance-1.5-pro": 150,
5268
5581
  kling: 170
5269
5582
  };
5583
+ AUDIO_NATIVE_DEFAULT_MODELS = /* @__PURE__ */ new Set([
5584
+ "seedance-2-fast",
5585
+ "seedance-2"
5586
+ ]);
5270
5587
  VIDEO_MODEL_ENUM = [
5271
5588
  "seedance-2-fast",
5272
5589
  "kling-3",
@@ -5295,57 +5612,6 @@ var init_content2 = __esm({
5295
5612
  }
5296
5613
  });
5297
5614
 
5298
- // src/lib/sanitize-error.ts
5299
- function sanitizeError(error) {
5300
- const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
5301
- for (const [pattern, userMessage] of ERROR_PATTERNS) {
5302
- if (pattern.test(msg)) {
5303
- return userMessage;
5304
- }
5305
- }
5306
- return "An unexpected error occurred. Please try again.";
5307
- }
5308
- var ERROR_PATTERNS;
5309
- var init_sanitize_error = __esm({
5310
- "src/lib/sanitize-error.ts"() {
5311
- "use strict";
5312
- ERROR_PATTERNS = [
5313
- // Postgres / PostgREST
5314
- [/PGRST301|permission denied/i, "Access denied. Check your account permissions."],
5315
- [/42P01|does not exist/i, "Service temporarily unavailable. Please try again."],
5316
- [/23505|unique.*constraint|duplicate key/i, "A duplicate record already exists."],
5317
- [/23503|foreign key/i, "Referenced record not found."],
5318
- // Gemini / Google AI
5319
- [/google.*api.*key|googleapis\.com.*40[13]/i, "Content generation failed. Please try again."],
5320
- [
5321
- /RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
5322
- "AI service rate limit reached. Please wait and retry."
5323
- ],
5324
- [
5325
- /SAFETY|prompt.*blocked|content.*filter/i,
5326
- "Content was blocked by the AI safety filter. Try rephrasing."
5327
- ],
5328
- [/gemini.*error|generativelanguage/i, "Content generation failed. Please try again."],
5329
- // Kie.ai
5330
- [/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
5331
- // Stripe
5332
- [/stripe.*api|sk_live_|sk_test_/i, "Payment processing error. Please try again."],
5333
- // Network / fetch
5334
- [
5335
- /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
5336
- "External service unavailable. Please try again."
5337
- ],
5338
- [/fetch failed|network error|abort.*timeout/i, "Network request failed. Please try again."],
5339
- [/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
5340
- // Supabase Edge Function internals
5341
- [/FunctionsHttpError|non-2xx status/i, "Backend service error. Please try again."],
5342
- [/JWT|token.*expired|token.*invalid/i, "Authentication expired. Please re-authenticate."],
5343
- // Generic sensitive patterns (API keys, URLs with secrets)
5344
- [/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i, "An internal error occurred. Please try again."]
5345
- ];
5346
- }
5347
- });
5348
-
5349
5615
  // src/lib/ssrf.ts
5350
5616
  import { promises as dnsPromises } from "node:dns";
5351
5617
  function isBlockedIP(ip) {
@@ -5499,6 +5765,122 @@ var init_ssrf = __esm({
5499
5765
  }
5500
5766
  });
5501
5767
 
5768
+ // src/lib/connected-account-routing.ts
5769
+ function canonicalPlatform(value) {
5770
+ const normalized = value.trim().toLowerCase();
5771
+ return normalized === "x" ? "twitter" : normalized;
5772
+ }
5773
+ function providerPlatform(value) {
5774
+ return PLATFORM_CASE_MAP[canonicalPlatform(value)] ?? value;
5775
+ }
5776
+ function isUsable(account) {
5777
+ const status = account.effective_status ?? account.status;
5778
+ return status === "active" || status === "expires_soon";
5779
+ }
5780
+ function normalizeRequestedIds(requested) {
5781
+ const ids = /* @__PURE__ */ new Map();
5782
+ for (const [platform3, accountId] of Object.entries(requested ?? {})) {
5783
+ const canonical = canonicalPlatform(platform3);
5784
+ const existing = ids.get(canonical);
5785
+ if (existing && existing !== accountId) {
5786
+ return {
5787
+ error: `Conflicting account IDs were supplied for ${platform3} and its platform alias.`
5788
+ };
5789
+ }
5790
+ ids.set(canonical, accountId);
5791
+ }
5792
+ return { ids };
5793
+ }
5794
+ async function resolveConnectedAccountRouting(input) {
5795
+ const normalizedRequested = normalizeRequestedIds(input.requestedAccountIds);
5796
+ if (normalizedRequested.error) return { error: normalizedRequested.error };
5797
+ const targetPlatforms = new Set(input.platforms.map(canonicalPlatform));
5798
+ for (const requestedPlatform of normalizedRequested.ids?.keys() ?? []) {
5799
+ if (!targetPlatforms.has(requestedPlatform)) {
5800
+ return {
5801
+ error: `An account ID was supplied for untargeted platform ${requestedPlatform}.`
5802
+ };
5803
+ }
5804
+ }
5805
+ const { data, error } = await callEdgeFunction(
5806
+ "mcp-data",
5807
+ {
5808
+ action: "connected-accounts",
5809
+ projectId: input.projectId,
5810
+ project_id: input.projectId
5811
+ },
5812
+ { timeoutMs: 1e4 }
5813
+ );
5814
+ if (error || !Array.isArray(data?.accounts)) {
5815
+ return {
5816
+ error: `Connected-account verification failed: ${error ?? data?.error ?? "no account inventory returned"}.`
5817
+ };
5818
+ }
5819
+ const connectedAccountIds = {};
5820
+ for (const platform3 of input.platforms) {
5821
+ const canonical = canonicalPlatform(platform3);
5822
+ const displayPlatform = providerPlatform(platform3);
5823
+ const platformAccounts = data.accounts.filter(
5824
+ (account) => canonicalPlatform(account.platform) === canonical && account.project_id === input.projectId && isUsable(account)
5825
+ );
5826
+ const requestedId = normalizedRequested.ids?.get(canonical);
5827
+ let selected;
5828
+ if (requestedId) {
5829
+ selected = data.accounts.find((account) => account.id === requestedId);
5830
+ if (!selected) {
5831
+ return {
5832
+ error: `${displayPlatform}: account ${requestedId} is not available for project_id ${input.projectId}.`
5833
+ };
5834
+ }
5835
+ if (canonicalPlatform(selected.platform) !== canonical) {
5836
+ return {
5837
+ error: `${displayPlatform}: account ${requestedId} belongs to ${selected.platform}.`
5838
+ };
5839
+ }
5840
+ if (selected.project_id !== input.projectId) {
5841
+ return {
5842
+ error: `${displayPlatform}: account ${requestedId} is not bound to project_id ${input.projectId}.`
5843
+ };
5844
+ }
5845
+ if (!isUsable(selected)) {
5846
+ return {
5847
+ error: `${displayPlatform}: account ${requestedId} is ${selected.effective_status ?? selected.status}.`
5848
+ };
5849
+ }
5850
+ } else if (platformAccounts.length === 1) {
5851
+ selected = platformAccounts[0];
5852
+ } else if (platformAccounts.length === 0) {
5853
+ return {
5854
+ error: `${displayPlatform}: no active account is bound to project_id ${input.projectId}.`
5855
+ };
5856
+ } else {
5857
+ return {
5858
+ error: `${displayPlatform}: multiple active accounts are bound to project_id ${input.projectId}; pass the exact account ID returned by list_connected_accounts.`
5859
+ };
5860
+ }
5861
+ connectedAccountIds[displayPlatform] = selected.id;
5862
+ }
5863
+ return { connectedAccountIds };
5864
+ }
5865
+ var PLATFORM_CASE_MAP;
5866
+ var init_connected_account_routing = __esm({
5867
+ "src/lib/connected-account-routing.ts"() {
5868
+ "use strict";
5869
+ init_edge_function();
5870
+ PLATFORM_CASE_MAP = {
5871
+ youtube: "YouTube",
5872
+ tiktok: "TikTok",
5873
+ instagram: "Instagram",
5874
+ twitter: "Twitter",
5875
+ x: "Twitter",
5876
+ linkedin: "LinkedIn",
5877
+ facebook: "Facebook",
5878
+ threads: "Threads",
5879
+ bluesky: "Bluesky"
5880
+ };
5881
+ }
5882
+ });
5883
+
5502
5884
  // src/tools/distribution.ts
5503
5885
  import { z as z3 } from "zod";
5504
5886
  import { createHash as createHash2 } from "node:crypto";
@@ -5611,21 +5993,6 @@ async function validatePublishMediaUrl(url) {
5611
5993
  function accountEffectiveStatus(account) {
5612
5994
  return account.effective_status || account.status;
5613
5995
  }
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
5996
  async function rehostExternalUrl(mediaUrl, projectId) {
5630
5997
  const ssrf = await validateUrlForSSRF(mediaUrl);
5631
5998
  if (!ssrf.isValid) {
@@ -5761,17 +6128,17 @@ function registerDistributionTools(server) {
5761
6128
  schedule_at: z3.string().optional().describe(
5762
6129
  'ISO 8601 UTC datetime for scheduled posting (e.g. "2026-03-20T14:00:00Z"). Omit to post immediately. Must be in the future.'
5763
6130
  ),
5764
- project_id: z3.string().optional().describe(
6131
+ project_id: z3.string().uuid().optional().describe(
5765
6132
  "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
6133
  ),
5767
6134
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
5768
6135
  attribution: z3.boolean().optional().describe(
5769
6136
  'If true, appends "Created with Social Neuron" to the caption. Default: false.'
5770
6137
  ),
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."
6138
+ account_id: z3.string().uuid().optional().describe(
6139
+ "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
6140
  ),
5774
- account_ids: z3.record(z3.string(), z3.string()).optional().describe(
6141
+ account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
5775
6142
  '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
6143
  ),
5777
6144
  auto_rehost: z3.boolean().optional().describe(
@@ -5815,6 +6182,45 @@ function registerDistributionTools(server) {
5815
6182
  isError: true
5816
6183
  };
5817
6184
  }
6185
+ const projectResolution = await resolveProjectForConnectedAccountTool(
6186
+ project_id,
6187
+ platforms
6188
+ );
6189
+ if (!projectResolution.projectId) {
6190
+ return {
6191
+ content: [
6192
+ {
6193
+ type: "text",
6194
+ 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."
6195
+ }
6196
+ ],
6197
+ isError: true
6198
+ };
6199
+ }
6200
+ const resolvedProjectId = projectResolution.projectId;
6201
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
6202
+ if (account_id && account_ids) {
6203
+ return {
6204
+ content: [
6205
+ {
6206
+ type: "text",
6207
+ text: "Pass either account_id or account_ids, not both."
6208
+ }
6209
+ ],
6210
+ isError: true
6211
+ };
6212
+ }
6213
+ if (account_id && platforms.length !== 1) {
6214
+ return {
6215
+ content: [
6216
+ {
6217
+ type: "text",
6218
+ text: "account_id is valid only for a single target platform. Use account_ids for multi-platform publishing."
6219
+ }
6220
+ ],
6221
+ isError: true
6222
+ };
6223
+ }
5818
6224
  const userId = await getDefaultUserId();
5819
6225
  const rateLimit = checkRateLimit("posting", `schedule_post:${userId}`);
5820
6226
  if (!rateLimit.allowed) {
@@ -5919,11 +6325,16 @@ function registerDistributionTools(server) {
5919
6325
  }
5920
6326
  const resolvedJobs = resolved;
5921
6327
  resolvedMediaUrls = resolvedJobs.map((item) => item.url);
5922
- resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map((item) => item.trustedR2);
6328
+ resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map(
6329
+ (item) => item.trustedR2
6330
+ );
5923
6331
  }
5924
6332
  const shouldRehost = auto_rehost !== false;
5925
6333
  if (shouldRehost && resolvedMediaUrl && !resolvedMediaUrlIsTrustedR2) {
5926
- const rehost = await rehostExternalUrl(resolvedMediaUrl, project_id);
6334
+ const rehost = await rehostExternalUrl(
6335
+ resolvedMediaUrl,
6336
+ resolvedProjectId
6337
+ );
5927
6338
  if ("error" in rehost) {
5928
6339
  return {
5929
6340
  content: [
@@ -5941,7 +6352,7 @@ function registerDistributionTools(server) {
5941
6352
  if (shouldRehost && resolvedMediaUrls && resolvedMediaUrls.length > 0) {
5942
6353
  const rehosted = await Promise.all(
5943
6354
  resolvedMediaUrls.map(
5944
- (u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, project_id)
6355
+ (u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, resolvedProjectId)
5945
6356
  )
5946
6357
  );
5947
6358
  const failIdx = rehosted.findIndex((r) => "error" in r);
@@ -6008,7 +6419,7 @@ function registerDistributionTools(server) {
6008
6419
  }
6009
6420
  }
6010
6421
  const normalizedPlatforms = platforms.map(
6011
- (p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
6422
+ (p) => PLATFORM_CASE_MAP2[p.toLowerCase()] || p
6012
6423
  );
6013
6424
  const blockedPlatforms = normalizedPlatforms.filter(
6014
6425
  (p) => MCP_NOT_LIVE_FOR_POSTING.has(p)
@@ -6024,92 +6435,27 @@ function registerDistributionTools(server) {
6024
6435
  isError: true
6025
6436
  };
6026
6437
  }
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;
6438
+ let requestedAccountIds;
6102
6439
  if (account_id) {
6103
- connectedAccountIds = {};
6104
- for (const p of normalizedPlatforms) {
6105
- connectedAccountIds[p] = account_id;
6106
- }
6440
+ requestedAccountIds = { [normalizedPlatforms[0]]: account_id };
6107
6441
  } 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
- }
6442
+ requestedAccountIds = account_ids;
6443
+ }
6444
+ const routing = await resolveConnectedAccountRouting({
6445
+ projectId: resolvedProjectId,
6446
+ platforms: normalizedPlatforms,
6447
+ requestedAccountIds
6448
+ });
6449
+ if (routing.error || !routing.connectedAccountIds) {
6450
+ return {
6451
+ content: [
6452
+ {
6453
+ type: "text",
6454
+ text: `Cannot post \u2014 ${routing.error ?? "exact connected-account routing could not be established."}`
6455
+ }
6456
+ ],
6457
+ isError: true
6458
+ };
6113
6459
  }
6114
6460
  let finalCaption = caption;
6115
6461
  if (attribution && finalCaption) {
@@ -6163,8 +6509,9 @@ Created with Social Neuron`;
6163
6509
  title,
6164
6510
  hashtags,
6165
6511
  scheduledAt: schedule_at,
6166
- projectId: project_id,
6167
- ...connectedAccountIds ? { connectedAccountIds } : {},
6512
+ projectId: resolvedProjectId,
6513
+ project_id: resolvedProjectId,
6514
+ connectedAccountIds: routing.connectedAccountIds,
6168
6515
  ...normalizedPlatformMetadata ? {
6169
6516
  platformMetadata: convertPlatformMetadata(
6170
6517
  normalizedPlatformMetadata
@@ -6199,10 +6546,14 @@ Created with Social Neuron`;
6199
6546
  isError: true
6200
6547
  };
6201
6548
  }
6549
+ const responseData = projectAutoResolvedNote ? { ...data, project_auto_resolved: projectAutoResolvedNote } : data;
6202
6550
  const lines = [
6203
6551
  data.success ? "Post scheduled successfully." : "Post scheduling had errors.",
6204
6552
  `Scheduled for: ${data.scheduledAt}`
6205
6553
  ];
6554
+ if (projectAutoResolvedNote) {
6555
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
6556
+ }
6206
6557
  if (tiktokAutoInboxApplied) {
6207
6558
  lines.push(
6208
6559
  "",
@@ -6220,7 +6571,7 @@ Created with Social Neuron`;
6220
6571
  }
6221
6572
  }
6222
6573
  if (format === "json") {
6223
- const structuredContent = asEnvelope3(data);
6574
+ const structuredContent = asEnvelope3(responseData);
6224
6575
  return {
6225
6576
  structuredContent,
6226
6577
  content: [
@@ -6233,7 +6584,7 @@ Created with Social Neuron`;
6233
6584
  };
6234
6585
  }
6235
6586
  return {
6236
- structuredContent: asEnvelope3(data),
6587
+ structuredContent: asEnvelope3(responseData),
6237
6588
  content: [{ type: "text", text: lines.join("\n") }],
6238
6589
  isError: !data.success
6239
6590
  };
@@ -6247,7 +6598,9 @@ Created with Social Neuron`;
6247
6598
  project_id: z3.string().uuid().optional().describe(
6248
6599
  "Brand/project ID that owns the post. Defaults to the authenticated key's project or the account default."
6249
6600
  ),
6250
- scheduled_at: z3.string().datetime({ offset: true }).describe("New future publish time as an ISO 8601 datetime with timezone."),
6601
+ scheduled_at: z3.string().datetime({ offset: true }).describe(
6602
+ "New future publish time as an ISO 8601 datetime with timezone."
6603
+ ),
6251
6604
  expected_scheduled_at: z3.string().datetime({ offset: true }).optional().describe(
6252
6605
  "Optional current schedule timestamp. If it changed since you read it, the update is rejected instead of silently overwriting it."
6253
6606
  ),
@@ -6303,7 +6656,11 @@ Created with Social Neuron`;
6303
6656
  projectId: resolvedProjectId,
6304
6657
  project_id: resolvedProjectId,
6305
6658
  scheduled_at: next.toISOString(),
6306
- ...expected_scheduled_at ? { expected_scheduled_at: new Date(expected_scheduled_at).toISOString() } : {}
6659
+ ...expected_scheduled_at ? {
6660
+ expected_scheduled_at: new Date(
6661
+ expected_scheduled_at
6662
+ ).toISOString()
6663
+ } : {}
6307
6664
  });
6308
6665
  if (error || !result?.success) {
6309
6666
  const code = result?.error ?? error ?? "reschedule_failed";
@@ -6336,17 +6693,34 @@ Created with Social Neuron`;
6336
6693
  "list_connected_accounts",
6337
6694
  "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
6695
  {
6339
- project_id: z3.string().optional().describe(
6696
+ project_id: z3.string().uuid().optional().describe(
6340
6697
  "Brand/project ID to scope connected accounts. Use the same project_id when calling schedule_post."
6341
6698
  ),
6342
- include_all: z3.boolean().optional().describe("If true, include expired or inactive accounts as well as usable accounts."),
6699
+ include_all: z3.boolean().optional().describe(
6700
+ "If true, include expired or inactive accounts as well as usable accounts."
6701
+ ),
6343
6702
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6344
6703
  },
6345
6704
  async ({ project_id, include_all, response_format }) => {
6346
6705
  const format = response_format ?? "text";
6706
+ const projectResolution = await resolveProjectForConnectedAccountTool(project_id);
6707
+ if (!projectResolution.projectId) {
6708
+ return {
6709
+ content: [
6710
+ {
6711
+ type: "text",
6712
+ 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."
6713
+ }
6714
+ ],
6715
+ isError: true
6716
+ };
6717
+ }
6718
+ const resolvedProjectId = projectResolution.projectId;
6719
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
6347
6720
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
6348
6721
  action: "connected-accounts",
6349
- ...project_id ? { projectId: project_id, project_id } : {},
6722
+ projectId: resolvedProjectId,
6723
+ project_id: resolvedProjectId,
6350
6724
  ...include_all ? { includeAll: true } : {}
6351
6725
  });
6352
6726
  if (efError || !result?.success) {
@@ -6360,10 +6734,27 @@ Created with Social Neuron`;
6360
6734
  isError: true
6361
6735
  };
6362
6736
  }
6363
- const accounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
6737
+ const parsedAccounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
6738
+ if (parsedAccounts.some(
6739
+ (account) => account.project_id !== resolvedProjectId
6740
+ )) {
6741
+ return {
6742
+ content: [
6743
+ {
6744
+ type: "text",
6745
+ text: "Connected-account project attestation failed. No account inventory was returned."
6746
+ }
6747
+ ],
6748
+ isError: true
6749
+ };
6750
+ }
6751
+ const accounts = parsedAccounts;
6364
6752
  if (accounts.length === 0) {
6365
6753
  if (format === "json") {
6366
- const structuredContent = asEnvelope3({ accounts: [] });
6754
+ const structuredContent = asEnvelope3({
6755
+ accounts: [],
6756
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
6757
+ });
6367
6758
  return {
6368
6759
  structuredContent,
6369
6760
  content: [
@@ -6378,13 +6769,15 @@ Created with Social Neuron`;
6378
6769
  content: [
6379
6770
  {
6380
6771
  type: "text",
6381
- text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections."
6772
+ text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections." + (projectAutoResolvedNote ? `
6773
+
6774
+ Note: ${projectAutoResolvedNote}` : "")
6382
6775
  }
6383
6776
  ]
6384
6777
  };
6385
6778
  }
6386
6779
  const lines = [
6387
- `${accounts.length} connected account(s)${project_id ? ` for project ${project_id}` : ""}:`,
6780
+ `${accounts.length} connected account(s) for project ${resolvedProjectId}:`,
6388
6781
  ""
6389
6782
  ];
6390
6783
  for (const account of accounts) {
@@ -6396,8 +6789,14 @@ Created with Social Neuron`;
6396
6789
  ` ${platformLower}: ${name} | id=${account.id} | ${project} | status=${status} (connected ${account.created_at.split("T")[0]})`
6397
6790
  );
6398
6791
  }
6792
+ if (projectAutoResolvedNote) {
6793
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
6794
+ }
6399
6795
  if (format === "json") {
6400
- const structuredContent = asEnvelope3({ accounts });
6796
+ const structuredContent = asEnvelope3({
6797
+ accounts,
6798
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
6799
+ });
6401
6800
  return {
6402
6801
  structuredContent,
6403
6802
  content: [
@@ -6592,7 +6991,10 @@ Created with Social Neuron`;
6592
6991
  if (!Number.isFinite(startDate.getTime())) {
6593
6992
  return {
6594
6993
  content: [
6595
- { type: "text", text: "start_after must be a valid ISO datetime." }
6994
+ {
6995
+ type: "text",
6996
+ text: "start_after must be a valid ISO datetime."
6997
+ }
6596
6998
  ],
6597
6999
  isError: true
6598
7000
  };
@@ -6693,11 +7095,14 @@ Created with Social Neuron`;
6693
7095
  "Schedule all posts in a content plan. Optionally auto-assigns time slots and runs quality checks before scheduling. Supports dry-run mode.",
6694
7096
  {
6695
7097
  plan: z3.object({
7098
+ project_id: z3.string().uuid().optional(),
7099
+ account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe("Exact connected-account ID per platform."),
6696
7100
  posts: z3.array(
6697
7101
  z3.object({
6698
7102
  id: z3.string(),
6699
7103
  caption: z3.string(),
6700
7104
  platform: z3.string(),
7105
+ connected_account_id: z3.string().uuid().optional(),
6701
7106
  title: z3.string().optional(),
6702
7107
  media_url: z3.string().optional(),
6703
7108
  schedule_at: z3.string().optional(),
@@ -6705,6 +7110,12 @@ Created with Social Neuron`;
6705
7110
  })
6706
7111
  )
6707
7112
  }).passthrough().optional(),
7113
+ project_id: z3.string().uuid().optional().describe(
7114
+ "Exact brand/project ID. Defaults only when the authenticated user has one project."
7115
+ ),
7116
+ account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
7117
+ "Exact connected-account ID per platform for every post in the plan."
7118
+ ),
6708
7119
  plan_id: z3.string().uuid().optional().describe("Persisted content plan ID from content_plans table"),
6709
7120
  auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
6710
7121
  dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
@@ -6721,6 +7132,8 @@ Created with Social Neuron`;
6721
7132
  async ({
6722
7133
  plan,
6723
7134
  plan_id,
7135
+ project_id,
7136
+ account_ids,
6724
7137
  auto_slot,
6725
7138
  dry_run,
6726
7139
  response_format,
@@ -6730,9 +7143,11 @@ Created with Social Neuron`;
6730
7143
  idempotency_seed
6731
7144
  }) => {
6732
7145
  try {
7146
+ const effectiveBatchSize = batch_size ?? 4;
6733
7147
  let workingPlan = plan;
6734
7148
  let effectivePlanId = plan_id;
6735
- let effectiveProjectId;
7149
+ let effectiveProjectId = project_id;
7150
+ let projectAutoResolvedNote;
6736
7151
  let approvalSummary;
6737
7152
  if (!workingPlan && plan_id) {
6738
7153
  const { data: planResult, error: planError } = await callEdgeFunction("mcp-data", { action: "get-content-plan", plan_id });
@@ -6779,23 +7194,59 @@ Created with Social Neuron`;
6779
7194
  posts: postsFromPayload
6780
7195
  };
6781
7196
  effectivePlanId = stored.id;
6782
- effectiveProjectId = stored.project_id ?? void 0;
7197
+ if (effectiveProjectId && stored.project_id && effectiveProjectId !== stored.project_id) {
7198
+ return {
7199
+ content: [
7200
+ {
7201
+ type: "text",
7202
+ text: `project_id ${effectiveProjectId} does not own plan ${plan_id}.`
7203
+ }
7204
+ ],
7205
+ isError: true
7206
+ };
7207
+ }
7208
+ effectiveProjectId = stored.project_id ?? effectiveProjectId;
7209
+ }
7210
+ if (!workingPlan) {
7211
+ return {
7212
+ content: [
7213
+ {
7214
+ type: "text",
7215
+ text: "Provide either `plan` (inline) or `plan_id` (persisted) to schedule content."
7216
+ }
7217
+ ],
7218
+ isError: true
7219
+ };
6783
7220
  }
6784
- if (!workingPlan) {
7221
+ const planProjectId = workingPlan.project_id;
7222
+ if (effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0 && effectiveProjectId !== planProjectId) {
6785
7223
  return {
6786
7224
  content: [
6787
7225
  {
6788
7226
  type: "text",
6789
- text: "Provide either `plan` (inline) or `plan_id` (persisted) to schedule content."
7227
+ text: `Conflicting project_id values were supplied for the plan (${planProjectId}) and request (${effectiveProjectId}).`
6790
7228
  }
6791
7229
  ],
6792
7230
  isError: true
6793
7231
  };
6794
7232
  }
7233
+ if (!effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0) {
7234
+ effectiveProjectId = planProjectId;
7235
+ }
6795
7236
  if (!effectiveProjectId) {
6796
- const planProjectId = workingPlan.project_id;
6797
- if (typeof planProjectId === "string" && planProjectId.length > 0) {
6798
- effectiveProjectId = planProjectId;
7237
+ const projectResolution = await resolveProjectForConnectedAccountTool();
7238
+ effectiveProjectId = projectResolution.projectId;
7239
+ projectAutoResolvedNote = projectResolution.autoResolvedNote;
7240
+ if (!effectiveProjectId) {
7241
+ return {
7242
+ content: [
7243
+ {
7244
+ type: "text",
7245
+ 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."
7246
+ }
7247
+ ],
7248
+ isError: true
7249
+ };
6799
7250
  }
6800
7251
  }
6801
7252
  if (effectivePlanId) {
@@ -7014,6 +7465,61 @@ Created with Social Neuron`;
7014
7465
  isError: false
7015
7466
  };
7016
7467
  }
7468
+ const embeddedAccountIds = workingPlan.account_ids ?? {};
7469
+ for (const [platform3, accountId] of Object.entries(account_ids ?? {})) {
7470
+ if (embeddedAccountIds[platform3] && embeddedAccountIds[platform3] !== accountId) {
7471
+ return {
7472
+ content: [
7473
+ {
7474
+ type: "text",
7475
+ text: `Conflicting account_ids values were supplied for ${platform3}.`
7476
+ }
7477
+ ],
7478
+ isError: true
7479
+ };
7480
+ }
7481
+ }
7482
+ const requestedPlanAccountIds = {
7483
+ ...embeddedAccountIds,
7484
+ ...account_ids ?? {}
7485
+ };
7486
+ for (const post of workingPlan.posts) {
7487
+ if (!post.connected_account_id) continue;
7488
+ const key = post.platform.toLowerCase() === "x" ? "twitter" : post.platform.toLowerCase();
7489
+ const existing = requestedPlanAccountIds[key];
7490
+ if (existing && existing !== post.connected_account_id) {
7491
+ return {
7492
+ content: [
7493
+ {
7494
+ type: "text",
7495
+ text: `Plan contains conflicting connected_account_id values for ${post.platform}. Use separate plans when scheduling the same platform through different accounts.`
7496
+ }
7497
+ ],
7498
+ isError: true
7499
+ };
7500
+ }
7501
+ requestedPlanAccountIds[key] = post.connected_account_id;
7502
+ }
7503
+ const planPlatforms = Array.from(
7504
+ new Set(workingPlan.posts.map((post) => post.platform))
7505
+ );
7506
+ const planRouting = await resolveConnectedAccountRouting({
7507
+ projectId: effectiveProjectId,
7508
+ platforms: planPlatforms,
7509
+ requestedAccountIds: requestedPlanAccountIds
7510
+ });
7511
+ if (planRouting.error || !planRouting.connectedAccountIds) {
7512
+ return {
7513
+ content: [
7514
+ {
7515
+ type: "text",
7516
+ text: `Cannot schedule plan \u2014 ${planRouting.error ?? "exact connected-account routing could not be established."}`
7517
+ }
7518
+ ],
7519
+ isError: true
7520
+ };
7521
+ }
7522
+ const verifiedPlanAccountIds = planRouting.connectedAccountIds;
7017
7523
  let scheduled = 0;
7018
7524
  let failed = 0;
7019
7525
  const results = [];
@@ -7042,7 +7548,7 @@ Created with Social Neuron`;
7042
7548
  retryable: false
7043
7549
  };
7044
7550
  }
7045
- const normalizedPlatform = PLATFORM_CASE_MAP[post.platform.toLowerCase()] ?? post.platform;
7551
+ const normalizedPlatform = PLATFORM_CASE_MAP2[post.platform.toLowerCase()] ?? post.platform;
7046
7552
  const idempotencyKey = buildIdempotencyKey(post);
7047
7553
  const { data, error } = await callEdgeFunction(
7048
7554
  "schedule-post",
@@ -7053,6 +7559,13 @@ Created with Social Neuron`;
7053
7559
  mediaUrl: post.media_url,
7054
7560
  scheduledAt: post.schedule_at,
7055
7561
  hashtags: post.hashtags,
7562
+ ...effectiveProjectId ? {
7563
+ projectId: effectiveProjectId,
7564
+ project_id: effectiveProjectId
7565
+ } : {},
7566
+ connectedAccountIds: {
7567
+ [normalizedPlatform]: verifiedPlanAccountIds[normalizedPlatform]
7568
+ },
7056
7569
  ...effectivePlanId ? { planId: effectivePlanId } : {},
7057
7570
  idempotencyKey
7058
7571
  },
@@ -7111,7 +7624,7 @@ Created with Social Neuron`;
7111
7624
  const platformBatches = Array.from(grouped.entries()).map(
7112
7625
  async ([platform3, platformPosts]) => {
7113
7626
  const platformResults = [];
7114
- const batches = chunk(platformPosts, batch_size);
7627
+ const batches = chunk(platformPosts, effectiveBatchSize);
7115
7628
  for (const batch of batches) {
7116
7629
  const settled = await Promise.allSettled(
7117
7630
  batch.map((post) => scheduleOne(post))
@@ -7172,7 +7685,8 @@ Created with Social Neuron`;
7172
7685
  total_posts: workingPlan.posts.length,
7173
7686
  scheduled,
7174
7687
  failed
7175
- }
7688
+ },
7689
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
7176
7690
  }),
7177
7691
  null,
7178
7692
  2
@@ -7196,6 +7710,9 @@ Created with Social Neuron`;
7196
7710
  lines.push(
7197
7711
  `Scheduled: ${scheduled}/${workingPlan.posts.length} | Failed: ${failed}/${workingPlan.posts.length}`
7198
7712
  );
7713
+ if (projectAutoResolvedNote) {
7714
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
7715
+ }
7199
7716
  return {
7200
7717
  content: [{ type: "text", text: lines.join("\n") }],
7201
7718
  isError: failed > 0
@@ -7215,7 +7732,7 @@ Created with Social Neuron`;
7215
7732
  }
7216
7733
  );
7217
7734
  }
7218
- var PLATFORM_CASE_MAP, TIKTOK_AUDIT_APPROVED, MCP_NOT_LIVE_FOR_POSTING, VIDEO_FILE_EXTENSIONS, IMAGE_FILE_EXTENSIONS;
7735
+ var PLATFORM_CASE_MAP2, TIKTOK_AUDIT_APPROVED, MCP_NOT_LIVE_FOR_POSTING, VIDEO_FILE_EXTENSIONS, IMAGE_FILE_EXTENSIONS;
7219
7736
  var init_distribution = __esm({
7220
7737
  "src/tools/distribution.ts"() {
7221
7738
  "use strict";
@@ -7226,11 +7743,17 @@ var init_distribution = __esm({
7226
7743
  init_supabase();
7227
7744
  init_quality();
7228
7745
  init_version();
7229
- PLATFORM_CASE_MAP = {
7746
+ init_connected_account_routing();
7747
+ PLATFORM_CASE_MAP2 = {
7230
7748
  youtube: "YouTube",
7231
7749
  tiktok: "TikTok",
7232
7750
  instagram: "Instagram",
7233
7751
  twitter: "Twitter",
7752
+ // 'x' is the platform's current branding but connected_account_routing.ts
7753
+ // (and the DB convention) still key on 'Twitter' — keep both aliases
7754
+ // resolving to the same case so schedule_content_plan's platform:'x' posts
7755
+ // don't fall through to an undefined binding (F8, 2026-07-15).
7756
+ x: "Twitter",
7234
7757
  linkedin: "LinkedIn",
7235
7758
  facebook: "Facebook",
7236
7759
  threads: "Threads",
@@ -7772,15 +8295,35 @@ function registerAnalyticsTools(server) {
7772
8295
  content_id: z5.string().uuid().optional().describe(
7773
8296
  "Filter to a specific content_history ID to see performance of one piece of content."
7774
8297
  ),
7775
- project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
8298
+ project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
7776
8299
  limit: z5.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
7777
8300
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7778
8301
  },
7779
- async ({ platform: platform3, days, content_id, project_id, limit, response_format }) => {
8302
+ async ({
8303
+ platform: platform3,
8304
+ days,
8305
+ content_id,
8306
+ project_id,
8307
+ limit,
8308
+ response_format
8309
+ }) => {
7780
8310
  const format = response_format ?? "text";
7781
8311
  const lookbackDays = days ?? 30;
7782
8312
  const maxPosts = limit ?? 20;
7783
- const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
8313
+ const projectResolution = await resolveProjectStrict(project_id);
8314
+ if (!projectResolution.projectId) {
8315
+ return {
8316
+ content: [
8317
+ {
8318
+ type: "text",
8319
+ 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."
8320
+ }
8321
+ ],
8322
+ isError: true
8323
+ };
8324
+ }
8325
+ const resolvedProjectId = projectResolution.projectId;
8326
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
7784
8327
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
7785
8328
  action: "analytics",
7786
8329
  platform: platform3,
@@ -7791,11 +8334,17 @@ function registerAnalyticsTools(server) {
7791
8334
  limit: Math.min(maxPosts * 5, 100),
7792
8335
  latestOnly: true,
7793
8336
  contentId: content_id,
7794
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
8337
+ projectId: resolvedProjectId,
8338
+ project_id: resolvedProjectId
7795
8339
  });
7796
8340
  if (efError) {
7797
8341
  return {
7798
- content: [{ type: "text", text: `Failed to fetch analytics: ${efError}` }],
8342
+ content: [
8343
+ {
8344
+ type: "text",
8345
+ text: `Failed to fetch analytics: ${efError}`
8346
+ }
8347
+ ],
7799
8348
  isError: true
7800
8349
  };
7801
8350
  }
@@ -7815,7 +8364,8 @@ function registerAnalyticsTools(server) {
7815
8364
  totalViews: 0,
7816
8365
  totalEngagement: 0,
7817
8366
  postCount: 0,
7818
- posts: []
8367
+ posts: [],
8368
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
7819
8369
  });
7820
8370
  return {
7821
8371
  structuredContent,
@@ -7831,7 +8381,9 @@ function registerAnalyticsTools(server) {
7831
8381
  content: [
7832
8382
  {
7833
8383
  type: "text",
7834
- text: `No analytics data found for the last ${lookbackDays} days${platform3 ? ` on ${platform3}` : ""}.`
8384
+ text: `No analytics data found for the last ${lookbackDays} days${platform3 ? ` on ${platform3}` : ""}.` + (projectAutoResolvedNote ? `
8385
+
8386
+ Note: ${projectAutoResolvedNote}` : "")
7835
8387
  }
7836
8388
  ]
7837
8389
  };
@@ -7863,20 +8415,27 @@ function registerAnalyticsTools(server) {
7863
8415
  postCount: posts.length,
7864
8416
  posts
7865
8417
  };
7866
- return formatAnalytics(summary, lookbackDays, format);
8418
+ return formatAnalytics(
8419
+ summary,
8420
+ lookbackDays,
8421
+ format,
8422
+ projectAutoResolvedNote
8423
+ );
7867
8424
  }
7868
8425
  );
7869
8426
  server.tool(
7870
8427
  "refresh_platform_analytics",
7871
8428
  "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
8429
  {
7873
- project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
8430
+ project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
7874
8431
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7875
8432
  },
7876
8433
  async ({ project_id, response_format }) => {
7877
8434
  const format = response_format ?? "text";
7878
8435
  const userId = await getDefaultUserId();
7879
- const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
8436
+ const projectResolution = await resolveProjectStrict(project_id);
8437
+ const resolvedProjectId = projectResolution.projectId;
8438
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
7880
8439
  const rateLimit = checkRateLimit(
7881
8440
  "posting",
7882
8441
  `refresh_platform_analytics:${userId}:${resolvedProjectId ?? "default"}`
@@ -7892,25 +8451,48 @@ function registerAnalyticsTools(server) {
7892
8451
  isError: true
7893
8452
  };
7894
8453
  }
8454
+ if (!resolvedProjectId) {
8455
+ return {
8456
+ content: [
8457
+ {
8458
+ type: "text",
8459
+ 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."
8460
+ }
8461
+ ],
8462
+ isError: true
8463
+ };
8464
+ }
7895
8465
  const { data, error } = await callEdgeFunction("fetch-analytics", {
7896
8466
  userId,
7897
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
8467
+ projectId: resolvedProjectId,
8468
+ project_id: resolvedProjectId
7898
8469
  });
7899
8470
  if (error) {
7900
8471
  return {
7901
- content: [{ type: "text", text: `Error refreshing analytics: ${error}` }],
8472
+ content: [
8473
+ {
8474
+ type: "text",
8475
+ text: `Error refreshing analytics: ${error}`
8476
+ }
8477
+ ],
7902
8478
  isError: true
7903
8479
  };
7904
8480
  }
7905
8481
  const result = data;
7906
8482
  if (!result.success) {
7907
8483
  return {
7908
- content: [{ type: "text", text: "Analytics refresh failed." }],
8484
+ content: [
8485
+ { type: "text", text: "Analytics refresh failed." }
8486
+ ],
7909
8487
  isError: true
7910
8488
  };
7911
8489
  }
7912
- const queued = (result.results ?? []).filter((r) => r.status === "queued").length;
7913
- const errored = (result.results ?? []).filter((r) => r.status === "error").length;
8490
+ const queued = (result.results ?? []).filter(
8491
+ (r) => r.status === "queued"
8492
+ ).length;
8493
+ const errored = (result.results ?? []).filter(
8494
+ (r) => r.status === "error"
8495
+ ).length;
7914
8496
  const lines = [
7915
8497
  `Analytics refresh triggered successfully.`,
7916
8498
  ` Posts processed: ${result.postsProcessed}`,
@@ -7919,13 +8501,17 @@ function registerAnalyticsTools(server) {
7919
8501
  if (errored > 0) {
7920
8502
  lines.push(` Errors: ${errored}`);
7921
8503
  }
8504
+ if (projectAutoResolvedNote) {
8505
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
8506
+ }
7922
8507
  if (format === "json") {
7923
8508
  const structuredContent = asEnvelope4({
7924
8509
  success: true,
7925
8510
  postsProcessed: result.postsProcessed,
7926
8511
  queued,
7927
8512
  errored,
7928
- projectId: resolvedProjectId ?? null
8513
+ projectId: resolvedProjectId ?? null,
8514
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
7929
8515
  });
7930
8516
  return {
7931
8517
  structuredContent,
@@ -7941,12 +8527,21 @@ function registerAnalyticsTools(server) {
7941
8527
  }
7942
8528
  );
7943
8529
  }
7944
- function formatAnalytics(summary, days, format) {
7945
- const structuredContent = asEnvelope4({ ...summary, days });
8530
+ function formatAnalytics(summary, days, format, projectAutoResolvedNote) {
8531
+ const structuredContent = asEnvelope4({
8532
+ ...summary,
8533
+ days,
8534
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
8535
+ });
7946
8536
  if (format === "json") {
7947
8537
  return {
7948
8538
  structuredContent,
7949
- content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }]
8539
+ content: [
8540
+ {
8541
+ type: "text",
8542
+ text: JSON.stringify(structuredContent, null, 2)
8543
+ }
8544
+ ]
7950
8545
  };
7951
8546
  }
7952
8547
  const lines = [
@@ -7970,6 +8565,9 @@ function formatAnalytics(summary, days, format) {
7970
8565
  lines.push(line);
7971
8566
  }
7972
8567
  }
8568
+ if (projectAutoResolvedNote) {
8569
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
8570
+ }
7973
8571
  return {
7974
8572
  structuredContent,
7975
8573
  content: [{ type: "text", text: lines.join("\n") }]
@@ -9399,15 +9997,64 @@ function registerYouTubeAnalyticsTools(server) {
9399
9997
  start_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Start date in YYYY-MM-DD format."),
9400
9998
  end_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("End date in YYYY-MM-DD format."),
9401
9999
  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.'),
10000
+ max_results: z10.number().min(1).max(50).optional().describe(
10001
+ 'Max videos to return for "topVideos" action. Defaults to 10.'
10002
+ ),
10003
+ connected_account_id: z10.string().uuid().optional().describe(
10004
+ "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."
10005
+ ),
10006
+ project_id: z10.string().uuid().optional().describe(
10007
+ "Exact brand/project ID. Defaults only when the authenticated user has one project."
10008
+ ),
9403
10009
  response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9404
10010
  },
9405
- async ({ action, start_date, end_date, video_id, max_results, response_format }) => {
10011
+ async ({
10012
+ action,
10013
+ start_date,
10014
+ end_date,
10015
+ video_id,
10016
+ max_results,
10017
+ connected_account_id,
10018
+ project_id,
10019
+ response_format
10020
+ }) => {
9406
10021
  const format = response_format ?? "text";
9407
10022
  if (action === "video" && !video_id) {
9408
10023
  return {
9409
10024
  content: [
9410
- { type: "text", text: 'Error: video_id is required when action is "video".' }
10025
+ {
10026
+ type: "text",
10027
+ text: 'Error: video_id is required when action is "video".'
10028
+ }
10029
+ ],
10030
+ isError: true
10031
+ };
10032
+ }
10033
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
10034
+ if (!resolvedProjectId) {
10035
+ return {
10036
+ content: [
10037
+ {
10038
+ type: "text",
10039
+ text: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
10040
+ }
10041
+ ],
10042
+ isError: true
10043
+ };
10044
+ }
10045
+ const routing = await resolveConnectedAccountRouting({
10046
+ projectId: resolvedProjectId,
10047
+ platforms: ["youtube"],
10048
+ requestedAccountIds: connected_account_id ? { youtube: connected_account_id } : void 0
10049
+ });
10050
+ const resolvedAccountId = routing.connectedAccountIds?.YouTube;
10051
+ if (routing.error || !resolvedAccountId) {
10052
+ return {
10053
+ content: [
10054
+ {
10055
+ type: "text",
10056
+ text: routing.error ?? "YouTube: exact connected-account routing could not be established."
10057
+ }
9411
10058
  ],
9412
10059
  isError: true
9413
10060
  };
@@ -9417,11 +10064,19 @@ function registerYouTubeAnalyticsTools(server) {
9417
10064
  startDate: start_date,
9418
10065
  endDate: end_date,
9419
10066
  videoId: video_id,
9420
- maxResults: max_results ?? 10
10067
+ maxResults: max_results ?? 10,
10068
+ projectId: resolvedProjectId,
10069
+ project_id: resolvedProjectId,
10070
+ connectedAccountId: resolvedAccountId
9421
10071
  });
9422
10072
  if (error) {
9423
10073
  return {
9424
- content: [{ type: "text", text: `YouTube Analytics error: ${error}` }],
10074
+ content: [
10075
+ {
10076
+ type: "text",
10077
+ text: `YouTube Analytics error: ${error}`
10078
+ }
10079
+ ],
9425
10080
  isError: true
9426
10081
  };
9427
10082
  }
@@ -9434,7 +10089,12 @@ function registerYouTubeAnalyticsTools(server) {
9434
10089
  {
9435
10090
  type: "text",
9436
10091
  text: JSON.stringify(
9437
- asEnvelope7({ action, startDate: start_date, endDate: end_date, analytics: a }),
10092
+ asEnvelope7({
10093
+ action,
10094
+ startDate: start_date,
10095
+ endDate: end_date,
10096
+ analytics: a
10097
+ }),
9438
10098
  null,
9439
10099
  2
9440
10100
  )
@@ -9461,7 +10121,10 @@ function registerYouTubeAnalyticsTools(server) {
9461
10121
  if (days.length === 0) {
9462
10122
  return {
9463
10123
  content: [
9464
- { type: "text", text: "No daily analytics data found for this period." }
10124
+ {
10125
+ type: "text",
10126
+ text: "No daily analytics data found for this period."
10127
+ }
9465
10128
  ]
9466
10129
  };
9467
10130
  }
@@ -9484,7 +10147,10 @@ function registerYouTubeAnalyticsTools(server) {
9484
10147
  ]
9485
10148
  };
9486
10149
  }
9487
- const lines = [`YouTube Daily Analytics (${start_date} to ${end_date}):`, ""];
10150
+ const lines = [
10151
+ `YouTube Daily Analytics (${start_date} to ${end_date}):`,
10152
+ ""
10153
+ ];
9488
10154
  for (const d of days) {
9489
10155
  lines.push(
9490
10156
  ` ${d.date}: ${d.views.toLocaleString()} views, ${d.watchTimeMinutes.toLocaleString()} min watch, +${d.subscribersGained} subs, ${d.likes} likes, ${d.comments} comments`
@@ -9531,7 +10197,12 @@ function registerYouTubeAnalyticsTools(server) {
9531
10197
  const videos = result.topVideos ?? [];
9532
10198
  if (videos.length === 0) {
9533
10199
  return {
9534
- content: [{ type: "text", text: "No top videos found for this period." }]
10200
+ content: [
10201
+ {
10202
+ type: "text",
10203
+ text: "No top videos found for this period."
10204
+ }
10205
+ ]
9535
10206
  };
9536
10207
  }
9537
10208
  if (format === "json") {
@@ -9553,7 +10224,10 @@ function registerYouTubeAnalyticsTools(server) {
9553
10224
  ]
9554
10225
  };
9555
10226
  }
9556
- const lines = [`Top ${videos.length} YouTube Videos (${start_date} to ${end_date}):`, ""];
10227
+ const lines = [
10228
+ `Top ${videos.length} YouTube Videos (${start_date} to ${end_date}):`,
10229
+ ""
10230
+ ];
9557
10231
  for (let i = 0; i < videos.length; i++) {
9558
10232
  const v = videos[i];
9559
10233
  lines.push(
@@ -9566,11 +10240,18 @@ function registerYouTubeAnalyticsTools(server) {
9566
10240
  }
9567
10241
  if (format === "json") {
9568
10242
  return {
9569
- content: [{ type: "text", text: JSON.stringify(asEnvelope7(result), null, 2) }]
10243
+ content: [
10244
+ {
10245
+ type: "text",
10246
+ text: JSON.stringify(asEnvelope7(result), null, 2)
10247
+ }
10248
+ ]
9570
10249
  };
9571
10250
  }
9572
10251
  return {
9573
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
10252
+ content: [
10253
+ { type: "text", text: JSON.stringify(result, null, 2) }
10254
+ ]
9574
10255
  };
9575
10256
  }
9576
10257
  );
@@ -9579,6 +10260,8 @@ var init_youtube_analytics = __esm({
9579
10260
  "src/tools/youtube-analytics.ts"() {
9580
10261
  "use strict";
9581
10262
  init_edge_function();
10263
+ init_supabase();
10264
+ init_connected_account_routing();
9582
10265
  init_version();
9583
10266
  }
9584
10267
  });
@@ -9594,6 +10277,29 @@ function asEnvelope8(data) {
9594
10277
  data
9595
10278
  };
9596
10279
  }
10280
+ async function exactYouTubeRoute(projectId, connectedAccountId) {
10281
+ const resolvedProjectId = projectId ?? await getDefaultProjectId() ?? void 0;
10282
+ if (!resolvedProjectId) {
10283
+ return {
10284
+ error: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
10285
+ };
10286
+ }
10287
+ const routing = await resolveConnectedAccountRouting({
10288
+ projectId: resolvedProjectId,
10289
+ platforms: ["youtube"],
10290
+ requestedAccountIds: connectedAccountId ? { youtube: connectedAccountId } : void 0
10291
+ });
10292
+ const resolvedAccountId = routing.connectedAccountIds?.YouTube;
10293
+ if (routing.error || !resolvedAccountId) {
10294
+ return {
10295
+ error: routing.error ?? "YouTube: exact connected-account routing could not be established."
10296
+ };
10297
+ }
10298
+ return {
10299
+ projectId: resolvedProjectId,
10300
+ connectedAccountId: resolvedAccountId
10301
+ };
10302
+ }
9597
10303
  function registerCommentsTools(server) {
9598
10304
  server.tool(
9599
10305
  "list_comments",
@@ -9606,19 +10312,42 @@ function registerCommentsTools(server) {
9606
10312
  page_token: z11.string().optional().describe(
9607
10313
  "Pagination cursor from previous list_comments response nextPageToken field. Omit for first page of results."
9608
10314
  ),
10315
+ connected_account_id: z11.string().uuid().optional().describe(
10316
+ "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."
10317
+ ),
10318
+ project_id: PROJECT_ID_SCHEMA,
9609
10319
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9610
10320
  },
9611
- async ({ video_id, max_results, page_token, response_format }) => {
10321
+ async ({
10322
+ video_id,
10323
+ max_results,
10324
+ page_token,
10325
+ connected_account_id,
10326
+ project_id,
10327
+ response_format
10328
+ }) => {
9612
10329
  const format = response_format ?? "text";
10330
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
10331
+ if ("error" in route) {
10332
+ return {
10333
+ content: [{ type: "text", text: route.error }],
10334
+ isError: true
10335
+ };
10336
+ }
9613
10337
  const { data, error } = await callEdgeFunction("youtube-comments", {
9614
10338
  action: "list",
9615
10339
  videoId: video_id,
9616
10340
  maxResults: max_results ?? 50,
9617
- pageToken: page_token
10341
+ pageToken: page_token,
10342
+ projectId: route.projectId,
10343
+ project_id: route.projectId,
10344
+ connectedAccountId: route.connectedAccountId
9618
10345
  });
9619
10346
  if (error) {
9620
10347
  return {
9621
- content: [{ type: "text", text: `Error listing comments: ${error}` }],
10348
+ content: [
10349
+ { type: "text", text: `Error listing comments: ${error}` }
10350
+ ],
9622
10351
  isError: true
9623
10352
  };
9624
10353
  }
@@ -9630,7 +10359,10 @@ function registerCommentsTools(server) {
9630
10359
  {
9631
10360
  type: "text",
9632
10361
  text: JSON.stringify(
9633
- asEnvelope8({ comments, nextPageToken: result.nextPageToken ?? null }),
10362
+ asEnvelope8({
10363
+ comments,
10364
+ nextPageToken: result.nextPageToken ?? null
10365
+ }),
9634
10366
  null,
9635
10367
  2
9636
10368
  )
@@ -9667,12 +10399,31 @@ function registerCommentsTools(server) {
9667
10399
  "reply_to_comment",
9668
10400
  "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
10401
  {
9670
- parent_id: z11.string().describe("The ID of the parent comment to reply to (from list_comments)."),
10402
+ parent_id: z11.string().describe(
10403
+ "The ID of the parent comment to reply to (from list_comments)."
10404
+ ),
9671
10405
  text: z11.string().min(1).describe("The reply text."),
10406
+ connected_account_id: z11.string().uuid().optional().describe(
10407
+ "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."
10408
+ ),
10409
+ project_id: PROJECT_ID_SCHEMA,
9672
10410
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9673
10411
  },
9674
- async ({ parent_id, text, response_format }) => {
10412
+ async ({
10413
+ parent_id,
10414
+ text,
10415
+ connected_account_id,
10416
+ project_id,
10417
+ response_format
10418
+ }) => {
9675
10419
  const format = response_format ?? "text";
10420
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
10421
+ if ("error" in route) {
10422
+ return {
10423
+ content: [{ type: "text", text: route.error }],
10424
+ isError: true
10425
+ };
10426
+ }
9676
10427
  const userId = await getDefaultUserId();
9677
10428
  const rateLimit = checkRateLimit("posting", `reply_to_comment:${userId}`);
9678
10429
  if (!rateLimit.allowed) {
@@ -9689,18 +10440,31 @@ function registerCommentsTools(server) {
9689
10440
  const { data, error } = await callEdgeFunction("youtube-comments", {
9690
10441
  action: "reply",
9691
10442
  parentId: parent_id,
9692
- text
10443
+ text,
10444
+ projectId: route.projectId,
10445
+ project_id: route.projectId,
10446
+ connectedAccountId: route.connectedAccountId
9693
10447
  });
9694
10448
  if (error) {
9695
10449
  return {
9696
- content: [{ type: "text", text: `Error replying to comment: ${error}` }],
10450
+ content: [
10451
+ {
10452
+ type: "text",
10453
+ text: `Error replying to comment: ${error}`
10454
+ }
10455
+ ],
9697
10456
  isError: true
9698
10457
  };
9699
10458
  }
9700
10459
  const result = data;
9701
10460
  if (format === "json") {
9702
10461
  return {
9703
- content: [{ type: "text", text: JSON.stringify(asEnvelope8(result), null, 2) }]
10462
+ content: [
10463
+ {
10464
+ type: "text",
10465
+ text: JSON.stringify(asEnvelope8(result), null, 2)
10466
+ }
10467
+ ]
9704
10468
  };
9705
10469
  }
9706
10470
  return {
@@ -9721,10 +10485,27 @@ function registerCommentsTools(server) {
9721
10485
  {
9722
10486
  video_id: z11.string().describe("The YouTube video ID to comment on."),
9723
10487
  text: z11.string().min(1).describe("The comment text."),
10488
+ connected_account_id: z11.string().uuid().optional().describe(
10489
+ "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."
10490
+ ),
10491
+ project_id: PROJECT_ID_SCHEMA,
9724
10492
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9725
10493
  },
9726
- async ({ video_id, text, response_format }) => {
10494
+ async ({
10495
+ video_id,
10496
+ text,
10497
+ connected_account_id,
10498
+ project_id,
10499
+ response_format
10500
+ }) => {
9727
10501
  const format = response_format ?? "text";
10502
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
10503
+ if ("error" in route) {
10504
+ return {
10505
+ content: [{ type: "text", text: route.error }],
10506
+ isError: true
10507
+ };
10508
+ }
9728
10509
  const userId = await getDefaultUserId();
9729
10510
  const rateLimit = checkRateLimit("posting", `post_comment:${userId}`);
9730
10511
  if (!rateLimit.allowed) {
@@ -9741,18 +10522,28 @@ function registerCommentsTools(server) {
9741
10522
  const { data, error } = await callEdgeFunction("youtube-comments", {
9742
10523
  action: "post",
9743
10524
  videoId: video_id,
9744
- text
10525
+ text,
10526
+ projectId: route.projectId,
10527
+ project_id: route.projectId,
10528
+ connectedAccountId: route.connectedAccountId
9745
10529
  });
9746
10530
  if (error) {
9747
10531
  return {
9748
- content: [{ type: "text", text: `Error posting comment: ${error}` }],
10532
+ content: [
10533
+ { type: "text", text: `Error posting comment: ${error}` }
10534
+ ],
9749
10535
  isError: true
9750
10536
  };
9751
10537
  }
9752
10538
  const result = data;
9753
10539
  if (format === "json") {
9754
10540
  return {
9755
- content: [{ type: "text", text: JSON.stringify(asEnvelope8(result), null, 2) }]
10541
+ content: [
10542
+ {
10543
+ type: "text",
10544
+ text: JSON.stringify(asEnvelope8(result), null, 2)
10545
+ }
10546
+ ]
9756
10547
  };
9757
10548
  }
9758
10549
  return {
@@ -9773,10 +10564,27 @@ function registerCommentsTools(server) {
9773
10564
  {
9774
10565
  comment_id: z11.string().describe("The comment ID to moderate."),
9775
10566
  moderation_status: z11.enum(["published", "rejected"]).describe('"published" to approve, "rejected" to hide.'),
10567
+ connected_account_id: z11.string().uuid().optional().describe(
10568
+ "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."
10569
+ ),
10570
+ project_id: PROJECT_ID_SCHEMA,
9776
10571
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9777
10572
  },
9778
- async ({ comment_id, moderation_status, response_format }) => {
10573
+ async ({
10574
+ comment_id,
10575
+ moderation_status,
10576
+ connected_account_id,
10577
+ project_id,
10578
+ response_format
10579
+ }) => {
9779
10580
  const format = response_format ?? "text";
10581
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
10582
+ if ("error" in route) {
10583
+ return {
10584
+ content: [{ type: "text", text: route.error }],
10585
+ isError: true
10586
+ };
10587
+ }
9780
10588
  const userId = await getDefaultUserId();
9781
10589
  const rateLimit = checkRateLimit("posting", `moderate_comment:${userId}`);
9782
10590
  if (!rateLimit.allowed) {
@@ -9793,11 +10601,19 @@ function registerCommentsTools(server) {
9793
10601
  const { error } = await callEdgeFunction("youtube-comments", {
9794
10602
  action: "moderate",
9795
10603
  commentId: comment_id,
9796
- moderationStatus: moderation_status
10604
+ moderationStatus: moderation_status,
10605
+ projectId: route.projectId,
10606
+ project_id: route.projectId,
10607
+ connectedAccountId: route.connectedAccountId
9797
10608
  });
9798
10609
  if (error) {
9799
10610
  return {
9800
- content: [{ type: "text", text: `Error moderating comment: ${error}` }],
10611
+ content: [
10612
+ {
10613
+ type: "text",
10614
+ text: `Error moderating comment: ${error}`
10615
+ }
10616
+ ],
9801
10617
  isError: true
9802
10618
  };
9803
10619
  }
@@ -9834,10 +10650,26 @@ function registerCommentsTools(server) {
9834
10650
  "Delete a YouTube comment. Only works for comments owned by the authenticated channel.",
9835
10651
  {
9836
10652
  comment_id: z11.string().describe("The comment ID to delete."),
10653
+ connected_account_id: z11.string().uuid().optional().describe(
10654
+ "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."
10655
+ ),
10656
+ project_id: PROJECT_ID_SCHEMA,
9837
10657
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9838
10658
  },
9839
- async ({ comment_id, response_format }) => {
10659
+ async ({
10660
+ comment_id,
10661
+ connected_account_id,
10662
+ project_id,
10663
+ response_format
10664
+ }) => {
9840
10665
  const format = response_format ?? "text";
10666
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
10667
+ if ("error" in route) {
10668
+ return {
10669
+ content: [{ type: "text", text: route.error }],
10670
+ isError: true
10671
+ };
10672
+ }
9841
10673
  const userId = await getDefaultUserId();
9842
10674
  const rateLimit = checkRateLimit("posting", `delete_comment:${userId}`);
9843
10675
  if (!rateLimit.allowed) {
@@ -9853,11 +10685,16 @@ function registerCommentsTools(server) {
9853
10685
  }
9854
10686
  const { error } = await callEdgeFunction("youtube-comments", {
9855
10687
  action: "delete",
9856
- commentId: comment_id
10688
+ commentId: comment_id,
10689
+ projectId: route.projectId,
10690
+ project_id: route.projectId,
10691
+ connectedAccountId: route.connectedAccountId
9857
10692
  });
9858
10693
  if (error) {
9859
10694
  return {
9860
- content: [{ type: "text", text: `Error deleting comment: ${error}` }],
10695
+ content: [
10696
+ { type: "text", text: `Error deleting comment: ${error}` }
10697
+ ],
9861
10698
  isError: true
9862
10699
  };
9863
10700
  }
@@ -9866,24 +10703,38 @@ function registerCommentsTools(server) {
9866
10703
  content: [
9867
10704
  {
9868
10705
  type: "text",
9869
- text: JSON.stringify(asEnvelope8({ success: true, commentId: comment_id }), null, 2)
10706
+ text: JSON.stringify(
10707
+ asEnvelope8({ success: true, commentId: comment_id }),
10708
+ null,
10709
+ 2
10710
+ )
9870
10711
  }
9871
10712
  ]
9872
10713
  };
9873
10714
  }
9874
10715
  return {
9875
- content: [{ type: "text", text: `Comment ${comment_id} deleted successfully.` }]
10716
+ content: [
10717
+ {
10718
+ type: "text",
10719
+ text: `Comment ${comment_id} deleted successfully.`
10720
+ }
10721
+ ]
9876
10722
  };
9877
10723
  }
9878
10724
  );
9879
10725
  }
10726
+ var PROJECT_ID_SCHEMA;
9880
10727
  var init_comments = __esm({
9881
10728
  "src/tools/comments.ts"() {
9882
10729
  "use strict";
9883
10730
  init_edge_function();
9884
10731
  init_rate_limit();
9885
10732
  init_supabase();
10733
+ init_connected_account_routing();
9886
10734
  init_version();
10735
+ PROJECT_ID_SCHEMA = z11.string().uuid().optional().describe(
10736
+ "Exact brand/project ID. Defaults only when the authenticated user has one project."
10737
+ );
9887
10738
  }
9888
10739
  });
9889
10740
 
@@ -12594,6 +13445,14 @@ var init_plan_approvals = __esm({
12594
13445
 
12595
13446
  // src/tools/discovery.ts
12596
13447
  import { z as z23 } from "zod";
13448
+ function isHostedTransport() {
13449
+ return process.env.MCP_TRANSPORT === "http";
13450
+ }
13451
+ function isPubliclyDiscoverable(tool) {
13452
+ if (tool.internal || tool.hiddenFromPublicCount) return false;
13453
+ if (tool.localOnly && isHostedTransport()) return false;
13454
+ return true;
13455
+ }
12597
13456
  function toolKnowledgeDocument(tool) {
12598
13457
  const lines = [
12599
13458
  `Tool: ${tool.name}`,
@@ -12604,7 +13463,8 @@ function toolKnowledgeDocument(tool) {
12604
13463
  if (tool.task_intent) lines.push(`Task intent: ${tool.task_intent}`);
12605
13464
  if (tool.use_when) lines.push(`Use when: ${tool.use_when}`);
12606
13465
  if (tool.avoid_when) lines.push(`Avoid when: ${tool.avoid_when}`);
12607
- if (tool.next_tools?.length) lines.push(`Common next tools: ${tool.next_tools.join(", ")}`);
13466
+ if (tool.next_tools?.length)
13467
+ lines.push(`Common next tools: ${tool.next_tools.join(", ")}`);
12608
13468
  return {
12609
13469
  id: `tool:${tool.name}`,
12610
13470
  title: `MCP tool: ${tool.name}`,
@@ -12621,9 +13481,7 @@ function toolKnowledgeDocument(tool) {
12621
13481
  function getKnowledgeDocuments() {
12622
13482
  return [
12623
13483
  ...STATIC_KNOWLEDGE_DOCUMENTS,
12624
- ...TOOL_CATALOG.filter((t) => !t.internal && !t.hiddenFromPublicCount).map(
12625
- toolKnowledgeDocument
12626
- )
13484
+ ...TOOL_CATALOG.filter(isPubliclyDiscoverable).map(toolKnowledgeDocument)
12627
13485
  ];
12628
13486
  }
12629
13487
  function tokenize(input) {
@@ -12677,7 +13535,9 @@ function registerDiscoveryTools(server) {
12677
13535
  };
12678
13536
  return {
12679
13537
  structuredContent,
12680
- content: [{ type: "text", text: JSON.stringify(structuredContent) }]
13538
+ content: [
13539
+ { type: "text", text: JSON.stringify(structuredContent) }
13540
+ ]
12681
13541
  };
12682
13542
  }
12683
13543
  );
@@ -12698,10 +13558,14 @@ function registerDiscoveryTools(server) {
12698
13558
  }
12699
13559
  },
12700
13560
  async ({ id }) => {
12701
- const doc = getKnowledgeDocuments().find((candidate) => candidate.id === id);
13561
+ const doc = getKnowledgeDocuments().find(
13562
+ (candidate) => candidate.id === id
13563
+ );
12702
13564
  if (!doc) {
12703
13565
  return {
12704
- content: [{ type: "text", text: `Document not found: ${id}` }],
13566
+ content: [
13567
+ { type: "text", text: `Document not found: ${id}` }
13568
+ ],
12705
13569
  isError: true
12706
13570
  };
12707
13571
  }
@@ -12714,7 +13578,9 @@ function registerDiscoveryTools(server) {
12714
13578
  };
12715
13579
  return {
12716
13580
  structuredContent,
12717
- content: [{ type: "text", text: JSON.stringify(structuredContent) }]
13581
+ content: [
13582
+ { type: "text", text: JSON.stringify(structuredContent) }
13583
+ ]
12718
13584
  };
12719
13585
  }
12720
13586
  );
@@ -12723,7 +13589,9 @@ function registerDiscoveryTools(server) {
12723
13589
  'Search available tools by name, description, module, or scope. Use "name" detail (~50 tokens) for quick lookup, "summary" (~500 tokens) for descriptions, "full" for complete input schemas. Start here if unsure which tool to call \u2014 filter by module (e.g. "planning", "content", "analytics") to narrow results.',
12724
13590
  {
12725
13591
  query: z23.string().optional().describe("Search query to filter tools by name or description"),
12726
- module: z23.string().optional().describe('Filter by module name (e.g. "planning", "content", "analytics")'),
13592
+ module: z23.string().optional().describe(
13593
+ 'Filter by module name (e.g. "planning", "content", "analytics")'
13594
+ ),
12727
13595
  scope: z23.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
12728
13596
  detail: z23.enum(["name", "summary", "full"]).default("summary").describe(
12729
13597
  'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
@@ -12740,14 +13608,18 @@ function registerDiscoveryTools(server) {
12740
13608
  if (query) {
12741
13609
  results = searchTools(query);
12742
13610
  }
12743
- results = results.filter((t) => !t.internal && !t.hiddenFromPublicCount);
13611
+ results = results.filter(isPubliclyDiscoverable);
12744
13612
  if (module) {
12745
13613
  const moduleTools = getToolsByModule(module);
12746
- results = results.filter((t) => moduleTools.some((mt) => mt.name === t.name));
13614
+ results = results.filter(
13615
+ (t) => moduleTools.some((mt) => mt.name === t.name)
13616
+ );
12747
13617
  }
12748
13618
  if (scope) {
12749
13619
  const scopeTools = getToolsByScope(scope);
12750
- results = results.filter((t) => scopeTools.some((st) => st.name === t.name));
13620
+ results = results.filter(
13621
+ (t) => scopeTools.some((st) => st.name === t.name)
13622
+ );
12751
13623
  }
12752
13624
  if (available_only) {
12753
13625
  results = results.filter(isAvailable);
@@ -12785,7 +13657,12 @@ function registerDiscoveryTools(server) {
12785
13657
  text: JSON.stringify(
12786
13658
  {
12787
13659
  toolCount: results.length,
12788
- ...hasKnownScopes ? { scopes: { available: currentScopes, unavailable_matches: unavailableCount } } : {},
13660
+ ...hasKnownScopes ? {
13661
+ scopes: {
13662
+ available: currentScopes,
13663
+ unavailable_matches: unavailableCount
13664
+ }
13665
+ } : {},
12789
13666
  tools: output
12790
13667
  },
12791
13668
  null,
@@ -12876,7 +13753,10 @@ var init_discovery2 = __esm({
12876
13753
  import { z as z24 } from "zod";
12877
13754
  import { randomUUID as randomUUID3 } from "node:crypto";
12878
13755
  function asEnvelope20(data) {
12879
- return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
13756
+ return {
13757
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
13758
+ data
13759
+ };
12880
13760
  }
12881
13761
  function registerPipelineTools(server) {
12882
13762
  server.tool(
@@ -12899,7 +13779,10 @@ function registerPipelineTools(server) {
12899
13779
  action: "pipeline-readiness",
12900
13780
  platforms,
12901
13781
  estimated_posts,
12902
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
13782
+ ...resolvedProjectId ? {
13783
+ projectId: resolvedProjectId,
13784
+ project_id: resolvedProjectId
13785
+ } : {}
12903
13786
  },
12904
13787
  { timeoutMs: 15e3 }
12905
13788
  );
@@ -12917,16 +13800,24 @@ function registerPipelineTools(server) {
12917
13800
  const blockers = [];
12918
13801
  const warnings = [];
12919
13802
  if (!isUnlimited && credits < estimatedCost) {
12920
- blockers.push(`Insufficient credits: ${credits} available, ~${estimatedCost} needed`);
13803
+ blockers.push(
13804
+ `Insufficient credits: ${credits} available, ~${estimatedCost} needed`
13805
+ );
12921
13806
  }
12922
13807
  if (missingPlatforms.length > 0) {
12923
- blockers.push(`Missing connected accounts: ${missingPlatforms.join(", ")}`);
13808
+ blockers.push(
13809
+ `Missing connected accounts: ${missingPlatforms.join(", ")}`
13810
+ );
12924
13811
  }
12925
13812
  if (!hasBrand) {
12926
- warnings.push("No brand profile found. Content will use generic voice.");
13813
+ warnings.push(
13814
+ "No brand profile found. Content will use generic voice."
13815
+ );
12927
13816
  }
12928
13817
  if (pendingApprovals > 0) {
12929
- warnings.push(`${pendingApprovals} pending approval(s) from previous runs.`);
13818
+ warnings.push(
13819
+ `${pendingApprovals} pending approval(s) from previous runs.`
13820
+ );
12930
13821
  }
12931
13822
  if (!insightsFresh) {
12932
13823
  warnings.push(
@@ -12941,7 +13832,10 @@ function registerPipelineTools(server) {
12941
13832
  estimated_cost: estimatedCost,
12942
13833
  sufficient: credits >= estimatedCost
12943
13834
  },
12944
- connected_accounts: { platforms: connectedPlatforms, missing: missingPlatforms },
13835
+ connected_accounts: {
13836
+ platforms: connectedPlatforms,
13837
+ missing: missingPlatforms
13838
+ },
12945
13839
  brand_profile: { exists: hasBrand },
12946
13840
  pending_approvals: { count: pendingApprovals },
12947
13841
  insights_available: {
@@ -12955,11 +13849,18 @@ function registerPipelineTools(server) {
12955
13849
  };
12956
13850
  if (format === "json") {
12957
13851
  return {
12958
- content: [{ type: "text", text: JSON.stringify(asEnvelope20(result), null, 2) }]
13852
+ content: [
13853
+ {
13854
+ type: "text",
13855
+ text: JSON.stringify(asEnvelope20(result), null, 2)
13856
+ }
13857
+ ]
12959
13858
  };
12960
13859
  }
12961
13860
  const lines = [];
12962
- lines.push(`Pipeline Readiness: ${result.ready ? "READY" : "NOT READY"}`);
13861
+ lines.push(
13862
+ `Pipeline Readiness: ${result.ready ? "READY" : "NOT READY"}`
13863
+ );
12963
13864
  lines.push("=".repeat(40));
12964
13865
  lines.push(
12965
13866
  `Credits: ${credits} available, ~${estimatedCost} needed \u2014 ${credits >= estimatedCost ? "OK" : "BLOCKED"}`
@@ -12967,7 +13868,9 @@ function registerPipelineTools(server) {
12967
13868
  lines.push(
12968
13869
  `Accounts: ${connectedPlatforms.length} connected${missingPlatforms.length > 0 ? ` (missing: ${missingPlatforms.join(", ")})` : " \u2014 OK"}`
12969
13870
  );
12970
- lines.push(`Brand: ${hasBrand ? "OK" : "Missing (will use generic voice)"}`);
13871
+ lines.push(
13872
+ `Brand: ${hasBrand ? "OK" : "Missing (will use generic voice)"}`
13873
+ );
12971
13874
  lines.push(`Pending Approvals: ${pendingApprovals}`);
12972
13875
  lines.push(
12973
13876
  `Insights: ${insightsFresh ? "Fresh" : insightAge === null ? "None available" : `${insightAge} days old`}`
@@ -12986,7 +13889,12 @@ function registerPipelineTools(server) {
12986
13889
  } catch (err) {
12987
13890
  const message = sanitizeError(err);
12988
13891
  return {
12989
- content: [{ type: "text", text: `Readiness check failed: ${message}` }],
13892
+ content: [
13893
+ {
13894
+ type: "text",
13895
+ text: `Readiness check failed: ${message}`
13896
+ }
13897
+ ],
12990
13898
  isError: true
12991
13899
  };
12992
13900
  }
@@ -13000,6 +13908,9 @@ function registerPipelineTools(server) {
13000
13908
  topic: z24.string().optional().describe("Content topic (required if no source_url)"),
13001
13909
  source_url: z24.string().optional().describe("URL to extract content from"),
13002
13910
  platforms: z24.array(PLATFORM_ENUM2).min(1).describe("Target platforms"),
13911
+ account_ids: z24.record(z24.string(), z24.string().uuid()).optional().describe(
13912
+ "Exact connected-account ID per target platform. Required when a project has multiple accounts on one platform."
13913
+ ),
13003
13914
  days: z24.number().min(1).max(7).default(5).describe("Days to plan"),
13004
13915
  posts_per_day: z24.number().min(1).max(3).default(1).describe("Posts per platform per day"),
13005
13916
  approval_mode: z24.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe(
@@ -13021,6 +13932,7 @@ function registerPipelineTools(server) {
13021
13932
  topic,
13022
13933
  source_url,
13023
13934
  platforms,
13935
+ account_ids,
13024
13936
  days,
13025
13937
  posts_per_day,
13026
13938
  approval_mode,
@@ -13038,7 +13950,12 @@ function registerPipelineTools(server) {
13038
13950
  let creditsUsed = 0;
13039
13951
  if (!topic && !source_url) {
13040
13952
  return {
13041
- content: [{ type: "text", text: "Either topic or source_url is required." }],
13953
+ content: [
13954
+ {
13955
+ type: "text",
13956
+ text: "Either topic or source_url is required."
13957
+ }
13958
+ ],
13042
13959
  isError: true
13043
13960
  };
13044
13961
  }
@@ -13068,6 +13985,35 @@ function registerPipelineTools(server) {
13068
13985
  }
13069
13986
  try {
13070
13987
  const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
13988
+ if (schedulingRequested && !resolvedProjectId) {
13989
+ return {
13990
+ content: [
13991
+ {
13992
+ type: "text",
13993
+ text: "A project_id is required to schedule pipeline output. Configure an explicit project or use an API key scoped to exactly one project."
13994
+ }
13995
+ ],
13996
+ isError: true
13997
+ };
13998
+ }
13999
+ if (schedulingRequested) {
14000
+ const preflightRouting = await resolveConnectedAccountRouting({
14001
+ projectId: resolvedProjectId,
14002
+ platforms,
14003
+ requestedAccountIds: account_ids
14004
+ });
14005
+ if (preflightRouting.error || !preflightRouting.connectedAccountIds) {
14006
+ return {
14007
+ content: [
14008
+ {
14009
+ type: "text",
14010
+ text: `Cannot run publishing pipeline \u2014 ${preflightRouting.error ?? "exact connected-account routing could not be established."} (checked before any credits were spent.)`
14011
+ }
14012
+ ],
14013
+ isError: true
14014
+ };
14015
+ }
14016
+ }
13071
14017
  const estimatedCost = BASE_PLAN_CREDITS + (source_url ? SOURCE_EXTRACTION_CREDITS : 0);
13072
14018
  const { data: budgetData } = await callEdgeFunction(
13073
14019
  "mcp-data",
@@ -13121,7 +14067,13 @@ function registerPipelineTools(server) {
13121
14067
  "social-neuron-ai",
13122
14068
  {
13123
14069
  type: "generation",
13124
- prompt: buildPlanPrompt(resolvedTopic, platforms, days, posts_per_day, source_url),
14070
+ prompt: buildPlanPrompt(
14071
+ resolvedTopic,
14072
+ platforms,
14073
+ days,
14074
+ posts_per_day,
14075
+ source_url
14076
+ ),
13125
14077
  model: "gemini-2.5-flash",
13126
14078
  responseFormat: "json",
13127
14079
  ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
@@ -13129,7 +14081,10 @@ function registerPipelineTools(server) {
13129
14081
  { timeoutMs: 6e4 }
13130
14082
  );
13131
14083
  if (planError || !planData) {
13132
- errors.push({ stage: "planning", message: planError ?? "No AI response" });
14084
+ errors.push({
14085
+ stage: "planning",
14086
+ message: planError ?? "No AI response"
14087
+ });
13133
14088
  await callEdgeFunction(
13134
14089
  "mcp-data",
13135
14090
  {
@@ -13146,7 +14101,10 @@ function registerPipelineTools(server) {
13146
14101
  );
13147
14102
  return {
13148
14103
  content: [
13149
- { type: "text", text: `Planning failed: ${planError ?? "No AI response"}` }
14104
+ {
14105
+ type: "text",
14106
+ text: `Planning failed: ${planError ?? "No AI response"}`
14107
+ }
13150
14108
  ],
13151
14109
  isError: true
13152
14110
  };
@@ -13176,20 +14134,22 @@ function registerPipelineTools(server) {
13176
14134
  const postsArray = extractJsonArray(rawText);
13177
14135
  const requestedPlatformSet = new Set(platforms);
13178
14136
  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
- }));
14137
+ const parsedPosts = (postsArray ?? []).map(
14138
+ (p) => ({
14139
+ id: String(p.id ?? randomUUID3().slice(0, 8)),
14140
+ day: Number(p.day ?? 1),
14141
+ date: String(p.date ?? ""),
14142
+ platform: String(p.platform ?? ""),
14143
+ content_type: p.content_type ?? "caption",
14144
+ caption: String(p.caption ?? ""),
14145
+ title: p.title ? String(p.title) : void 0,
14146
+ hashtags: Array.isArray(p.hashtags) ? p.hashtags.map(String) : void 0,
14147
+ hook: String(p.hook ?? ""),
14148
+ angle: String(p.angle ?? ""),
14149
+ visual_direction: p.visual_direction ? String(p.visual_direction) : void 0,
14150
+ media_type: p.media_type ? String(p.media_type) : void 0
14151
+ })
14152
+ );
13193
14153
  const platformFilteredPosts = parsedPosts.filter(
13194
14154
  (post) => requestedPlatformSet.has(post.platform)
13195
14155
  );
@@ -13272,7 +14232,10 @@ function registerPipelineTools(server) {
13272
14232
  estimated_credits: estimatedCost,
13273
14233
  generated_at: (/* @__PURE__ */ new Date()).toISOString()
13274
14234
  },
13275
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
14235
+ ...resolvedProjectId ? {
14236
+ projectId: resolvedProjectId,
14237
+ project_id: resolvedProjectId
14238
+ } : {}
13276
14239
  },
13277
14240
  { timeoutMs: 1e4 }
13278
14241
  );
@@ -13325,6 +14288,22 @@ function registerPipelineTools(server) {
13325
14288
  }
13326
14289
  }
13327
14290
  let postsScheduled = 0;
14291
+ const pipelineRouting = schedulingRequested && postsApproved > 0 ? await resolveConnectedAccountRouting({
14292
+ projectId: resolvedProjectId,
14293
+ platforms,
14294
+ requestedAccountIds: account_ids
14295
+ }) : void 0;
14296
+ if (pipelineRouting?.error || schedulingRequested && postsApproved > 0 && !pipelineRouting?.connectedAccountIds) {
14297
+ return {
14298
+ content: [
14299
+ {
14300
+ type: "text",
14301
+ text: `Cannot run publishing pipeline \u2014 ${pipelineRouting?.error ?? "exact connected-account routing could not be established."}`
14302
+ }
14303
+ ],
14304
+ isError: true
14305
+ };
14306
+ }
13328
14307
  if (!dry_run && !skipSet.has("schedule") && postsApproved > 0) {
13329
14308
  const approvedPosts = posts.filter((p) => p.status === "approved");
13330
14309
  const scheduleBase = /* @__PURE__ */ new Date();
@@ -13332,10 +14311,27 @@ function registerPipelineTools(server) {
13332
14311
  const scheduleBaseMs = scheduleBase.getTime();
13333
14312
  for (const post of approvedPosts) {
13334
14313
  if (creditsUsed >= creditLimit) {
13335
- errors.push({ stage: "schedule", message: "Credit limit reached" });
14314
+ errors.push({
14315
+ stage: "schedule",
14316
+ message: "Credit limit reached"
14317
+ });
13336
14318
  break;
13337
14319
  }
13338
- const scheduledAt = post.schedule_at ?? new Date(scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5).toISOString();
14320
+ const scheduledAt = post.schedule_at ?? new Date(
14321
+ scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5
14322
+ ).toISOString();
14323
+ const route = Object.entries(
14324
+ pipelineRouting.connectedAccountIds
14325
+ ).find(
14326
+ ([platform3]) => platform3.toLowerCase() === post.platform.toLowerCase()
14327
+ );
14328
+ if (!route) {
14329
+ errors.push({
14330
+ stage: "schedule",
14331
+ message: `No verified connected account route for ${post.platform}`
14332
+ });
14333
+ continue;
14334
+ }
13339
14335
  try {
13340
14336
  const { error: schedError } = await callEdgeFunction(
13341
14337
  "schedule-post",
@@ -13348,7 +14344,11 @@ function registerPipelineTools(server) {
13348
14344
  scheduledAt,
13349
14345
  planId,
13350
14346
  idempotencyKey: `pipeline-${planId}-${post.id}`,
13351
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
14347
+ ...resolvedProjectId ? {
14348
+ projectId: resolvedProjectId,
14349
+ project_id: resolvedProjectId
14350
+ } : {},
14351
+ connectedAccountIds: { [route[0]]: route[1] }
13352
14352
  },
13353
14353
  { timeoutMs: 15e3 }
13354
14354
  );
@@ -13414,12 +14414,17 @@ function registerPipelineTools(server) {
13414
14414
  if (response_format === "json") {
13415
14415
  return {
13416
14416
  content: [
13417
- { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
14417
+ {
14418
+ type: "text",
14419
+ text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
14420
+ }
13418
14421
  ]
13419
14422
  };
13420
14423
  }
13421
14424
  const lines = [];
13422
- lines.push(`Pipeline ${pipelineId.slice(0, 8)}... ${finalStatus.toUpperCase()}`);
14425
+ lines.push(
14426
+ `Pipeline ${pipelineId.slice(0, 8)}... ${finalStatus.toUpperCase()}`
14427
+ );
13423
14428
  lines.push("=".repeat(40));
13424
14429
  lines.push(`Posts generated: ${posts.length}`);
13425
14430
  lines.push(`Posts approved: ${postsApproved}`);
@@ -13458,7 +14463,9 @@ function registerPipelineTools(server) {
13458
14463
  } catch {
13459
14464
  }
13460
14465
  return {
13461
- content: [{ type: "text", text: `Pipeline failed: ${message}` }],
14466
+ content: [
14467
+ { type: "text", text: `Pipeline failed: ${message}` }
14468
+ ],
13462
14469
  isError: true
13463
14470
  };
13464
14471
  }
@@ -13495,7 +14502,12 @@ function registerPipelineTools(server) {
13495
14502
  }
13496
14503
  if (format === "json") {
13497
14504
  return {
13498
- content: [{ type: "text", text: JSON.stringify(asEnvelope20(data), null, 2) }]
14505
+ content: [
14506
+ {
14507
+ type: "text",
14508
+ text: JSON.stringify(asEnvelope20(data), null, 2)
14509
+ }
14510
+ ]
13499
14511
  };
13500
14512
  }
13501
14513
  const lines = [];
@@ -13535,10 +14547,19 @@ function registerPipelineTools(server) {
13535
14547
  },
13536
14548
  async ({ plan_id, quality_threshold, response_format }) => {
13537
14549
  try {
13538
- const { data: loadResult, error: loadError } = await callEdgeFunction("mcp-data", { action: "auto-approve-plan", plan_id }, { timeoutMs: 1e4 });
14550
+ const { data: loadResult, error: loadError } = await callEdgeFunction(
14551
+ "mcp-data",
14552
+ { action: "auto-approve-plan", plan_id },
14553
+ { timeoutMs: 1e4 }
14554
+ );
13539
14555
  if (loadError) {
13540
14556
  return {
13541
- content: [{ type: "text", text: `Failed to load plan: ${loadError}` }],
14557
+ content: [
14558
+ {
14559
+ type: "text",
14560
+ text: `Failed to load plan: ${loadError}`
14561
+ }
14562
+ ],
13542
14563
  isError: true
13543
14564
  };
13544
14565
  }
@@ -13546,7 +14567,10 @@ function registerPipelineTools(server) {
13546
14567
  if (!stored?.plan_payload) {
13547
14568
  return {
13548
14569
  content: [
13549
- { type: "text", text: `No content plan found for plan_id=${plan_id}` }
14570
+ {
14571
+ type: "text",
14572
+ text: `No content plan found for plan_id=${plan_id}`
14573
+ }
13550
14574
  ],
13551
14575
  isError: true
13552
14576
  };
@@ -13573,7 +14597,11 @@ function registerPipelineTools(server) {
13573
14597
  blockers: []
13574
14598
  };
13575
14599
  autoApproved++;
13576
- details.push({ post_id: post.id, action: "approved", score: quality.total });
14600
+ details.push({
14601
+ post_id: post.id,
14602
+ action: "approved",
14603
+ score: quality.total
14604
+ });
13577
14605
  } else if (quality.total >= quality_threshold - 5) {
13578
14606
  post.status = "needs_edit";
13579
14607
  post.quality = {
@@ -13583,7 +14611,11 @@ function registerPipelineTools(server) {
13583
14611
  blockers: quality.blockers
13584
14612
  };
13585
14613
  flagged++;
13586
- details.push({ post_id: post.id, action: "flagged", score: quality.total });
14614
+ details.push({
14615
+ post_id: post.id,
14616
+ action: "flagged",
14617
+ score: quality.total
14618
+ });
13587
14619
  } else {
13588
14620
  post.status = "rejected";
13589
14621
  post.quality = {
@@ -13593,7 +14625,11 @@ function registerPipelineTools(server) {
13593
14625
  blockers: quality.blockers
13594
14626
  };
13595
14627
  rejected++;
13596
- details.push({ post_id: post.id, action: "rejected", score: quality.total });
14628
+ details.push({
14629
+ post_id: post.id,
14630
+ action: "rejected",
14631
+ score: quality.total
14632
+ });
13597
14633
  }
13598
14634
  }
13599
14635
  const newStatus = flagged === 0 && rejected === 0 ? "approved" : "in_review";
@@ -13629,7 +14665,10 @@ function registerPipelineTools(server) {
13629
14665
  if (response_format === "json") {
13630
14666
  return {
13631
14667
  content: [
13632
- { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
14668
+ {
14669
+ type: "text",
14670
+ text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
14671
+ }
13633
14672
  ]
13634
14673
  };
13635
14674
  }
@@ -13650,7 +14689,9 @@ function registerPipelineTools(server) {
13650
14689
  } catch (err) {
13651
14690
  const message = sanitizeError(err);
13652
14691
  return {
13653
- content: [{ type: "text", text: `Auto-approve failed: ${message}` }],
14692
+ content: [
14693
+ { type: "text", text: `Auto-approve failed: ${message}` }
14694
+ ],
13654
14695
  isError: true
13655
14696
  };
13656
14697
  }
@@ -13684,6 +14725,7 @@ var init_pipeline = __esm({
13684
14725
  init_quality();
13685
14726
  init_version();
13686
14727
  init_parse_utils();
14728
+ init_connected_account_routing();
13687
14729
  PLATFORM_ENUM2 = z24.enum([
13688
14730
  "youtube",
13689
14731
  "tiktok",
@@ -15211,25 +16253,39 @@ function registerCarouselTools(server) {
15211
16253
  ]).optional().describe("Carousel template. Default: hormozi-authority."),
15212
16254
  slide_count: z28.number().min(3).max(10).optional().describe("Number of slides (3-10). Default: 7."),
15213
16255
  aspect_ratio: z28.enum(["1:1", "4:5", "9:16"]).optional().describe("Aspect ratio for both carousel and images. Default: 1:1."),
15214
- style: z28.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe("Visual style. Default: hormozi for hormozi-authority template."),
16256
+ style: z28.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
16257
+ "Visual style. Default: hormozi for hormozi-authority template."
16258
+ ),
15215
16259
  image_style_suffix: z28.string().max(500).optional().describe(
15216
16260
  'Style suffix appended to every image prompt for visual consistency across slides. Example: "dark moody lighting, cinematic, 35mm film grain".'
15217
16261
  ),
15218
16262
  hook: z28.string().max(300).optional().describe(
15219
16263
  "Explicit hook/opener for slide 1. Overrides any hook derived from topic. Keep under 15 words for strongest scroll-stop."
15220
16264
  ),
15221
- hook_family: z28.enum(["curiosity", "authority", "pain_point", "contrarian", "data_driven"]).optional().describe(
16265
+ hook_family: z28.enum([
16266
+ "curiosity",
16267
+ "authority",
16268
+ "pain_point",
16269
+ "contrarian",
16270
+ "data_driven"
16271
+ ]).optional().describe(
15222
16272
  "Hook family tag. Recorded on the carousel so downstream analytics can attribute engagement to hook pattern."
15223
16273
  ),
15224
- cta_text: z28.string().max(200).optional().describe("Explicit CTA copy for the final slide. If omitted, derived from topic."),
15225
- cta_url: z28.string().url().optional().describe("URL promoted on the CTA slide. Defaults to the project landing page."),
16274
+ cta_text: z28.string().max(200).optional().describe(
16275
+ "Explicit CTA copy for the final slide. If omitted, derived from topic."
16276
+ ),
16277
+ cta_url: z28.string().url().optional().describe(
16278
+ "URL promoted on the CTA slide. Defaults to the project landing page."
16279
+ ),
15226
16280
  tone: z28.string().max(200).optional().describe(
15227
16281
  'Voice/tone override. Composes with the brand profile voice when present. Example: "educational, confident, not arrogant".'
15228
16282
  ),
15229
16283
  constraints: z28.string().max(500).optional().describe(
15230
16284
  'Content constraints applied at generation time. Example: "No fabricated statistics. Sentence case only. No ALL CAPS."'
15231
16285
  ),
15232
- platform: z28.enum(["linkedin", "instagram", "tiktok", "x"]).optional().describe("Target platform. Affects tone conventions and slide-count guardrails."),
16286
+ platform: z28.enum(["linkedin", "instagram", "tiktok", "x"]).optional().describe(
16287
+ "Target platform. Affects tone conventions and slide-count guardrails."
16288
+ ),
15233
16289
  brand_id: z28.string().optional().describe(
15234
16290
  "Brand/project ID to pull visual context from (colors, logo, mood). Falls back to project_id, then default project."
15235
16291
  ),
@@ -15261,7 +16317,9 @@ function registerCarouselTools(server) {
15261
16317
  const slideCount = slide_count ?? 7;
15262
16318
  const ratio = aspect_ratio ?? "1:1";
15263
16319
  let brandContext = null;
15264
- const brandProjectId = brand_id || project_id || await getDefaultProjectId();
16320
+ const defaultProjectId = await getDefaultProjectId();
16321
+ const brandProjectId = brand_id || project_id || defaultProjectId;
16322
+ const resolvedProjectId = project_id || defaultProjectId;
15265
16323
  if (brandProjectId) {
15266
16324
  brandContext = await fetchBrandVisualContext(brandProjectId);
15267
16325
  }
@@ -15270,10 +16328,7 @@ function registerCarouselTools(server) {
15270
16328
  const totalEstimatedCost = carouselTextCost + slideCount * perImageCost;
15271
16329
  const budgetCheck = checkCreditBudget(totalEstimatedCost);
15272
16330
  if (!budgetCheck.ok) {
15273
- return {
15274
- content: [{ type: "text", text: budgetCheck.message }],
15275
- isError: true
15276
- };
16331
+ return budgetCheck.error;
15277
16332
  }
15278
16333
  const assetBudget = checkAssetBudget(slideCount);
15279
16334
  if (!assetBudget.ok) {
@@ -15283,7 +16338,10 @@ function registerCarouselTools(server) {
15283
16338
  };
15284
16339
  }
15285
16340
  const userId = await getDefaultUserId();
15286
- const rateLimit = checkRateLimit("generation", `create_carousel:${userId}`);
16341
+ const rateLimit = checkRateLimit(
16342
+ "generation",
16343
+ `create_carousel:${userId}`
16344
+ );
15287
16345
  if (!rateLimit.allowed) {
15288
16346
  return {
15289
16347
  content: [
@@ -15303,7 +16361,7 @@ function registerCarouselTools(server) {
15303
16361
  slideCount,
15304
16362
  aspectRatio: ratio,
15305
16363
  style: resolvedStyle,
15306
- projectId: project_id,
16364
+ projectId: resolvedProjectId,
15307
16365
  hook,
15308
16366
  hookFamily: hook_family,
15309
16367
  ctaText: cta_text,
@@ -15317,7 +16375,12 @@ function registerCarouselTools(server) {
15317
16375
  if (carouselError || !carouselData?.carousel) {
15318
16376
  const errMsg = carouselError ?? "No carousel data returned";
15319
16377
  return {
15320
- content: [{ type: "text", text: `Carousel text generation failed: ${errMsg}` }],
16378
+ content: [
16379
+ {
16380
+ type: "text",
16381
+ text: `Carousel text generation failed: ${errMsg}`
16382
+ }
16383
+ ],
15321
16384
  isError: true
15322
16385
  };
15323
16386
  }
@@ -15346,7 +16409,8 @@ function registerCarouselTools(server) {
15346
16409
  {
15347
16410
  prompt: imagePrompt,
15348
16411
  model: image_model,
15349
- aspectRatio: ratio
16412
+ aspectRatio: ratio,
16413
+ ...resolvedProjectId && { projectId: resolvedProjectId }
15350
16414
  },
15351
16415
  { timeoutMs: 3e4 }
15352
16416
  );
@@ -15403,14 +16467,19 @@ function registerCarouselTools(server) {
15403
16467
  type: "text",
15404
16468
  text: JSON.stringify(
15405
16469
  {
15406
- _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
16470
+ _meta: {
16471
+ version: MCP_VERSION,
16472
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
16473
+ },
15407
16474
  data: {
15408
16475
  carouselId: carousel.id,
15409
16476
  templateId,
15410
16477
  style: resolvedStyle,
15411
16478
  slideCount: carousel.slides.length,
15412
16479
  slides: carousel.slides.map((s) => {
15413
- const job = imageJobs.find((j) => j.slideNumber === s.slideNumber);
16480
+ const job = imageJobs.find(
16481
+ (j) => j.slideNumber === s.slideNumber
16482
+ );
15414
16483
  return {
15415
16484
  ...s,
15416
16485
  imageJobId: job?.jobId ?? null,
@@ -15437,9 +16506,17 @@ function registerCarouselTools(server) {
15437
16506
  credits: {
15438
16507
  textGeneration: textCredits,
15439
16508
  imagesEstimated: successfulJobs.length * perImageCost,
15440
- imagesCharged: imageJobs.reduce((sum, job) => sum + (job.creditsCharged ?? 0), 0),
15441
- imagesRefunded: imageJobs.reduce((sum, job) => sum + (job.creditsRefunded ?? 0), 0),
15442
- billingUnknownSlides: imageJobs.filter((job) => job.billingStatus === "unknown").length,
16509
+ imagesCharged: imageJobs.reduce(
16510
+ (sum, job) => sum + (job.creditsCharged ?? 0),
16511
+ 0
16512
+ ),
16513
+ imagesRefunded: imageJobs.reduce(
16514
+ (sum, job) => sum + (job.creditsRefunded ?? 0),
16515
+ 0
16516
+ ),
16517
+ billingUnknownSlides: imageJobs.filter(
16518
+ (job) => job.billingStatus === "unknown"
16519
+ ).length,
15443
16520
  totalEstimated: textCredits + successfulJobs.length * perImageCost
15444
16521
  }
15445
16522
  }
@@ -15467,7 +16544,9 @@ function registerCarouselTools(server) {
15467
16544
  for (const slide of carousel.slides) {
15468
16545
  const job = imageJobs.find((j) => j.slideNumber === slide.slideNumber);
15469
16546
  const status = job?.jobId ? `image: ${job.jobId}` : `image FAILED: ${job?.error}`;
15470
- lines.push(` ${slide.slideNumber}. ${slide.headline || "(no headline)"} [${status}]`);
16547
+ lines.push(
16548
+ ` ${slide.slideNumber}. ${slide.headline || "(no headline)"} [${status}]`
16549
+ );
15471
16550
  }
15472
16551
  if (failedJobs.length > 0) {
15473
16552
  lines.push("");
@@ -15478,7 +16557,9 @@ function registerCarouselTools(server) {
15478
16557
  const jobIdList = successfulJobs.map((j) => j.jobId).join(", ");
15479
16558
  lines.push("");
15480
16559
  lines.push("Next steps:");
15481
- lines.push(` 1. Poll each job: check_status with job_id for each of: ${jobIdList}`);
16560
+ lines.push(
16561
+ ` 1. Poll each job: check_status with job_id for each of: ${jobIdList}`
16562
+ );
15482
16563
  lines.push(
15483
16564
  " 2. When all complete: schedule_post with job_ids=[...] and media_type=CAROUSEL_ALBUM"
15484
16565
  );
@@ -16348,28 +17429,33 @@ var init_analytics_pulse = __esm({
16348
17429
 
16349
17430
  // src/tools/connections.ts
16350
17431
  import { z as z33 } from "zod";
16351
- function findActiveAccount(accounts, platform3) {
17432
+ function findActiveAccounts(accounts, platform3, projectId) {
16352
17433
  const target = platform3.toLowerCase();
16353
- return accounts.find((a) => {
17434
+ return accounts.filter((a) => {
16354
17435
  const effectiveStatus = a.effective_status || a.status;
16355
- return a.platform.toLowerCase() === target && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
16356
- }) ?? null;
17436
+ return a.platform.toLowerCase() === target && a.project_id === projectId && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
17437
+ });
16357
17438
  }
16358
17439
  function registerConnectionTools(server) {
16359
17440
  server.tool(
16360
17441
  "start_platform_connection",
16361
17442
  "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
17443
  {
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."
17444
+ platform: z33.enum(PLATFORM_ENUM4).describe(
17445
+ "Platform to connect. Lower-case: instagram, tiktok, youtube, etc."
17446
+ ),
17447
+ project_id: z33.string().uuid().optional().describe(
17448
+ "Brand/project ID to bind the new social account to. Required when the account has multiple brands."
16366
17449
  ),
16367
17450
  response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
16368
17451
  },
16369
17452
  async ({ platform: platform3, project_id, response_format }) => {
16370
17453
  const format = response_format ?? "text";
16371
17454
  const userId = await getDefaultUserId();
16372
- const rl = checkRateLimit("posting", `start_platform_connection:${userId}`);
17455
+ const rl = checkRateLimit(
17456
+ "posting",
17457
+ `start_platform_connection:${userId}`
17458
+ );
16373
17459
  if (!rl.allowed) {
16374
17460
  return {
16375
17461
  content: [
@@ -16381,12 +17467,27 @@ function registerConnectionTools(server) {
16381
17467
  isError: true
16382
17468
  };
16383
17469
  }
17470
+ const projectResolution = await resolveProjectStrict(project_id);
17471
+ if (!projectResolution.projectId) {
17472
+ return {
17473
+ content: [
17474
+ {
17475
+ type: "text",
17476
+ 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."
17477
+ }
17478
+ ],
17479
+ isError: true
17480
+ };
17481
+ }
17482
+ const resolvedProjectId = projectResolution.projectId;
17483
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
16384
17484
  const { data, error } = await callEdgeFunction(
16385
17485
  "mcp-data",
16386
17486
  {
16387
17487
  action: "mint-connection-nonce",
16388
17488
  platform: platform3,
16389
- ...project_id ? { projectId: project_id, project_id } : {}
17489
+ projectId: resolvedProjectId,
17490
+ project_id: resolvedProjectId
16390
17491
  },
16391
17492
  { timeoutMs: 1e4 }
16392
17493
  );
@@ -16402,6 +17503,17 @@ function registerConnectionTools(server) {
16402
17503
  isError: true
16403
17504
  };
16404
17505
  }
17506
+ if (data.project_id !== resolvedProjectId) {
17507
+ return {
17508
+ content: [
17509
+ {
17510
+ type: "text",
17511
+ text: "Connection link project attestation failed. No OAuth link was returned."
17512
+ }
17513
+ ],
17514
+ isError: true
17515
+ };
17516
+ }
16405
17517
  if (format === "json") {
16406
17518
  return {
16407
17519
  content: [
@@ -16410,10 +17522,11 @@ function registerConnectionTools(server) {
16410
17522
  text: JSON.stringify(
16411
17523
  {
16412
17524
  platform: data.platform,
16413
- project_id: project_id ?? null,
17525
+ project_id: resolvedProjectId,
16414
17526
  deep_link: data.deep_link,
16415
17527
  expires_at: data.expires_at,
16416
- next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection."
17528
+ next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection.",
17529
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
16417
17530
  },
16418
17531
  null,
16419
17532
  2
@@ -16429,7 +17542,7 @@ function registerConnectionTools(server) {
16429
17542
  type: "text",
16430
17543
  text: [
16431
17544
  `${data.platform} connection ready.`,
16432
- ...project_id ? [`Brand/project: ${project_id}`] : [],
17545
+ `Brand/project: ${resolvedProjectId}`,
16433
17546
  "",
16434
17547
  'Ask the user to open this link in a browser and click "Connect" on the platform:',
16435
17548
  ` ${data.deep_link}`,
@@ -16437,7 +17550,8 @@ function registerConnectionTools(server) {
16437
17550
  `Link expires at: ${data.expires_at} (~2 minutes).`,
16438
17551
  "",
16439
17552
  "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."
17553
+ "This is a one-time browser setup \u2014 not another OAuth flow inside Claude.",
17554
+ ...projectAutoResolvedNote ? ["", `Note: ${projectAutoResolvedNote}`] : []
16441
17555
  ].join("\n")
16442
17556
  }
16443
17557
  ],
@@ -16450,14 +17564,20 @@ function registerConnectionTools(server) {
16450
17564
  "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
17565
  {
16452
17566
  platform: z33.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
16453
- project_id: z33.string().optional().describe(
17567
+ project_id: z33.string().uuid().optional().describe(
16454
17568
  "Brand/project ID to scope the connection poll. Use the same project_id passed to start_platform_connection."
16455
17569
  ),
16456
17570
  timeout_s: z33.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
16457
17571
  poll_interval_s: z33.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
16458
17572
  response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
16459
17573
  },
16460
- async ({ platform: platform3, project_id, timeout_s, poll_interval_s, response_format }) => {
17574
+ async ({
17575
+ platform: platform3,
17576
+ project_id,
17577
+ timeout_s,
17578
+ poll_interval_s,
17579
+ response_format
17580
+ }) => {
16461
17581
  const format = response_format ?? "text";
16462
17582
  const startedAt = Date.now();
16463
17583
  const timeoutMs = (timeout_s ?? 120) * 1e3;
@@ -16476,6 +17596,18 @@ function registerConnectionTools(server) {
16476
17596
  isError: true
16477
17597
  };
16478
17598
  }
17599
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
17600
+ if (!resolvedProjectId) {
17601
+ return {
17602
+ content: [
17603
+ {
17604
+ type: "text",
17605
+ text: "A project_id is required to wait for a connection. Use the same project_id used to start the OAuth flow."
17606
+ }
17607
+ ],
17608
+ isError: true
17609
+ };
17610
+ }
16479
17611
  const activeWaits = inFlightWaitsByUser.get(userId) ?? 0;
16480
17612
  if (activeWaits >= MAX_CONCURRENT_WAITS_PER_USER) {
16481
17613
  return {
@@ -16497,12 +17629,62 @@ function registerConnectionTools(server) {
16497
17629
  "mcp-data",
16498
17630
  {
16499
17631
  action: "connected-accounts",
16500
- ...project_id ? { projectId: project_id, project_id } : {}
17632
+ projectId: resolvedProjectId,
17633
+ project_id: resolvedProjectId
16501
17634
  },
16502
17635
  { timeoutMs: 1e4 }
16503
17636
  );
16504
17637
  if (!error && data?.success) {
16505
- const found = findActiveAccount(data.accounts ?? [], platform3);
17638
+ const foundAccounts = findActiveAccounts(
17639
+ data.accounts ?? [],
17640
+ platform3,
17641
+ resolvedProjectId
17642
+ );
17643
+ if (foundAccounts.length > 1) {
17644
+ if (format === "json") {
17645
+ return {
17646
+ content: [
17647
+ {
17648
+ type: "text",
17649
+ text: JSON.stringify(
17650
+ {
17651
+ connected: true,
17652
+ platform: platform3,
17653
+ project_id: resolvedProjectId,
17654
+ accounts: foundAccounts.map((a) => ({
17655
+ id: a.id,
17656
+ username: a.username,
17657
+ connected_at: a.created_at
17658
+ })),
17659
+ attempts,
17660
+ message: `${platform3} has ${foundAccounts.length} active accounts for this project. Pass the exact account_id to schedule_post.`
17661
+ },
17662
+ null,
17663
+ 2
17664
+ )
17665
+ }
17666
+ ],
17667
+ isError: false
17668
+ };
17669
+ }
17670
+ return {
17671
+ content: [
17672
+ {
17673
+ type: "text",
17674
+ text: [
17675
+ `${platform3} is connected \u2014 ${foundAccounts.length} active accounts found for project ${resolvedProjectId}:`,
17676
+ ...foundAccounts.map(
17677
+ (a) => ` ${a.username || "(unnamed)"} (id=${a.id})`
17678
+ ),
17679
+ "",
17680
+ "Call schedule_post with the exact account_id (or account_ids) for the one you mean."
17681
+ ].join("\n")
17682
+ }
17683
+ ],
17684
+ isError: false
17685
+ };
17686
+ }
17687
+ const found = foundAccounts[0];
16506
17688
  if (found) {
16507
17689
  if (format === "json") {
16508
17690
  return {
@@ -16513,7 +17695,7 @@ function registerConnectionTools(server) {
16513
17695
  {
16514
17696
  connected: true,
16515
17697
  platform: found.platform,
16516
- project_id: found.project_id ?? project_id ?? null,
17698
+ project_id: resolvedProjectId,
16517
17699
  account_id: found.id,
16518
17700
  username: found.username,
16519
17701
  connected_at: found.created_at,
@@ -16533,7 +17715,7 @@ function registerConnectionTools(server) {
16533
17715
  type: "text",
16534
17716
  text: [
16535
17717
  `${found.platform} is connected.`,
16536
- ...found.project_id || project_id ? [`Brand/project: ${found.project_id ?? project_id}`] : [],
17718
+ `Brand/project: ${resolvedProjectId}`,
16537
17719
  `Account: ${found.username || "(unnamed)"} (id=${found.id})`,
16538
17720
  `Detected after ${attempts} poll(s) in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s.`,
16539
17721
  "Ready to call `schedule_post`."
@@ -16546,7 +17728,9 @@ function registerConnectionTools(server) {
16546
17728
  }
16547
17729
  const remaining = deadline - Date.now();
16548
17730
  if (remaining <= 0) break;
16549
- await new Promise((resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining)));
17731
+ await new Promise(
17732
+ (resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining))
17733
+ );
16550
17734
  }
16551
17735
  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
17736
  if (format === "json") {
@@ -16558,7 +17742,7 @@ function registerConnectionTools(server) {
16558
17742
  {
16559
17743
  connected: false,
16560
17744
  platform: platform3,
16561
- project_id: project_id ?? null,
17745
+ project_id: resolvedProjectId,
16562
17746
  attempts,
16563
17747
  timed_out: true,
16564
17748
  message