@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/http.js CHANGED
@@ -46,6 +46,209 @@ var init_request_context = __esm({
46
46
  }
47
47
  });
48
48
 
49
+ // src/lib/edge-function.ts
50
+ var edge_function_exports = {};
51
+ __export(edge_function_exports, {
52
+ callEdgeFunction: () => callEdgeFunction
53
+ });
54
+ function safeGatewayError(responseText, status) {
55
+ try {
56
+ const parsed = JSON.parse(responseText);
57
+ const nested = parsed.error && typeof parsed.error === "object" ? parsed.error : null;
58
+ const candidates = [
59
+ nested?.error_type,
60
+ nested?.code,
61
+ parsed.error_type,
62
+ parsed.error_code,
63
+ parsed.code,
64
+ typeof parsed.error === "string" ? parsed.error : null
65
+ ];
66
+ for (const value of candidates) {
67
+ if (typeof value !== "string") continue;
68
+ const normalized = value.trim().toLowerCase();
69
+ if (SAFE_GATEWAY_ERROR_CODES.has(normalized)) return normalized;
70
+ const embedded = [...SAFE_GATEWAY_ERROR_CODES].find((code) => normalized.includes(code));
71
+ if (embedded) return embedded;
72
+ }
73
+ } catch {
74
+ }
75
+ return `Backend request failed (HTTP ${status}).`;
76
+ }
77
+ function safeFailureData(responseText) {
78
+ try {
79
+ const parsed = JSON.parse(responseText);
80
+ const metric = (name) => {
81
+ const value = parsed[name];
82
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
83
+ };
84
+ const billingStatus = typeof parsed.billing_status === "string" && SAFE_BILLING_STATUSES.has(parsed.billing_status) ? parsed.billing_status : void 0;
85
+ const failureReason = parsed.failure_reason === "generation_failed" || parsed.failure_reason === "authentication_failed" || parsed.failure_reason === "cancelled_by_user" ? parsed.failure_reason : void 0;
86
+ const jobStatus = typeof parsed.status === "string" && SAFE_JOB_STATUSES.has(parsed.status) ? parsed.status : void 0;
87
+ const safe = {
88
+ ...jobStatus ? { status: jobStatus } : {},
89
+ ...metric("credits_reserved") !== void 0 ? { credits_reserved: metric("credits_reserved") } : {},
90
+ ...metric("credits_charged") !== void 0 ? { credits_charged: metric("credits_charged") } : {},
91
+ ...metric("credits_refunded") !== void 0 ? { credits_refunded: metric("credits_refunded") } : {},
92
+ ...billingStatus ? { billing_status: billingStatus } : {},
93
+ ...failureReason ? { failure_reason: failureReason } : {}
94
+ };
95
+ return Object.keys(safe).length > 0 ? safe : null;
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+ function getApiKeyOrNull() {
101
+ const envKey = process.env.SOCIALNEURON_API_KEY;
102
+ if (envKey && envKey.trim().length) return envKey.trim();
103
+ const requestToken = getRequestToken();
104
+ if (requestToken) return requestToken;
105
+ return getAuthenticatedApiKey();
106
+ }
107
+ async function callEdgeFunction(functionName, body, options) {
108
+ const supabaseUrl = getSupabaseUrl();
109
+ const apiKey = getApiKeyOrNull();
110
+ const controller = new AbortController();
111
+ const timeoutMs = options?.timeoutMs ?? 6e4;
112
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
113
+ const enrichedBody = { ...body };
114
+ if (!enrichedBody.userId && !enrichedBody.user_id) {
115
+ try {
116
+ const defaultId = await getDefaultUserId();
117
+ enrichedBody.userId = defaultId;
118
+ enrichedBody.user_id = defaultId;
119
+ } catch {
120
+ }
121
+ } else {
122
+ if (enrichedBody.userId && !enrichedBody.user_id) enrichedBody.user_id = enrichedBody.userId;
123
+ if (enrichedBody.user_id && !enrichedBody.userId) enrichedBody.userId = enrichedBody.user_id;
124
+ }
125
+ if (!enrichedBody.projectId && !enrichedBody.project_id) {
126
+ try {
127
+ const { getDefaultProjectId: getDefaultProjectId2 } = await Promise.resolve().then(() => (init_supabase(), supabase_exports));
128
+ const defaultProjectId = await getDefaultProjectId2();
129
+ if (defaultProjectId) {
130
+ enrichedBody.projectId = defaultProjectId;
131
+ enrichedBody.project_id = defaultProjectId;
132
+ }
133
+ } catch {
134
+ }
135
+ }
136
+ let url;
137
+ let method = options?.method ?? "POST";
138
+ let headers;
139
+ let requestBody;
140
+ if (!apiKey) {
141
+ clearTimeout(timer);
142
+ return {
143
+ data: null,
144
+ error: "Not authenticated. Run: npx @socialneuron/mcp-server login \u2014 Requires a paid plan (Starter+). See https://socialneuron.com/pricing"
145
+ };
146
+ }
147
+ url = new URL(`${supabaseUrl}/functions/v1/mcp-gateway`);
148
+ headers = {
149
+ Authorization: `Bearer ${apiKey}`,
150
+ "Content-Type": "application/json"
151
+ };
152
+ requestBody = {
153
+ functionName,
154
+ body: enrichedBody,
155
+ query: options?.query,
156
+ method: method.toUpperCase(),
157
+ timeoutMs
158
+ };
159
+ method = "POST";
160
+ try {
161
+ const response = await fetch(url.toString(), {
162
+ method,
163
+ headers,
164
+ body: method === "GET" ? void 0 : JSON.stringify(requestBody),
165
+ signal: controller.signal
166
+ });
167
+ clearTimeout(timer);
168
+ const responseText = await response.text();
169
+ if (!response.ok) {
170
+ const errorCode = safeGatewayError(responseText, response.status);
171
+ const failureData = safeFailureData(responseText);
172
+ if (response.status === 401) {
173
+ return {
174
+ data: failureData,
175
+ error: `Authentication failed (HTTP 401). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
176
+ };
177
+ }
178
+ if (response.status === 403) {
179
+ return {
180
+ data: failureData,
181
+ error: `Forbidden (HTTP 403): ${errorCode}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
182
+ };
183
+ }
184
+ if (response.status === 429) {
185
+ const retryAfter = response.headers.get("retry-after") || "60";
186
+ return {
187
+ data: failureData,
188
+ error: `Rate limit exceeded (HTTP 429). Wait ${retryAfter}s before retrying. Reduce request frequency or upgrade your plan.`
189
+ };
190
+ }
191
+ return { data: failureData, error: errorCode };
192
+ }
193
+ try {
194
+ const data = JSON.parse(responseText);
195
+ return { data, error: null };
196
+ } catch {
197
+ return { data: { text: responseText }, error: null };
198
+ }
199
+ } catch (err) {
200
+ clearTimeout(timer);
201
+ if (err instanceof Error && err.name === "AbortError") {
202
+ return {
203
+ data: null,
204
+ error: `Edge Function '${functionName}' timed out after ${timeoutMs}ms`
205
+ };
206
+ }
207
+ return { data: null, error: "Network request failed. Please retry." };
208
+ }
209
+ }
210
+ var SAFE_GATEWAY_ERROR_CODES, SAFE_BILLING_STATUSES, SAFE_JOB_STATUSES;
211
+ var init_edge_function = __esm({
212
+ "src/lib/edge-function.ts"() {
213
+ "use strict";
214
+ init_supabase();
215
+ init_request_context();
216
+ SAFE_GATEWAY_ERROR_CODES = /* @__PURE__ */ new Set([
217
+ "daily_limit_reached",
218
+ "insufficient_credits",
219
+ "project_scope_mismatch",
220
+ "schedule_conflict",
221
+ "post_not_found",
222
+ "post_not_reschedulable",
223
+ "post_in_progress",
224
+ "not_cancellable",
225
+ "publishing_in_progress",
226
+ "plan_upgrade_required",
227
+ "rate_limited",
228
+ "validation_error",
229
+ "permission_denied",
230
+ "not_found"
231
+ ]);
232
+ SAFE_BILLING_STATUSES = /* @__PURE__ */ new Set([
233
+ "reserved",
234
+ "charged",
235
+ "refunded",
236
+ "failed_no_charge",
237
+ "refund_pending",
238
+ "not_charged"
239
+ ]);
240
+ SAFE_JOB_STATUSES = /* @__PURE__ */ new Set([
241
+ "queued",
242
+ "pending",
243
+ "processing",
244
+ "completed",
245
+ "failed",
246
+ "cancelled",
247
+ "canceled"
248
+ ]);
249
+ }
250
+ });
251
+
49
252
  // src/cli/credentials.ts
50
253
  var credentials_exports = {};
51
254
  __export(credentials_exports, {
@@ -72,6 +275,10 @@ import {
72
275
  } from "node:fs";
73
276
  import { homedir, platform } from "node:os";
74
277
  import { join } from "node:path";
278
+ function loadNativeKeyring() {
279
+ nativeKeyringModule ??= import("@napi-rs/keyring").catch(() => null);
280
+ return nativeKeyringModule;
281
+ }
75
282
  function assertSafeCredentialPaths() {
76
283
  if (platform() === "win32") return;
77
284
  const uid = process.getuid?.();
@@ -191,49 +398,90 @@ function writeCredentialsFile(data) {
191
398
  }
192
399
  hardenCredentialPermissions();
193
400
  }
194
- function macKeychainRead(service) {
401
+ function isMacKeychainItemMissing(error) {
402
+ const commandError = error;
403
+ if (commandError?.status === 44) return true;
404
+ const detail = `${commandError?.stderr ?? ""}
405
+ ${commandError?.message ?? ""}`;
406
+ return /\berrsecitemnotfound\b/i.test(detail) || /(?:^|:\s*)the specified item could not be found in the keychain\.\s*$/i.test(detail.trim());
407
+ }
408
+ async function inspectMacKeychain(service) {
409
+ const native = await loadNativeKeyring();
410
+ if (native) {
411
+ try {
412
+ const value = new native.Entry(service, KEYCHAIN_ACCOUNT).getPassword();
413
+ if (value) return { status: "found", value };
414
+ } catch {
415
+ }
416
+ }
195
417
  try {
196
418
  const result = execFileSync(
197
419
  "security",
198
420
  ["find-generic-password", "-a", KEYCHAIN_ACCOUNT, "-s", service, "-w"],
199
421
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
200
422
  );
201
- return result.trim() || null;
202
- } catch {
203
- return null;
423
+ const value = result.trim();
424
+ return value ? { status: "found", value } : { status: "missing" };
425
+ } catch (error) {
426
+ return isMacKeychainItemMissing(error) ? { status: "missing" } : { status: "unavailable" };
204
427
  }
205
428
  }
206
- function macKeychainWrite(service, value) {
429
+ async function macKeychainRead(service) {
430
+ const result = await inspectMacKeychain(service);
431
+ return result.status === "found" ? result.value : null;
432
+ }
433
+ async function macKeychainWrite(service, value) {
434
+ const native = await loadNativeKeyring();
435
+ if (!native) return false;
207
436
  try {
208
- execFileSync(
209
- "security",
210
- [
211
- "add-generic-password",
212
- "-a",
213
- KEYCHAIN_ACCOUNT,
214
- "-s",
215
- service,
216
- "-w",
217
- value,
218
- "-U"
219
- // update if exists
220
- ],
221
- { stdio: ["pipe", "pipe", "pipe"] }
222
- );
437
+ new native.Entry(service, KEYCHAIN_ACCOUNT).setPassword(value);
223
438
  return true;
224
439
  } catch {
225
440
  return false;
226
441
  }
227
442
  }
228
- function macKeychainDelete(service) {
443
+ async function macKeychainDelete(service) {
444
+ const native = await loadNativeKeyring();
445
+ let nativeDeleted = false;
446
+ if (native) {
447
+ try {
448
+ nativeDeleted = new native.Entry(service, KEYCHAIN_ACCOUNT).deletePassword();
449
+ } catch {
450
+ }
451
+ }
229
452
  try {
230
453
  execFileSync("security", ["delete-generic-password", "-a", KEYCHAIN_ACCOUNT, "-s", service], {
231
454
  stdio: ["pipe", "pipe", "pipe"]
232
455
  });
233
- return true;
234
- } catch {
235
- return false;
456
+ return "deleted";
457
+ } catch (error) {
458
+ if (isMacKeychainItemMissing(error)) return nativeDeleted ? "deleted" : "missing";
459
+ return "unavailable";
460
+ }
461
+ }
462
+ async function clearMacKeychain(service) {
463
+ for (let attempt = 0; attempt < 4; attempt += 1) {
464
+ const deleted = await macKeychainDelete(service);
465
+ if (deleted === "unavailable") break;
466
+ const remaining = await inspectMacKeychain(service);
467
+ if (remaining.status === "missing") return;
468
+ if (remaining.status === "unavailable") break;
236
469
  }
470
+ throw new Error(
471
+ "Unable to verify removal of the existing Social Neuron Keychain credential. Unlock Keychain Access, remove the item, and retry."
472
+ );
473
+ }
474
+ async function verifyMacFileFallback(service) {
475
+ const existing = await inspectMacKeychain(service);
476
+ if (existing.status === "missing") return;
477
+ if (existing.status === "found") {
478
+ throw new Error(
479
+ "An existing Social Neuron Keychain credential could not be replaced. The existing value was retained; unlock or remove it in Keychain Access and retry."
480
+ );
481
+ }
482
+ throw new Error(
483
+ "Unable to verify absence of an existing Social Neuron Keychain credential. Unlock Keychain Access and retry."
484
+ );
237
485
  }
238
486
  function linuxSecretRead(key) {
239
487
  try {
@@ -274,7 +522,7 @@ async function loadApiKey() {
274
522
  if (envKey) return envKey;
275
523
  const os = platform();
276
524
  if (os === "darwin") {
277
- const key = macKeychainRead(KEYCHAIN_SERVICE_API);
525
+ const key = await macKeychainRead(KEYCHAIN_SERVICE_API);
278
526
  if (key) return key;
279
527
  } else if (os === "linux") {
280
528
  const key = linuxSecretRead("api-key");
@@ -291,13 +539,18 @@ async function loadApiKey() {
291
539
  async function saveApiKey(key) {
292
540
  const os = platform();
293
541
  let saved = false;
542
+ let fallbackCredentials;
294
543
  if (os === "darwin") {
295
- saved = macKeychainWrite(KEYCHAIN_SERVICE_API, key);
544
+ saved = await macKeychainWrite(KEYCHAIN_SERVICE_API, key);
545
+ if (!saved) {
546
+ fallbackCredentials = readCredentialsFile();
547
+ await verifyMacFileFallback(KEYCHAIN_SERVICE_API);
548
+ }
296
549
  } else if (os === "linux") {
297
550
  saved = linuxSecretWrite("api-key", key);
298
551
  }
299
552
  if (!saved) {
300
- const creds = readCredentialsFile();
553
+ const creds = fallbackCredentials ?? readCredentialsFile();
301
554
  creds.apiKey = key;
302
555
  writeCredentialsFile(creds);
303
556
  if (os === "win32") {
@@ -320,8 +573,13 @@ async function saveApiKey(key) {
320
573
  }
321
574
  async function deleteApiKey() {
322
575
  const os = platform();
576
+ let keychainError;
323
577
  if (os === "darwin") {
324
- macKeychainDelete(KEYCHAIN_SERVICE_API);
578
+ try {
579
+ await clearMacKeychain(KEYCHAIN_SERVICE_API);
580
+ } catch (error) {
581
+ keychainError = error;
582
+ }
325
583
  } else if (os === "linux") {
326
584
  linuxSecretDelete("api-key");
327
585
  }
@@ -337,13 +595,14 @@ async function deleteApiKey() {
337
595
  writeCredentialsFile(creds);
338
596
  }
339
597
  }
598
+ if (keychainError) throw keychainError;
340
599
  }
341
600
  async function loadSupabaseUrl() {
342
601
  const envUrl = process.env.SOCIALNEURON_SUPABASE_URL || process.env.SUPABASE_URL;
343
602
  if (envUrl) return envUrl;
344
603
  const os = platform();
345
604
  if (os === "darwin") {
346
- const url = macKeychainRead(KEYCHAIN_SERVICE_URL);
605
+ const url = await macKeychainRead(KEYCHAIN_SERVICE_URL);
347
606
  if (url) return url;
348
607
  } else if (os === "linux") {
349
608
  const url = linuxSecretRead("supabase-url");
@@ -355,18 +614,23 @@ async function loadSupabaseUrl() {
355
614
  async function saveSupabaseUrl(url) {
356
615
  const os = platform();
357
616
  let saved = false;
617
+ let fallbackCredentials;
358
618
  if (os === "darwin") {
359
- saved = macKeychainWrite(KEYCHAIN_SERVICE_URL, url);
619
+ saved = await macKeychainWrite(KEYCHAIN_SERVICE_URL, url);
620
+ if (!saved) {
621
+ fallbackCredentials = readCredentialsFile();
622
+ await verifyMacFileFallback(KEYCHAIN_SERVICE_URL);
623
+ }
360
624
  } else if (os === "linux") {
361
625
  saved = linuxSecretWrite("supabase-url", url);
362
626
  }
363
627
  if (!saved) {
364
- const creds = readCredentialsFile();
628
+ const creds = fallbackCredentials ?? readCredentialsFile();
365
629
  creds.supabaseUrl = url;
366
630
  writeCredentialsFile(creds);
367
631
  }
368
632
  }
369
- var KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE_API, KEYCHAIN_SERVICE_URL, CONFIG_DIR, CREDENTIALS_FILE;
633
+ var KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE_API, KEYCHAIN_SERVICE_URL, CONFIG_DIR, CREDENTIALS_FILE, nativeKeyringModule;
370
634
  var init_credentials = __esm({
371
635
  "src/cli/credentials.ts"() {
372
636
  "use strict";
@@ -519,7 +783,10 @@ __export(supabase_exports, {
519
783
  getSupabaseUrl: () => getSupabaseUrl,
520
784
  initializeAuth: () => initializeAuth,
521
785
  isTelemetryDisabled: () => isTelemetryDisabled,
522
- logMcpToolInvocation: () => logMcpToolInvocation
786
+ listAccessibleProjectsWithAccountStatus: () => listAccessibleProjectsWithAccountStatus,
787
+ logMcpToolInvocation: () => logMcpToolInvocation,
788
+ resolveProjectForConnectedAccountTool: () => resolveProjectForConnectedAccountTool,
789
+ resolveProjectStrict: () => resolveProjectStrict
523
790
  });
524
791
  import { createClient } from "@supabase/supabase-js";
525
792
  import { randomUUID } from "node:crypto";
@@ -558,20 +825,17 @@ async function getDefaultUserId() {
558
825
  if (authenticatedUserId) return authenticatedUserId;
559
826
  const envUserId = process.env.SOCIALNEURON_USER_ID;
560
827
  if (envUserId) return envUserId;
561
- throw new Error("No user ID available. Set SOCIALNEURON_USER_ID or authenticate via API key.");
828
+ throw new Error(
829
+ "No user ID available. Set SOCIALNEURON_USER_ID or authenticate via API key."
830
+ );
562
831
  }
563
832
  async function getDefaultProjectId() {
564
833
  const requestProjectId = getRequestProjectId();
565
834
  if (requestProjectId) return requestProjectId;
566
835
  if (authenticatedProjectId) return authenticatedProjectId;
567
836
  const userId = await getDefaultUserId().catch(() => null);
568
- if (userId) {
569
- const cached3 = projectIdCache.get(userId);
570
- if (cached3) return cached3;
571
- }
572
837
  const envProjectId = process.env.SOCIALNEURON_PROJECT_ID;
573
838
  if (envProjectId) {
574
- if (userId) projectIdCache.set(userId, envProjectId);
575
839
  return envProjectId;
576
840
  }
577
841
  if (!userId) return null;
@@ -580,15 +844,126 @@ async function getDefaultProjectId() {
580
844
  const { data: memberships } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId);
581
845
  const orgIds = (memberships ?? []).map((m) => m.organization_id);
582
846
  if (orgIds.length === 0) return null;
583
- const { data } = await supabase.from("projects").select("id").in("organization_id", orgIds).order("created_at", { ascending: false }).limit(1).maybeSingle();
584
- if (data?.id) {
585
- projectIdCache.set(userId, data.id);
586
- return data.id;
587
- }
847
+ const { data } = await supabase.from("projects").select("id").in("organization_id", orgIds).order("created_at", { ascending: false }).limit(2);
848
+ if (data?.length === 1) return data[0].id;
588
849
  } catch {
589
850
  }
590
851
  return null;
591
852
  }
853
+ function normalizePlatformFilter(platform2) {
854
+ if (!platform2) return null;
855
+ const values = Array.isArray(platform2) ? platform2 : [platform2];
856
+ const set = new Set(values.filter(Boolean).map((p) => p.toLowerCase()));
857
+ return set.size > 0 ? set : null;
858
+ }
859
+ async function listAccessibleProjectsWithAccountStatusDirect(userId, platform2) {
860
+ try {
861
+ const supabase = getSupabaseClient();
862
+ const { data: memberships } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId);
863
+ const orgIds = (memberships ?? []).map((m) => m.organization_id);
864
+ if (orgIds.length === 0) return [];
865
+ const { data: projects } = await supabase.from("projects").select("id, name").in("organization_id", orgIds).order("created_at", { ascending: false });
866
+ const projectRows = projects ?? [];
867
+ if (projectRows.length === 0) return [];
868
+ const projectIds = projectRows.map((p) => p.id);
869
+ const { data: accounts } = await supabase.from("connected_accounts").select("project_id, status, platform").eq("user_id", userId).in("project_id", projectIds);
870
+ const anyAccountByProject = /* @__PURE__ */ new Set();
871
+ const platformsByProject = /* @__PURE__ */ new Map();
872
+ for (const row of accounts ?? []) {
873
+ if (row.status !== "active" && row.status !== "expires_soon") continue;
874
+ if (!row.project_id) continue;
875
+ anyAccountByProject.add(row.project_id);
876
+ if (row.platform) {
877
+ const set = platformsByProject.get(row.project_id) ?? /* @__PURE__ */ new Set();
878
+ set.add(row.platform.toLowerCase());
879
+ platformsByProject.set(row.project_id, set);
880
+ }
881
+ }
882
+ const requestedPlatforms = normalizePlatformFilter(platform2);
883
+ return projectRows.map((p) => {
884
+ const platforms = Array.from(platformsByProject.get(p.id) ?? []);
885
+ const hasConnectedAccounts = requestedPlatforms ? platforms.some((pl) => requestedPlatforms.has(pl)) : anyAccountByProject.has(p.id);
886
+ return { id: p.id, name: p.name, hasConnectedAccounts, platforms };
887
+ });
888
+ } catch {
889
+ return [];
890
+ }
891
+ }
892
+ async function listAccessibleProjectsWithAccountStatusViaEdgeFunction(userId, platform2) {
893
+ try {
894
+ const { callEdgeFunction: callEdgeFunction2 } = await Promise.resolve().then(() => (init_edge_function(), edge_function_exports));
895
+ const { data, error } = await callEdgeFunction2(
896
+ "mcp-data",
897
+ { action: "projects", userId, user_id: userId },
898
+ { timeoutMs: 1e4 }
899
+ );
900
+ if (error || !data?.success || !Array.isArray(data.projects)) return [];
901
+ const requestedPlatforms = normalizePlatformFilter(platform2);
902
+ return data.projects.map((p) => {
903
+ const platforms = (p.platforms ?? []).map((pl) => pl.toLowerCase());
904
+ const hasConnectedAccounts = requestedPlatforms ? platforms.some((pl) => requestedPlatforms.has(pl)) : Boolean(p.hasConnectedAccounts);
905
+ return { id: p.id, name: p.name, hasConnectedAccounts, platforms };
906
+ });
907
+ } catch {
908
+ return [];
909
+ }
910
+ }
911
+ async function listAccessibleProjectsWithAccountStatus(userId, platform2) {
912
+ if (getServiceKeyOrNull()) {
913
+ return listAccessibleProjectsWithAccountStatusDirect(userId, platform2);
914
+ }
915
+ return listAccessibleProjectsWithAccountStatusViaEdgeFunction(
916
+ userId,
917
+ platform2
918
+ );
919
+ }
920
+ async function resolveProjectForConnectedAccountTool(explicitProjectId, platform2) {
921
+ if (explicitProjectId) return { projectId: explicitProjectId };
922
+ const defaultProjectId = await getDefaultProjectId();
923
+ if (defaultProjectId) return { projectId: defaultProjectId };
924
+ const genericError = "project_id is required. Configure an explicit project or use an API key scoped to exactly one project.";
925
+ const userId = await getDefaultUserId().catch(() => null);
926
+ if (!userId) return { error: genericError };
927
+ const projects = await listAccessibleProjectsWithAccountStatus(
928
+ userId,
929
+ platform2
930
+ );
931
+ if (projects.length === 0) return { error: genericError };
932
+ const withAccounts = projects.filter((p) => p.hasConnectedAccounts);
933
+ if (withAccounts.length === 1) {
934
+ const chosen = withAccounts[0];
935
+ const platformNote = platform2 ? ` for ${Array.isArray(platform2) ? platform2.join("/") : platform2}` : "";
936
+ return {
937
+ projectId: chosen.id,
938
+ 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}.`,
939
+ projects
940
+ };
941
+ }
942
+ const projectList = projects.map(
943
+ (p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
944
+ ).join("; ");
945
+ return {
946
+ 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}.`,
947
+ projects
948
+ };
949
+ }
950
+ async function resolveProjectStrict(explicitProjectId) {
951
+ if (explicitProjectId) return { projectId: explicitProjectId };
952
+ const defaultProjectId = await getDefaultProjectId();
953
+ if (defaultProjectId) return { projectId: defaultProjectId };
954
+ const genericError = "project_id is required. Configure an explicit project or use an API key scoped to exactly one project.";
955
+ const userId = await getDefaultUserId().catch(() => null);
956
+ if (!userId) return { error: genericError };
957
+ const projects = await listAccessibleProjectsWithAccountStatus(userId);
958
+ if (projects.length === 0) return { error: genericError };
959
+ const projectList = projects.map(
960
+ (p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
961
+ ).join("; ");
962
+ return {
963
+ error: `project_id is required \u2014 your account has ${projects.length} projects. Pass the exact project_id from this list: ${projectList}.`,
964
+ projects
965
+ };
966
+ }
592
967
  async function initializeAuth() {
593
968
  const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
594
969
  const apiKey = await loadApiKey2();
@@ -614,8 +989,11 @@ async function initializeAuth() {
614
989
  }
615
990
  if (authenticatedExpiresAt) {
616
991
  const expiresMs = new Date(authenticatedExpiresAt).getTime();
617
- const daysLeft = Math.ceil((expiresMs - Date.now()) / (1e3 * 60 * 60 * 24));
618
- if (!_quietAuth) console.error("[MCP] Key expires: " + authenticatedExpiresAt);
992
+ const daysLeft = Math.ceil(
993
+ (expiresMs - Date.now()) / (1e3 * 60 * 60 * 24)
994
+ );
995
+ if (!_quietAuth)
996
+ console.error("[MCP] Key expires: " + authenticatedExpiresAt);
619
997
  if (daysLeft <= 7) {
620
998
  console.error(
621
999
  `[MCP] Warning: API key expires in ${daysLeft} day(s). Run: npx @socialneuron/mcp-server login`
@@ -694,7 +1072,7 @@ async function logMcpToolInvocation(args) {
694
1072
  Promise.resolve().then(() => (init_posthog(), posthog_exports)).then(({ captureToolEvent: captureToolEvent2 }) => captureToolEvent2(args)).catch(() => {
695
1073
  });
696
1074
  }
697
- 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;
1075
+ var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, authenticatedProjectId, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY;
698
1076
  var init_supabase = __esm({
699
1077
  "src/lib/supabase.ts"() {
700
1078
  "use strict";
@@ -712,7 +1090,6 @@ var init_supabase = __esm({
712
1090
  MCP_RUN_ID = randomUUID();
713
1091
  CLOUD_SUPABASE_URL = "https://rhukkjscgzauutioyeei.supabase.co";
714
1092
  CLOUD_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJodWtranNjZ3phdXV0aW95ZWVpIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ4NjM4ODYsImV4cCI6MjA4MDQzOTg4Nn0.JVtrviGvN0HaSh0JFS5KNl5FAB5ffG5Y1IMZsQFUrNQ";
715
- projectIdCache = /* @__PURE__ */ new Map();
716
1093
  }
717
1094
  });
718
1095
 
@@ -2013,201 +2390,9 @@ function scan(text, options) {
2013
2390
  }
2014
2391
 
2015
2392
  // src/tools/ideation.ts
2393
+ init_edge_function();
2016
2394
  import { z } from "zod";
2017
2395
 
2018
- // src/lib/edge-function.ts
2019
- init_supabase();
2020
- init_request_context();
2021
- var SAFE_GATEWAY_ERROR_CODES = /* @__PURE__ */ new Set([
2022
- "daily_limit_reached",
2023
- "insufficient_credits",
2024
- "project_scope_mismatch",
2025
- "schedule_conflict",
2026
- "post_not_found",
2027
- "post_not_reschedulable",
2028
- "post_in_progress",
2029
- "not_cancellable",
2030
- "publishing_in_progress",
2031
- "plan_upgrade_required",
2032
- "rate_limited",
2033
- "validation_error",
2034
- "permission_denied",
2035
- "not_found"
2036
- ]);
2037
- function safeGatewayError(responseText, status) {
2038
- try {
2039
- const parsed = JSON.parse(responseText);
2040
- const nested = parsed.error && typeof parsed.error === "object" ? parsed.error : null;
2041
- const candidates = [
2042
- nested?.error_type,
2043
- nested?.code,
2044
- parsed.error_type,
2045
- parsed.error_code,
2046
- parsed.code,
2047
- typeof parsed.error === "string" ? parsed.error : null
2048
- ];
2049
- for (const value of candidates) {
2050
- if (typeof value !== "string") continue;
2051
- const normalized = value.trim().toLowerCase();
2052
- if (SAFE_GATEWAY_ERROR_CODES.has(normalized)) return normalized;
2053
- const embedded = [...SAFE_GATEWAY_ERROR_CODES].find((code) => normalized.includes(code));
2054
- if (embedded) return embedded;
2055
- }
2056
- } catch {
2057
- }
2058
- return `Backend request failed (HTTP ${status}).`;
2059
- }
2060
- var SAFE_BILLING_STATUSES = /* @__PURE__ */ new Set([
2061
- "reserved",
2062
- "charged",
2063
- "refunded",
2064
- "failed_no_charge",
2065
- "refund_pending",
2066
- "not_charged"
2067
- ]);
2068
- var SAFE_JOB_STATUSES = /* @__PURE__ */ new Set([
2069
- "queued",
2070
- "pending",
2071
- "processing",
2072
- "completed",
2073
- "failed",
2074
- "cancelled",
2075
- "canceled"
2076
- ]);
2077
- function safeFailureData(responseText) {
2078
- try {
2079
- const parsed = JSON.parse(responseText);
2080
- const metric = (name) => {
2081
- const value = parsed[name];
2082
- return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
2083
- };
2084
- const billingStatus = typeof parsed.billing_status === "string" && SAFE_BILLING_STATUSES.has(parsed.billing_status) ? parsed.billing_status : void 0;
2085
- const failureReason = parsed.failure_reason === "generation_failed" || parsed.failure_reason === "authentication_failed" || parsed.failure_reason === "cancelled_by_user" ? parsed.failure_reason : void 0;
2086
- const jobStatus = typeof parsed.status === "string" && SAFE_JOB_STATUSES.has(parsed.status) ? parsed.status : void 0;
2087
- const safe = {
2088
- ...jobStatus ? { status: jobStatus } : {},
2089
- ...metric("credits_reserved") !== void 0 ? { credits_reserved: metric("credits_reserved") } : {},
2090
- ...metric("credits_charged") !== void 0 ? { credits_charged: metric("credits_charged") } : {},
2091
- ...metric("credits_refunded") !== void 0 ? { credits_refunded: metric("credits_refunded") } : {},
2092
- ...billingStatus ? { billing_status: billingStatus } : {},
2093
- ...failureReason ? { failure_reason: failureReason } : {}
2094
- };
2095
- return Object.keys(safe).length > 0 ? safe : null;
2096
- } catch {
2097
- return null;
2098
- }
2099
- }
2100
- function getApiKeyOrNull() {
2101
- const envKey = process.env.SOCIALNEURON_API_KEY;
2102
- if (envKey && envKey.trim().length) return envKey.trim();
2103
- const requestToken = getRequestToken();
2104
- if (requestToken) return requestToken;
2105
- return getAuthenticatedApiKey();
2106
- }
2107
- async function callEdgeFunction(functionName, body, options) {
2108
- const supabaseUrl = getSupabaseUrl();
2109
- const apiKey = getApiKeyOrNull();
2110
- const controller = new AbortController();
2111
- const timeoutMs = options?.timeoutMs ?? 6e4;
2112
- const timer = setTimeout(() => controller.abort(), timeoutMs);
2113
- const enrichedBody = { ...body };
2114
- if (!enrichedBody.userId && !enrichedBody.user_id) {
2115
- try {
2116
- const defaultId = await getDefaultUserId();
2117
- enrichedBody.userId = defaultId;
2118
- enrichedBody.user_id = defaultId;
2119
- } catch {
2120
- }
2121
- } else {
2122
- if (enrichedBody.userId && !enrichedBody.user_id) enrichedBody.user_id = enrichedBody.userId;
2123
- if (enrichedBody.user_id && !enrichedBody.userId) enrichedBody.userId = enrichedBody.user_id;
2124
- }
2125
- if (!enrichedBody.projectId && !enrichedBody.project_id) {
2126
- try {
2127
- const { getDefaultProjectId: getDefaultProjectId2 } = await Promise.resolve().then(() => (init_supabase(), supabase_exports));
2128
- const defaultProjectId = await getDefaultProjectId2();
2129
- if (defaultProjectId) {
2130
- enrichedBody.projectId = defaultProjectId;
2131
- enrichedBody.project_id = defaultProjectId;
2132
- }
2133
- } catch {
2134
- }
2135
- }
2136
- let url;
2137
- let method = options?.method ?? "POST";
2138
- let headers;
2139
- let requestBody;
2140
- if (!apiKey) {
2141
- clearTimeout(timer);
2142
- return {
2143
- data: null,
2144
- error: "Not authenticated. Run: npx @socialneuron/mcp-server login \u2014 Requires a paid plan (Starter+). See https://socialneuron.com/pricing"
2145
- };
2146
- }
2147
- url = new URL(`${supabaseUrl}/functions/v1/mcp-gateway`);
2148
- headers = {
2149
- Authorization: `Bearer ${apiKey}`,
2150
- "Content-Type": "application/json"
2151
- };
2152
- requestBody = {
2153
- functionName,
2154
- body: enrichedBody,
2155
- query: options?.query,
2156
- method: method.toUpperCase(),
2157
- timeoutMs
2158
- };
2159
- method = "POST";
2160
- try {
2161
- const response = await fetch(url.toString(), {
2162
- method,
2163
- headers,
2164
- body: method === "GET" ? void 0 : JSON.stringify(requestBody),
2165
- signal: controller.signal
2166
- });
2167
- clearTimeout(timer);
2168
- const responseText = await response.text();
2169
- if (!response.ok) {
2170
- const errorCode = safeGatewayError(responseText, response.status);
2171
- const failureData = safeFailureData(responseText);
2172
- if (response.status === 401) {
2173
- return {
2174
- data: failureData,
2175
- error: `Authentication failed (HTTP 401). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
2176
- };
2177
- }
2178
- if (response.status === 403) {
2179
- return {
2180
- data: failureData,
2181
- error: `Forbidden (HTTP 403): ${errorCode}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
2182
- };
2183
- }
2184
- if (response.status === 429) {
2185
- const retryAfter = response.headers.get("retry-after") || "60";
2186
- return {
2187
- data: failureData,
2188
- error: `Rate limit exceeded (HTTP 429). Wait ${retryAfter}s before retrying. Reduce request frequency or upgrade your plan.`
2189
- };
2190
- }
2191
- return { data: failureData, error: errorCode };
2192
- }
2193
- try {
2194
- const data = JSON.parse(responseText);
2195
- return { data, error: null };
2196
- } catch {
2197
- return { data: { text: responseText }, error: null };
2198
- }
2199
- } catch (err) {
2200
- clearTimeout(timer);
2201
- if (err instanceof Error && err.name === "AbortError") {
2202
- return {
2203
- data: null,
2204
- error: `Edge Function '${functionName}' timed out after ${timeoutMs}ms`
2205
- };
2206
- }
2207
- return { data: null, error: "Network request failed. Please retry." };
2208
- }
2209
- }
2210
-
2211
2396
  // src/lib/rate-limit.ts
2212
2397
  var CATEGORY_CONFIGS = {
2213
2398
  posting: { maxTokens: 30, refillRate: 30 / 60 },
@@ -2324,7 +2509,7 @@ function checkRateLimit(category, key) {
2324
2509
  init_supabase();
2325
2510
 
2326
2511
  // src/lib/version.ts
2327
- var MCP_VERSION = "1.8.2";
2512
+ var MCP_VERSION = "1.9.1";
2328
2513
 
2329
2514
  // src/tools/ideation.ts
2330
2515
  function asEnvelope(data) {
@@ -2725,9 +2910,90 @@ ${content}`,
2725
2910
  }
2726
2911
 
2727
2912
  // src/tools/content.ts
2913
+ init_edge_function();
2728
2914
  import { z as z2 } from "zod";
2729
2915
  init_supabase();
2730
2916
 
2917
+ // src/lib/sanitize-error.ts
2918
+ var ERROR_PATTERNS = [
2919
+ // Postgres / PostgREST
2920
+ [
2921
+ /PGRST301|permission denied/i,
2922
+ "Access denied. Check your account permissions."
2923
+ ],
2924
+ [
2925
+ /42P01|does not exist/i,
2926
+ "Service temporarily unavailable. Please try again."
2927
+ ],
2928
+ [
2929
+ /23505|unique.*constraint|duplicate key/i,
2930
+ "A duplicate record already exists."
2931
+ ],
2932
+ [/23503|foreign key/i, "Referenced record not found."],
2933
+ // Gemini / Google AI
2934
+ [
2935
+ /google.*api.*key|googleapis\.com.*40[13]/i,
2936
+ "Content generation failed. Please try again."
2937
+ ],
2938
+ [
2939
+ /RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
2940
+ "AI service rate limit reached. Please wait and retry."
2941
+ ],
2942
+ [
2943
+ /SAFETY|prompt.*blocked|content.*filter/i,
2944
+ "Content was blocked by the AI safety filter. Try rephrasing."
2945
+ ],
2946
+ [
2947
+ /gemini.*error|generativelanguage/i,
2948
+ "Content generation failed. Please try again."
2949
+ ],
2950
+ // Kie.ai
2951
+ [/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
2952
+ // Stripe
2953
+ [
2954
+ /stripe.*api|sk_live_|sk_test_/i,
2955
+ "Payment processing error. Please try again."
2956
+ ],
2957
+ // Network / fetch
2958
+ [
2959
+ /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
2960
+ "External service unavailable. Please try again."
2961
+ ],
2962
+ [
2963
+ /fetch failed|network error|abort.*timeout/i,
2964
+ "Network request failed. Please try again."
2965
+ ],
2966
+ [/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
2967
+ // Supabase Edge Function internals
2968
+ [
2969
+ /FunctionsHttpError|non-2xx status/i,
2970
+ "Backend service error. Please try again."
2971
+ ],
2972
+ [
2973
+ /JWT|token.*expired|token.*invalid/i,
2974
+ "Authentication expired. Please re-authenticate."
2975
+ ],
2976
+ // Generic sensitive patterns (API keys, URLs with secrets)
2977
+ [
2978
+ /[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i,
2979
+ "An internal error occurred. Please try again."
2980
+ ]
2981
+ ];
2982
+ var UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
2983
+ var EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
2984
+ function redactSensitiveIdentifiers(message) {
2985
+ return message.replace(EMAIL_PATTERN, "[redacted-email]").replace(UUID_PATTERN, "[redacted-id]");
2986
+ }
2987
+ function sanitizeError(error) {
2988
+ const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
2989
+ for (const [pattern, userMessage] of ERROR_PATTERNS) {
2990
+ if (pattern.test(msg)) {
2991
+ return userMessage;
2992
+ }
2993
+ }
2994
+ return "An unexpected error occurred. Please try again.";
2995
+ }
2996
+
2731
2997
  // src/lib/checkStatusShape.ts
2732
2998
  function buildCheckStatusPayload(job, liveStatus) {
2733
2999
  const status = liveStatus?.status ?? job.status;
@@ -2777,8 +3043,14 @@ function buildCheckStatusPayload(job, liveStatus) {
2777
3043
 
2778
3044
  // src/lib/budget.ts
2779
3045
  init_request_context();
2780
- var MAX_CREDITS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
2781
- var MAX_ASSETS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0));
3046
+ var MAX_CREDITS_PER_RUN = Math.max(
3047
+ 0,
3048
+ Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0)
3049
+ );
3050
+ var MAX_ASSETS_PER_RUN = Math.max(
3051
+ 0,
3052
+ Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0)
3053
+ );
2782
3054
  var _globalCreditsUsed = 0;
2783
3055
  var _globalAssetsGenerated = 0;
2784
3056
  function getCreditsUsed() {
@@ -2823,9 +3095,22 @@ function checkCreditBudget(estimatedCost) {
2823
3095
  }
2824
3096
  const used = getCreditsUsed();
2825
3097
  if (used + estimatedCost > MAX_CREDITS_PER_RUN) {
3098
+ 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.`;
2826
3099
  return {
2827
3100
  ok: false,
2828
- message: `Credit budget exceeded for this MCP run. Used=${used}, next~=${estimatedCost}, limit=${MAX_CREDITS_PER_RUN}.`
3101
+ message,
3102
+ error: toolError("billing_error", message, {
3103
+ recover_with: [
3104
+ "Raise SOCIALNEURON_MAX_CREDITS_PER_RUN to a higher credit ceiling.",
3105
+ "Unset SOCIALNEURON_MAX_CREDITS_PER_RUN to remove the per-run cap."
3106
+ ],
3107
+ details: {
3108
+ env_var: "SOCIALNEURON_MAX_CREDITS_PER_RUN",
3109
+ credits_used_this_run: used,
3110
+ estimated_call_cost: estimatedCost,
3111
+ max_credits_per_run: MAX_CREDITS_PER_RUN
3112
+ }
3113
+ })
2829
3114
  };
2830
3115
  }
2831
3116
  return { ok: true };
@@ -2875,6 +3160,10 @@ var VIDEO_CREDIT_ESTIMATES = {
2875
3160
  "seedance-1.5-pro": 150,
2876
3161
  kling: 170
2877
3162
  };
3163
+ var AUDIO_NATIVE_DEFAULT_MODELS = /* @__PURE__ */ new Set([
3164
+ "seedance-2-fast",
3165
+ "seedance-2"
3166
+ ]);
2878
3167
  var VIDEO_MODEL_ENUM = [
2879
3168
  "seedance-2-fast",
2880
3169
  "kling-3",
@@ -2903,7 +3192,7 @@ var IMAGE_CREDIT_ESTIMATES = {
2903
3192
  function registerContentTools(server) {
2904
3193
  server.tool(
2905
3194
  "generate_video",
2906
- "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.",
3195
+ "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.",
2907
3196
  {
2908
3197
  prompt: z2.string().max(2500).describe(
2909
3198
  '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.'
@@ -2918,7 +3207,7 @@ function registerContentTools(server) {
2918
3207
  "Video aspect ratio. 16:9 for YouTube/landscape, 9:16 for TikTok/Reels/Shorts, 1:1 for Instagram feed/square. Defaults to 16:9."
2919
3208
  ),
2920
3209
  enable_audio: z2.boolean().optional().describe(
2921
- "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."
3210
+ "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."
2922
3211
  ),
2923
3212
  image_url: z2.string().optional().describe(
2924
3213
  "Start frame image URL for image-to-video (Kling 3.0 frame control)."
@@ -2954,12 +3243,12 @@ function registerContentTools(server) {
2954
3243
  const estimatedCost = VIDEO_CREDIT_ESTIMATES[model] ?? 120;
2955
3244
  const budgetCheck = checkCreditBudget(estimatedCost);
2956
3245
  if (!budgetCheck.ok) {
2957
- return {
2958
- content: [{ type: "text", text: budgetCheck.message }],
2959
- isError: true
2960
- };
3246
+ return budgetCheck.error;
2961
3247
  }
2962
- const rateLimit = checkRateLimit("generation", `generate_video:${userId}`);
3248
+ const rateLimit = checkRateLimit(
3249
+ "generation",
3250
+ `generate_video:${userId}`
3251
+ );
2963
3252
  if (!rateLimit.allowed) {
2964
3253
  return {
2965
3254
  content: [
@@ -2978,9 +3267,12 @@ function registerContentTools(server) {
2978
3267
  model,
2979
3268
  duration: duration ?? 5,
2980
3269
  aspectRatio: aspect_ratio ?? "16:9",
2981
- // Default FALSE (2026-07-13): the old `?? true` default silently multiplied
2982
- // kling-family costs (2.65x on kling 2.6) for callers that never asked for audio.
2983
- enableAudio: enable_audio ?? false,
3270
+ // Default FALSE for most models (2026-07-13): the old `?? true` default silently
3271
+ // multiplied kling-family costs (2.65x on kling 2.6) for callers that never asked
3272
+ // for audio. Exception (2026-07-17): the seedance-2 family bills audio into its base
3273
+ // cost and generates it natively, so a false default there shipped silent no-audio
3274
+ // mp4s — those models default TRUE when the caller omits enable_audio.
3275
+ enableAudio: enable_audio ?? AUDIO_NATIVE_DEFAULT_MODELS.has(model),
2984
3276
  ...image_url && { imageUrl: image_url },
2985
3277
  ...end_frame_url && { endFrameUrl: end_frame_url },
2986
3278
  // The server reads projectId and enforces
@@ -3082,7 +3374,14 @@ function registerContentTools(server) {
3082
3374
  project_id: z2.string().optional().describe("Project ID to associate the generated image with."),
3083
3375
  response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
3084
3376
  },
3085
- async ({ prompt, model, aspect_ratio, image_url, project_id, response_format }) => {
3377
+ async ({
3378
+ prompt,
3379
+ model,
3380
+ aspect_ratio,
3381
+ image_url,
3382
+ project_id,
3383
+ response_format
3384
+ }) => {
3086
3385
  const format = response_format ?? "text";
3087
3386
  const userId = await getDefaultUserId();
3088
3387
  const assetBudget = checkAssetBudget();
@@ -3095,12 +3394,12 @@ function registerContentTools(server) {
3095
3394
  const estimatedCost = IMAGE_CREDIT_ESTIMATES[model] ?? 30;
3096
3395
  const budgetCheck = checkCreditBudget(estimatedCost);
3097
3396
  if (!budgetCheck.ok) {
3098
- return {
3099
- content: [{ type: "text", text: budgetCheck.message }],
3100
- isError: true
3101
- };
3397
+ return budgetCheck.error;
3102
3398
  }
3103
- const rateLimit = checkRateLimit("generation", `generate_image:${userId}`);
3399
+ const rateLimit = checkRateLimit(
3400
+ "generation",
3401
+ `generate_image:${userId}`
3402
+ );
3104
3403
  if (!rateLimit.allowed) {
3105
3404
  return {
3106
3405
  content: [
@@ -3231,6 +3530,15 @@ function registerContentTools(server) {
3231
3530
  isError: true
3232
3531
  };
3233
3532
  }
3533
+ let projectsDisclosure;
3534
+ const ownScopedProjectId = await getDefaultProjectId();
3535
+ if (!ownScopedProjectId) {
3536
+ const ownUserId = await getDefaultUserId().catch(() => null);
3537
+ if (ownUserId) {
3538
+ const list = await listAccessibleProjectsWithAccountStatus(ownUserId);
3539
+ if (list.length > 0) projectsDisclosure = list;
3540
+ }
3541
+ }
3234
3542
  if (job.external_id && (job.status === "pending" || job.status === "processing")) {
3235
3543
  const { data: liveStatus } = await callEdgeFunction(
3236
3544
  "kie-task-status",
@@ -3254,7 +3562,9 @@ function registerContentTools(server) {
3254
3562
  if (livePayload.error) {
3255
3563
  lines2.push(`Error: ${livePayload.error}`);
3256
3564
  }
3257
- const fallbackDisclosure2 = buildFallbackDisclosureLine(job.result_metadata);
3565
+ const fallbackDisclosure2 = buildFallbackDisclosureLine(
3566
+ job.result_metadata
3567
+ );
3258
3568
  if (fallbackDisclosure2) lines2.push(fallbackDisclosure2);
3259
3569
  lines2.push(`Credits: ${job.credits_cost}`);
3260
3570
  if (job.billing_status) {
@@ -3263,6 +3573,14 @@ function registerContentTools(server) {
3263
3573
  );
3264
3574
  }
3265
3575
  lines2.push(`Created: ${job.created_at}`);
3576
+ if (projectsDisclosure) {
3577
+ lines2.push(
3578
+ "",
3579
+ `Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
3580
+ (p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
3581
+ ).join("; ")}`
3582
+ );
3583
+ }
3266
3584
  if (format === "json") {
3267
3585
  return {
3268
3586
  content: [
@@ -3279,7 +3597,8 @@ function registerContentTools(server) {
3279
3597
  // `job` + `liveStatus`; do not duplicate them here.
3280
3598
  asEnvelope2({
3281
3599
  ...liveStatus,
3282
- ...livePayload
3600
+ ...livePayload,
3601
+ ...projectsDisclosure ? { projects: projectsDisclosure } : {}
3283
3602
  }),
3284
3603
  null,
3285
3604
  2
@@ -3320,9 +3639,11 @@ function registerContentTools(server) {
3320
3639
  );
3321
3640
  }
3322
3641
  if (job.error_message) {
3323
- lines.push(`Error: ${job.error_message}`);
3642
+ lines.push(`Error: ${redactSensitiveIdentifiers(job.error_message)}`);
3324
3643
  }
3325
- const fallbackDisclosure = buildFallbackDisclosureLine(job.result_metadata);
3644
+ const fallbackDisclosure = buildFallbackDisclosureLine(
3645
+ job.result_metadata
3646
+ );
3326
3647
  if (fallbackDisclosure) lines.push(fallbackDisclosure);
3327
3648
  lines.push(`Credits: ${job.credits_cost}`);
3328
3649
  if (job.billing_status) {
@@ -3334,10 +3655,19 @@ function registerContentTools(server) {
3334
3655
  if (job.completed_at) {
3335
3656
  lines.push(`Completed: ${job.completed_at}`);
3336
3657
  }
3658
+ if (projectsDisclosure) {
3659
+ lines.push(
3660
+ "",
3661
+ `Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
3662
+ (p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
3663
+ ).join("; ")}`
3664
+ );
3665
+ }
3337
3666
  if (format === "json") {
3338
3667
  const enriched = {
3339
3668
  ...job,
3340
- ...buildCheckStatusPayload(job)
3669
+ ...buildCheckStatusPayload(job),
3670
+ ...projectsDisclosure ? { projects: projectsDisclosure } : {}
3341
3671
  };
3342
3672
  return {
3343
3673
  content: [
@@ -3463,10 +3793,7 @@ Return ONLY valid JSON in this exact format:
3463
3793
  const estimatedCost = 10;
3464
3794
  const budgetCheck = checkCreditBudget(estimatedCost);
3465
3795
  if (!budgetCheck.ok) {
3466
- return {
3467
- content: [{ type: "text", text: budgetCheck.message }],
3468
- isError: true
3469
- };
3796
+ return budgetCheck.error;
3470
3797
  }
3471
3798
  const { data, error } = await callEdgeFunction(
3472
3799
  "social-neuron-ai",
@@ -3553,10 +3880,7 @@ Return ONLY valid JSON in this exact format:
3553
3880
  const estimatedCost = 15;
3554
3881
  const budgetCheck = checkCreditBudget(estimatedCost);
3555
3882
  if (!budgetCheck.ok) {
3556
- return {
3557
- content: [{ type: "text", text: budgetCheck.message }],
3558
- isError: true
3559
- };
3883
+ return budgetCheck.error;
3560
3884
  }
3561
3885
  const rateLimit = checkRateLimit(
3562
3886
  "generation",
@@ -3719,10 +4043,7 @@ Return ONLY valid JSON in this exact format:
3719
4043
  const estimatedCost = 10 + slideCount * 2;
3720
4044
  const budgetCheck = checkCreditBudget(estimatedCost);
3721
4045
  if (!budgetCheck.ok) {
3722
- return {
3723
- content: [{ type: "text", text: budgetCheck.message }],
3724
- isError: true
3725
- };
4046
+ return budgetCheck.error;
3726
4047
  }
3727
4048
  const userId = await getDefaultUserId();
3728
4049
  const rateLimit = checkRateLimit(
@@ -3798,83 +4119,39 @@ Return ONLY valid JSON in this exact format:
3798
4119
  credits: data.carousel.credits
3799
4120
  }),
3800
4121
  null,
3801
- 2
3802
- )
3803
- }
3804
- ]
3805
- };
3806
- }
3807
- const lines = [
3808
- `Carousel generated successfully.`,
3809
- ` ID: ${data.carousel.id}`,
3810
- ` Template: ${templateId}`,
3811
- ` Style: ${resolvedStyle}`,
3812
- ` Slides: ${data.carousel.slides.length}`,
3813
- ` Credits: ${creditsUsed}`,
3814
- "",
3815
- "Slides:",
3816
- ...data.carousel.slides.map(
3817
- (s, i) => ` ${i + 1}. ${s.headline || "(no headline)"}${s.emphasisWords?.length ? ` [emphasis: ${s.emphasisWords.join(", ")}]` : ""}`
3818
- ),
3819
- "",
3820
- "Next: Use generate_image for each slide, then schedule_post with media_urls to publish as Instagram carousel."
3821
- ];
3822
- return {
3823
- content: [{ type: "text", text: lines.join("\n") }]
3824
- };
3825
- }
3826
- );
3827
- }
3828
-
3829
- // src/tools/distribution.ts
3830
- import { z as z3 } from "zod";
3831
- import { createHash } from "node:crypto";
3832
-
3833
- // src/lib/sanitize-error.ts
3834
- var ERROR_PATTERNS = [
3835
- // Postgres / PostgREST
3836
- [/PGRST301|permission denied/i, "Access denied. Check your account permissions."],
3837
- [/42P01|does not exist/i, "Service temporarily unavailable. Please try again."],
3838
- [/23505|unique.*constraint|duplicate key/i, "A duplicate record already exists."],
3839
- [/23503|foreign key/i, "Referenced record not found."],
3840
- // Gemini / Google AI
3841
- [/google.*api.*key|googleapis\.com.*40[13]/i, "Content generation failed. Please try again."],
3842
- [
3843
- /RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
3844
- "AI service rate limit reached. Please wait and retry."
3845
- ],
3846
- [
3847
- /SAFETY|prompt.*blocked|content.*filter/i,
3848
- "Content was blocked by the AI safety filter. Try rephrasing."
3849
- ],
3850
- [/gemini.*error|generativelanguage/i, "Content generation failed. Please try again."],
3851
- // Kie.ai
3852
- [/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
3853
- // Stripe
3854
- [/stripe.*api|sk_live_|sk_test_/i, "Payment processing error. Please try again."],
3855
- // Network / fetch
3856
- [
3857
- /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
3858
- "External service unavailable. Please try again."
3859
- ],
3860
- [/fetch failed|network error|abort.*timeout/i, "Network request failed. Please try again."],
3861
- [/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
3862
- // Supabase Edge Function internals
3863
- [/FunctionsHttpError|non-2xx status/i, "Backend service error. Please try again."],
3864
- [/JWT|token.*expired|token.*invalid/i, "Authentication expired. Please re-authenticate."],
3865
- // Generic sensitive patterns (API keys, URLs with secrets)
3866
- [/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i, "An internal error occurred. Please try again."]
3867
- ];
3868
- function sanitizeError(error) {
3869
- const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
3870
- for (const [pattern, userMessage] of ERROR_PATTERNS) {
3871
- if (pattern.test(msg)) {
3872
- return userMessage;
4122
+ 2
4123
+ )
4124
+ }
4125
+ ]
4126
+ };
4127
+ }
4128
+ const lines = [
4129
+ `Carousel generated successfully.`,
4130
+ ` ID: ${data.carousel.id}`,
4131
+ ` Template: ${templateId}`,
4132
+ ` Style: ${resolvedStyle}`,
4133
+ ` Slides: ${data.carousel.slides.length}`,
4134
+ ` Credits: ${creditsUsed}`,
4135
+ "",
4136
+ "Slides:",
4137
+ ...data.carousel.slides.map(
4138
+ (s, i) => ` ${i + 1}. ${s.headline || "(no headline)"}${s.emphasisWords?.length ? ` [emphasis: ${s.emphasisWords.join(", ")}]` : ""}`
4139
+ ),
4140
+ "",
4141
+ "Next: Use generate_image for each slide, then schedule_post with media_urls to publish as Instagram carousel."
4142
+ ];
4143
+ return {
4144
+ content: [{ type: "text", text: lines.join("\n") }]
4145
+ };
3873
4146
  }
3874
- }
3875
- return "An unexpected error occurred. Please try again.";
4147
+ );
3876
4148
  }
3877
4149
 
4150
+ // src/tools/distribution.ts
4151
+ init_edge_function();
4152
+ import { z as z3 } from "zod";
4153
+ import { createHash } from "node:crypto";
4154
+
3878
4155
  // src/lib/ssrf.ts
3879
4156
  import { promises as dnsPromises } from "node:dns";
3880
4157
  var BLOCKED_IP_PATTERNS = [
@@ -4153,6 +4430,116 @@ function evaluateQuality(input) {
4153
4430
  };
4154
4431
  }
4155
4432
 
4433
+ // src/lib/connected-account-routing.ts
4434
+ init_edge_function();
4435
+ var PLATFORM_CASE_MAP = {
4436
+ youtube: "YouTube",
4437
+ tiktok: "TikTok",
4438
+ instagram: "Instagram",
4439
+ twitter: "Twitter",
4440
+ x: "Twitter",
4441
+ linkedin: "LinkedIn",
4442
+ facebook: "Facebook",
4443
+ threads: "Threads",
4444
+ bluesky: "Bluesky"
4445
+ };
4446
+ function canonicalPlatform(value) {
4447
+ const normalized = value.trim().toLowerCase();
4448
+ return normalized === "x" ? "twitter" : normalized;
4449
+ }
4450
+ function providerPlatform(value) {
4451
+ return PLATFORM_CASE_MAP[canonicalPlatform(value)] ?? value;
4452
+ }
4453
+ function isUsable(account) {
4454
+ const status = account.effective_status ?? account.status;
4455
+ return status === "active" || status === "expires_soon";
4456
+ }
4457
+ function normalizeRequestedIds(requested) {
4458
+ const ids = /* @__PURE__ */ new Map();
4459
+ for (const [platform2, accountId] of Object.entries(requested ?? {})) {
4460
+ const canonical = canonicalPlatform(platform2);
4461
+ const existing = ids.get(canonical);
4462
+ if (existing && existing !== accountId) {
4463
+ return {
4464
+ error: `Conflicting account IDs were supplied for ${platform2} and its platform alias.`
4465
+ };
4466
+ }
4467
+ ids.set(canonical, accountId);
4468
+ }
4469
+ return { ids };
4470
+ }
4471
+ async function resolveConnectedAccountRouting(input) {
4472
+ const normalizedRequested = normalizeRequestedIds(input.requestedAccountIds);
4473
+ if (normalizedRequested.error) return { error: normalizedRequested.error };
4474
+ const targetPlatforms = new Set(input.platforms.map(canonicalPlatform));
4475
+ for (const requestedPlatform of normalizedRequested.ids?.keys() ?? []) {
4476
+ if (!targetPlatforms.has(requestedPlatform)) {
4477
+ return {
4478
+ error: `An account ID was supplied for untargeted platform ${requestedPlatform}.`
4479
+ };
4480
+ }
4481
+ }
4482
+ const { data, error } = await callEdgeFunction(
4483
+ "mcp-data",
4484
+ {
4485
+ action: "connected-accounts",
4486
+ projectId: input.projectId,
4487
+ project_id: input.projectId
4488
+ },
4489
+ { timeoutMs: 1e4 }
4490
+ );
4491
+ if (error || !Array.isArray(data?.accounts)) {
4492
+ return {
4493
+ error: `Connected-account verification failed: ${error ?? data?.error ?? "no account inventory returned"}.`
4494
+ };
4495
+ }
4496
+ const connectedAccountIds = {};
4497
+ for (const platform2 of input.platforms) {
4498
+ const canonical = canonicalPlatform(platform2);
4499
+ const displayPlatform = providerPlatform(platform2);
4500
+ const platformAccounts = data.accounts.filter(
4501
+ (account) => canonicalPlatform(account.platform) === canonical && account.project_id === input.projectId && isUsable(account)
4502
+ );
4503
+ const requestedId = normalizedRequested.ids?.get(canonical);
4504
+ let selected;
4505
+ if (requestedId) {
4506
+ selected = data.accounts.find((account) => account.id === requestedId);
4507
+ if (!selected) {
4508
+ return {
4509
+ error: `${displayPlatform}: account ${requestedId} is not available for project_id ${input.projectId}.`
4510
+ };
4511
+ }
4512
+ if (canonicalPlatform(selected.platform) !== canonical) {
4513
+ return {
4514
+ error: `${displayPlatform}: account ${requestedId} belongs to ${selected.platform}.`
4515
+ };
4516
+ }
4517
+ if (selected.project_id !== input.projectId) {
4518
+ return {
4519
+ error: `${displayPlatform}: account ${requestedId} is not bound to project_id ${input.projectId}.`
4520
+ };
4521
+ }
4522
+ if (!isUsable(selected)) {
4523
+ return {
4524
+ error: `${displayPlatform}: account ${requestedId} is ${selected.effective_status ?? selected.status}.`
4525
+ };
4526
+ }
4527
+ } else if (platformAccounts.length === 1) {
4528
+ selected = platformAccounts[0];
4529
+ } else if (platformAccounts.length === 0) {
4530
+ return {
4531
+ error: `${displayPlatform}: no active account is bound to project_id ${input.projectId}.`
4532
+ };
4533
+ } else {
4534
+ return {
4535
+ error: `${displayPlatform}: multiple active accounts are bound to project_id ${input.projectId}; pass the exact account ID returned by list_connected_accounts.`
4536
+ };
4537
+ }
4538
+ connectedAccountIds[displayPlatform] = selected.id;
4539
+ }
4540
+ return { connectedAccountIds };
4541
+ }
4542
+
4156
4543
  // src/tools/distribution.ts
4157
4544
  function snakeToCamel(obj) {
4158
4545
  const result = {};
@@ -4170,11 +4557,16 @@ function convertPlatformMetadata(meta) {
4170
4557
  }
4171
4558
  return converted;
4172
4559
  }
4173
- var PLATFORM_CASE_MAP = {
4560
+ var PLATFORM_CASE_MAP2 = {
4174
4561
  youtube: "YouTube",
4175
4562
  tiktok: "TikTok",
4176
4563
  instagram: "Instagram",
4177
4564
  twitter: "Twitter",
4565
+ // 'x' is the platform's current branding but connected_account_routing.ts
4566
+ // (and the DB convention) still key on 'Twitter' — keep both aliases
4567
+ // resolving to the same case so schedule_content_plan's platform:'x' posts
4568
+ // don't fall through to an undefined binding (F8, 2026-07-15).
4569
+ x: "Twitter",
4178
4570
  linkedin: "LinkedIn",
4179
4571
  facebook: "Facebook",
4180
4572
  threads: "Threads",
@@ -4296,21 +4688,6 @@ async function validatePublishMediaUrl(url) {
4296
4688
  function accountEffectiveStatus(account) {
4297
4689
  return account.effective_status || account.status;
4298
4690
  }
4299
- function isUsableAccount(account) {
4300
- const status = accountEffectiveStatus(account);
4301
- return status === "active" || status === "expires_soon";
4302
- }
4303
- function formatAccountChoice(account) {
4304
- const name = account.username ? `@${account.username}` : "unnamed";
4305
- const project = account.project_id ? `project_id=${account.project_id}` : "project_id=unassigned";
4306
- const refresh = account.has_refresh_token ? "OAuth 2.0 refresh" : "no refresh token";
4307
- return `${account.id} (${name}, ${project}, ${accountEffectiveStatus(account)}, ${refresh})`;
4308
- }
4309
- function requestedAccountIdForPlatform(accountId, accountIds, platform2) {
4310
- if (accountId) return accountId;
4311
- if (!accountIds) return void 0;
4312
- return accountIds[platform2] || accountIds[platform2.toLowerCase()];
4313
- }
4314
4691
  async function rehostExternalUrl(mediaUrl, projectId) {
4315
4692
  const ssrf = await validateUrlForSSRF(mediaUrl);
4316
4693
  if (!ssrf.isValid) {
@@ -4446,17 +4823,17 @@ function registerDistributionTools(server) {
4446
4823
  schedule_at: z3.string().optional().describe(
4447
4824
  'ISO 8601 UTC datetime for scheduled posting (e.g. "2026-03-20T14:00:00Z"). Omit to post immediately. Must be in the future.'
4448
4825
  ),
4449
- project_id: z3.string().optional().describe(
4826
+ project_id: z3.string().uuid().optional().describe(
4450
4827
  "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."
4451
4828
  ),
4452
4829
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
4453
4830
  attribution: z3.boolean().optional().describe(
4454
4831
  'If true, appends "Created with Social Neuron" to the caption. Default: false.'
4455
4832
  ),
4456
- account_id: z3.string().optional().describe(
4457
- "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."
4833
+ account_id: z3.string().uuid().optional().describe(
4834
+ "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."
4458
4835
  ),
4459
- account_ids: z3.record(z3.string(), z3.string()).optional().describe(
4836
+ account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
4460
4837
  '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.'
4461
4838
  ),
4462
4839
  auto_rehost: z3.boolean().optional().describe(
@@ -4500,6 +4877,45 @@ function registerDistributionTools(server) {
4500
4877
  isError: true
4501
4878
  };
4502
4879
  }
4880
+ const projectResolution = await resolveProjectForConnectedAccountTool(
4881
+ project_id,
4882
+ platforms
4883
+ );
4884
+ if (!projectResolution.projectId) {
4885
+ return {
4886
+ content: [
4887
+ {
4888
+ type: "text",
4889
+ 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."
4890
+ }
4891
+ ],
4892
+ isError: true
4893
+ };
4894
+ }
4895
+ const resolvedProjectId = projectResolution.projectId;
4896
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
4897
+ if (account_id && account_ids) {
4898
+ return {
4899
+ content: [
4900
+ {
4901
+ type: "text",
4902
+ text: "Pass either account_id or account_ids, not both."
4903
+ }
4904
+ ],
4905
+ isError: true
4906
+ };
4907
+ }
4908
+ if (account_id && platforms.length !== 1) {
4909
+ return {
4910
+ content: [
4911
+ {
4912
+ type: "text",
4913
+ text: "account_id is valid only for a single target platform. Use account_ids for multi-platform publishing."
4914
+ }
4915
+ ],
4916
+ isError: true
4917
+ };
4918
+ }
4503
4919
  const userId = await getDefaultUserId();
4504
4920
  const rateLimit = checkRateLimit("posting", `schedule_post:${userId}`);
4505
4921
  if (!rateLimit.allowed) {
@@ -4604,11 +5020,16 @@ function registerDistributionTools(server) {
4604
5020
  }
4605
5021
  const resolvedJobs = resolved;
4606
5022
  resolvedMediaUrls = resolvedJobs.map((item) => item.url);
4607
- resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map((item) => item.trustedR2);
5023
+ resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map(
5024
+ (item) => item.trustedR2
5025
+ );
4608
5026
  }
4609
5027
  const shouldRehost = auto_rehost !== false;
4610
5028
  if (shouldRehost && resolvedMediaUrl && !resolvedMediaUrlIsTrustedR2) {
4611
- const rehost = await rehostExternalUrl(resolvedMediaUrl, project_id);
5029
+ const rehost = await rehostExternalUrl(
5030
+ resolvedMediaUrl,
5031
+ resolvedProjectId
5032
+ );
4612
5033
  if ("error" in rehost) {
4613
5034
  return {
4614
5035
  content: [
@@ -4626,7 +5047,7 @@ function registerDistributionTools(server) {
4626
5047
  if (shouldRehost && resolvedMediaUrls && resolvedMediaUrls.length > 0) {
4627
5048
  const rehosted = await Promise.all(
4628
5049
  resolvedMediaUrls.map(
4629
- (u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, project_id)
5050
+ (u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, resolvedProjectId)
4630
5051
  )
4631
5052
  );
4632
5053
  const failIdx = rehosted.findIndex((r) => "error" in r);
@@ -4693,7 +5114,7 @@ function registerDistributionTools(server) {
4693
5114
  }
4694
5115
  }
4695
5116
  const normalizedPlatforms = platforms.map(
4696
- (p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
5117
+ (p) => PLATFORM_CASE_MAP2[p.toLowerCase()] || p
4697
5118
  );
4698
5119
  const blockedPlatforms = normalizedPlatforms.filter(
4699
5120
  (p) => MCP_NOT_LIVE_FOR_POSTING.has(p)
@@ -4709,92 +5130,27 @@ function registerDistributionTools(server) {
4709
5130
  isError: true
4710
5131
  };
4711
5132
  }
4712
- const { data: accountsData } = await callEdgeFunction(
4713
- "mcp-data",
4714
- {
4715
- action: "connected-accounts",
4716
- ...project_id ? { projectId: project_id, project_id } : {}
4717
- },
4718
- { timeoutMs: 1e4 }
4719
- );
4720
- if (accountsData?.accounts) {
4721
- const accounts = accountsData.accounts;
4722
- const issues = [];
4723
- for (const platform2 of normalizedPlatforms) {
4724
- const platformAccounts = accounts.filter(
4725
- (a) => a.platform.toLowerCase() === platform2.toLowerCase() && isUsableAccount(a)
4726
- );
4727
- const requestedAccountId = requestedAccountIdForPlatform(
4728
- account_id,
4729
- account_ids,
4730
- platform2
4731
- );
4732
- if (requestedAccountId) {
4733
- const selected = accounts.find((a) => a.id === requestedAccountId);
4734
- if (!selected) {
4735
- issues.push(
4736
- `${platform2}: 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.`
4737
- );
4738
- } else if (selected.platform.toLowerCase() !== platform2.toLowerCase()) {
4739
- issues.push(
4740
- `${platform2}: Account "${requestedAccountId}" belongs to ${selected.platform}, not ${platform2}.`
4741
- );
4742
- } else if (!isUsableAccount(selected)) {
4743
- issues.push(
4744
- `${platform2}: Account "${selected.username || selected.id}" is ${accountEffectiveStatus(selected)}. Reconnect at socialneuron.com/settings/connections.`
4745
- );
4746
- } else if (project_id && selected.project_id && selected.project_id !== project_id) {
4747
- issues.push(
4748
- `${platform2}: 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.`
4749
- );
4750
- }
4751
- continue;
4752
- }
4753
- if (platformAccounts.length === 0) {
4754
- issues.push(
4755
- `${platform2}: 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="${platform2.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.`
4756
- );
4757
- } else if (platformAccounts.length > 1) {
4758
- const accountList = platformAccounts.map((a) => ` - ${formatAccountChoice(a)}`).join("\n");
4759
- issues.push(
4760
- `${platform2}: Multiple accounts found. Specify account_id or account_ids to choose:
4761
- ${accountList}`
4762
- );
4763
- } else if (platformAccounts.length === 1) {
4764
- const acct = platformAccounts[0];
4765
- if (acct.expires_at && new Date(acct.expires_at) < /* @__PURE__ */ new Date() && !acct.has_refresh_token) {
4766
- issues.push(
4767
- `${platform2}: Account "${acct.username || acct.id}" has expired OAuth and no refresh token. Reconnect at socialneuron.com/settings/connections.`
4768
- );
4769
- }
4770
- }
4771
- }
4772
- if (issues.length > 0) {
4773
- return {
4774
- content: [
4775
- {
4776
- type: "text",
4777
- text: `Cannot post \u2014 account issues found:
4778
-
4779
- ${issues.join("\n\n")}`
4780
- }
4781
- ],
4782
- isError: true
4783
- };
4784
- }
4785
- }
4786
- let connectedAccountIds;
5133
+ let requestedAccountIds;
4787
5134
  if (account_id) {
4788
- connectedAccountIds = {};
4789
- for (const p of normalizedPlatforms) {
4790
- connectedAccountIds[p] = account_id;
4791
- }
5135
+ requestedAccountIds = { [normalizedPlatforms[0]]: account_id };
4792
5136
  } else if (account_ids) {
4793
- connectedAccountIds = {};
4794
- for (const [key, val] of Object.entries(account_ids)) {
4795
- const normalizedKey = PLATFORM_CASE_MAP[key.toLowerCase()] || key;
4796
- connectedAccountIds[normalizedKey] = val;
4797
- }
5137
+ requestedAccountIds = account_ids;
5138
+ }
5139
+ const routing = await resolveConnectedAccountRouting({
5140
+ projectId: resolvedProjectId,
5141
+ platforms: normalizedPlatforms,
5142
+ requestedAccountIds
5143
+ });
5144
+ if (routing.error || !routing.connectedAccountIds) {
5145
+ return {
5146
+ content: [
5147
+ {
5148
+ type: "text",
5149
+ text: `Cannot post \u2014 ${routing.error ?? "exact connected-account routing could not be established."}`
5150
+ }
5151
+ ],
5152
+ isError: true
5153
+ };
4798
5154
  }
4799
5155
  let finalCaption = caption;
4800
5156
  if (attribution && finalCaption) {
@@ -4848,8 +5204,9 @@ Created with Social Neuron`;
4848
5204
  title,
4849
5205
  hashtags,
4850
5206
  scheduledAt: schedule_at,
4851
- projectId: project_id,
4852
- ...connectedAccountIds ? { connectedAccountIds } : {},
5207
+ projectId: resolvedProjectId,
5208
+ project_id: resolvedProjectId,
5209
+ connectedAccountIds: routing.connectedAccountIds,
4853
5210
  ...normalizedPlatformMetadata ? {
4854
5211
  platformMetadata: convertPlatformMetadata(
4855
5212
  normalizedPlatformMetadata
@@ -4884,10 +5241,14 @@ Created with Social Neuron`;
4884
5241
  isError: true
4885
5242
  };
4886
5243
  }
5244
+ const responseData = projectAutoResolvedNote ? { ...data, project_auto_resolved: projectAutoResolvedNote } : data;
4887
5245
  const lines = [
4888
5246
  data.success ? "Post scheduled successfully." : "Post scheduling had errors.",
4889
5247
  `Scheduled for: ${data.scheduledAt}`
4890
5248
  ];
5249
+ if (projectAutoResolvedNote) {
5250
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
5251
+ }
4891
5252
  if (tiktokAutoInboxApplied) {
4892
5253
  lines.push(
4893
5254
  "",
@@ -4905,7 +5266,7 @@ Created with Social Neuron`;
4905
5266
  }
4906
5267
  }
4907
5268
  if (format === "json") {
4908
- const structuredContent = asEnvelope3(data);
5269
+ const structuredContent = asEnvelope3(responseData);
4909
5270
  return {
4910
5271
  structuredContent,
4911
5272
  content: [
@@ -4918,7 +5279,7 @@ Created with Social Neuron`;
4918
5279
  };
4919
5280
  }
4920
5281
  return {
4921
- structuredContent: asEnvelope3(data),
5282
+ structuredContent: asEnvelope3(responseData),
4922
5283
  content: [{ type: "text", text: lines.join("\n") }],
4923
5284
  isError: !data.success
4924
5285
  };
@@ -4932,7 +5293,9 @@ Created with Social Neuron`;
4932
5293
  project_id: z3.string().uuid().optional().describe(
4933
5294
  "Brand/project ID that owns the post. Defaults to the authenticated key's project or the account default."
4934
5295
  ),
4935
- scheduled_at: z3.string().datetime({ offset: true }).describe("New future publish time as an ISO 8601 datetime with timezone."),
5296
+ scheduled_at: z3.string().datetime({ offset: true }).describe(
5297
+ "New future publish time as an ISO 8601 datetime with timezone."
5298
+ ),
4936
5299
  expected_scheduled_at: z3.string().datetime({ offset: true }).optional().describe(
4937
5300
  "Optional current schedule timestamp. If it changed since you read it, the update is rejected instead of silently overwriting it."
4938
5301
  ),
@@ -4988,7 +5351,11 @@ Created with Social Neuron`;
4988
5351
  projectId: resolvedProjectId,
4989
5352
  project_id: resolvedProjectId,
4990
5353
  scheduled_at: next.toISOString(),
4991
- ...expected_scheduled_at ? { expected_scheduled_at: new Date(expected_scheduled_at).toISOString() } : {}
5354
+ ...expected_scheduled_at ? {
5355
+ expected_scheduled_at: new Date(
5356
+ expected_scheduled_at
5357
+ ).toISOString()
5358
+ } : {}
4992
5359
  });
4993
5360
  if (error || !result?.success) {
4994
5361
  const code = result?.error ?? error ?? "reschedule_failed";
@@ -5021,17 +5388,34 @@ Created with Social Neuron`;
5021
5388
  "list_connected_accounts",
5022
5389
  "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.",
5023
5390
  {
5024
- project_id: z3.string().optional().describe(
5391
+ project_id: z3.string().uuid().optional().describe(
5025
5392
  "Brand/project ID to scope connected accounts. Use the same project_id when calling schedule_post."
5026
5393
  ),
5027
- include_all: z3.boolean().optional().describe("If true, include expired or inactive accounts as well as usable accounts."),
5394
+ include_all: z3.boolean().optional().describe(
5395
+ "If true, include expired or inactive accounts as well as usable accounts."
5396
+ ),
5028
5397
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
5029
5398
  },
5030
5399
  async ({ project_id, include_all, response_format }) => {
5031
5400
  const format = response_format ?? "text";
5401
+ const projectResolution = await resolveProjectForConnectedAccountTool(project_id);
5402
+ if (!projectResolution.projectId) {
5403
+ return {
5404
+ content: [
5405
+ {
5406
+ type: "text",
5407
+ 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."
5408
+ }
5409
+ ],
5410
+ isError: true
5411
+ };
5412
+ }
5413
+ const resolvedProjectId = projectResolution.projectId;
5414
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
5032
5415
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
5033
5416
  action: "connected-accounts",
5034
- ...project_id ? { projectId: project_id, project_id } : {},
5417
+ projectId: resolvedProjectId,
5418
+ project_id: resolvedProjectId,
5035
5419
  ...include_all ? { includeAll: true } : {}
5036
5420
  });
5037
5421
  if (efError || !result?.success) {
@@ -5045,10 +5429,27 @@ Created with Social Neuron`;
5045
5429
  isError: true
5046
5430
  };
5047
5431
  }
5048
- const accounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
5432
+ const parsedAccounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
5433
+ if (parsedAccounts.some(
5434
+ (account) => account.project_id !== resolvedProjectId
5435
+ )) {
5436
+ return {
5437
+ content: [
5438
+ {
5439
+ type: "text",
5440
+ text: "Connected-account project attestation failed. No account inventory was returned."
5441
+ }
5442
+ ],
5443
+ isError: true
5444
+ };
5445
+ }
5446
+ const accounts = parsedAccounts;
5049
5447
  if (accounts.length === 0) {
5050
5448
  if (format === "json") {
5051
- const structuredContent = asEnvelope3({ accounts: [] });
5449
+ const structuredContent = asEnvelope3({
5450
+ accounts: [],
5451
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
5452
+ });
5052
5453
  return {
5053
5454
  structuredContent,
5054
5455
  content: [
@@ -5063,13 +5464,15 @@ Created with Social Neuron`;
5063
5464
  content: [
5064
5465
  {
5065
5466
  type: "text",
5066
- text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections."
5467
+ text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections." + (projectAutoResolvedNote ? `
5468
+
5469
+ Note: ${projectAutoResolvedNote}` : "")
5067
5470
  }
5068
5471
  ]
5069
5472
  };
5070
5473
  }
5071
5474
  const lines = [
5072
- `${accounts.length} connected account(s)${project_id ? ` for project ${project_id}` : ""}:`,
5475
+ `${accounts.length} connected account(s) for project ${resolvedProjectId}:`,
5073
5476
  ""
5074
5477
  ];
5075
5478
  for (const account of accounts) {
@@ -5081,8 +5484,14 @@ Created with Social Neuron`;
5081
5484
  ` ${platformLower}: ${name} | id=${account.id} | ${project} | status=${status} (connected ${account.created_at.split("T")[0]})`
5082
5485
  );
5083
5486
  }
5487
+ if (projectAutoResolvedNote) {
5488
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
5489
+ }
5084
5490
  if (format === "json") {
5085
- const structuredContent = asEnvelope3({ accounts });
5491
+ const structuredContent = asEnvelope3({
5492
+ accounts,
5493
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
5494
+ });
5086
5495
  return {
5087
5496
  structuredContent,
5088
5497
  content: [
@@ -5277,7 +5686,10 @@ Created with Social Neuron`;
5277
5686
  if (!Number.isFinite(startDate.getTime())) {
5278
5687
  return {
5279
5688
  content: [
5280
- { type: "text", text: "start_after must be a valid ISO datetime." }
5689
+ {
5690
+ type: "text",
5691
+ text: "start_after must be a valid ISO datetime."
5692
+ }
5281
5693
  ],
5282
5694
  isError: true
5283
5695
  };
@@ -5378,11 +5790,14 @@ Created with Social Neuron`;
5378
5790
  "Schedule all posts in a content plan. Optionally auto-assigns time slots and runs quality checks before scheduling. Supports dry-run mode.",
5379
5791
  {
5380
5792
  plan: z3.object({
5793
+ project_id: z3.string().uuid().optional(),
5794
+ account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe("Exact connected-account ID per platform."),
5381
5795
  posts: z3.array(
5382
5796
  z3.object({
5383
5797
  id: z3.string(),
5384
5798
  caption: z3.string(),
5385
5799
  platform: z3.string(),
5800
+ connected_account_id: z3.string().uuid().optional(),
5386
5801
  title: z3.string().optional(),
5387
5802
  media_url: z3.string().optional(),
5388
5803
  schedule_at: z3.string().optional(),
@@ -5390,6 +5805,12 @@ Created with Social Neuron`;
5390
5805
  })
5391
5806
  )
5392
5807
  }).passthrough().optional(),
5808
+ project_id: z3.string().uuid().optional().describe(
5809
+ "Exact brand/project ID. Defaults only when the authenticated user has one project."
5810
+ ),
5811
+ account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
5812
+ "Exact connected-account ID per platform for every post in the plan."
5813
+ ),
5393
5814
  plan_id: z3.string().uuid().optional().describe("Persisted content plan ID from content_plans table"),
5394
5815
  auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
5395
5816
  dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
@@ -5406,6 +5827,8 @@ Created with Social Neuron`;
5406
5827
  async ({
5407
5828
  plan,
5408
5829
  plan_id,
5830
+ project_id,
5831
+ account_ids,
5409
5832
  auto_slot,
5410
5833
  dry_run,
5411
5834
  response_format,
@@ -5415,9 +5838,11 @@ Created with Social Neuron`;
5415
5838
  idempotency_seed
5416
5839
  }) => {
5417
5840
  try {
5841
+ const effectiveBatchSize = batch_size ?? 4;
5418
5842
  let workingPlan = plan;
5419
5843
  let effectivePlanId = plan_id;
5420
- let effectiveProjectId;
5844
+ let effectiveProjectId = project_id;
5845
+ let projectAutoResolvedNote;
5421
5846
  let approvalSummary;
5422
5847
  if (!workingPlan && plan_id) {
5423
5848
  const { data: planResult, error: planError } = await callEdgeFunction("mcp-data", { action: "get-content-plan", plan_id });
@@ -5464,7 +5889,18 @@ Created with Social Neuron`;
5464
5889
  posts: postsFromPayload
5465
5890
  };
5466
5891
  effectivePlanId = stored.id;
5467
- effectiveProjectId = stored.project_id ?? void 0;
5892
+ if (effectiveProjectId && stored.project_id && effectiveProjectId !== stored.project_id) {
5893
+ return {
5894
+ content: [
5895
+ {
5896
+ type: "text",
5897
+ text: `project_id ${effectiveProjectId} does not own plan ${plan_id}.`
5898
+ }
5899
+ ],
5900
+ isError: true
5901
+ };
5902
+ }
5903
+ effectiveProjectId = stored.project_id ?? effectiveProjectId;
5468
5904
  }
5469
5905
  if (!workingPlan) {
5470
5906
  return {
@@ -5477,10 +5913,35 @@ Created with Social Neuron`;
5477
5913
  isError: true
5478
5914
  };
5479
5915
  }
5916
+ const planProjectId = workingPlan.project_id;
5917
+ if (effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0 && effectiveProjectId !== planProjectId) {
5918
+ return {
5919
+ content: [
5920
+ {
5921
+ type: "text",
5922
+ text: `Conflicting project_id values were supplied for the plan (${planProjectId}) and request (${effectiveProjectId}).`
5923
+ }
5924
+ ],
5925
+ isError: true
5926
+ };
5927
+ }
5928
+ if (!effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0) {
5929
+ effectiveProjectId = planProjectId;
5930
+ }
5480
5931
  if (!effectiveProjectId) {
5481
- const planProjectId = workingPlan.project_id;
5482
- if (typeof planProjectId === "string" && planProjectId.length > 0) {
5483
- effectiveProjectId = planProjectId;
5932
+ const projectResolution = await resolveProjectForConnectedAccountTool();
5933
+ effectiveProjectId = projectResolution.projectId;
5934
+ projectAutoResolvedNote = projectResolution.autoResolvedNote;
5935
+ if (!effectiveProjectId) {
5936
+ return {
5937
+ content: [
5938
+ {
5939
+ type: "text",
5940
+ 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."
5941
+ }
5942
+ ],
5943
+ isError: true
5944
+ };
5484
5945
  }
5485
5946
  }
5486
5947
  if (effectivePlanId) {
@@ -5695,10 +6156,65 @@ Created with Social Neuron`;
5695
6156
  `Summary: ${passed}/${workingPlan.posts.length} passed quality check`
5696
6157
  );
5697
6158
  return {
5698
- content: [{ type: "text", text: lines2.join("\n") }],
5699
- isError: false
6159
+ content: [{ type: "text", text: lines2.join("\n") }],
6160
+ isError: false
6161
+ };
6162
+ }
6163
+ const embeddedAccountIds = workingPlan.account_ids ?? {};
6164
+ for (const [platform2, accountId] of Object.entries(account_ids ?? {})) {
6165
+ if (embeddedAccountIds[platform2] && embeddedAccountIds[platform2] !== accountId) {
6166
+ return {
6167
+ content: [
6168
+ {
6169
+ type: "text",
6170
+ text: `Conflicting account_ids values were supplied for ${platform2}.`
6171
+ }
6172
+ ],
6173
+ isError: true
6174
+ };
6175
+ }
6176
+ }
6177
+ const requestedPlanAccountIds = {
6178
+ ...embeddedAccountIds,
6179
+ ...account_ids ?? {}
6180
+ };
6181
+ for (const post of workingPlan.posts) {
6182
+ if (!post.connected_account_id) continue;
6183
+ const key = post.platform.toLowerCase() === "x" ? "twitter" : post.platform.toLowerCase();
6184
+ const existing = requestedPlanAccountIds[key];
6185
+ if (existing && existing !== post.connected_account_id) {
6186
+ return {
6187
+ content: [
6188
+ {
6189
+ type: "text",
6190
+ text: `Plan contains conflicting connected_account_id values for ${post.platform}. Use separate plans when scheduling the same platform through different accounts.`
6191
+ }
6192
+ ],
6193
+ isError: true
6194
+ };
6195
+ }
6196
+ requestedPlanAccountIds[key] = post.connected_account_id;
6197
+ }
6198
+ const planPlatforms = Array.from(
6199
+ new Set(workingPlan.posts.map((post) => post.platform))
6200
+ );
6201
+ const planRouting = await resolveConnectedAccountRouting({
6202
+ projectId: effectiveProjectId,
6203
+ platforms: planPlatforms,
6204
+ requestedAccountIds: requestedPlanAccountIds
6205
+ });
6206
+ if (planRouting.error || !planRouting.connectedAccountIds) {
6207
+ return {
6208
+ content: [
6209
+ {
6210
+ type: "text",
6211
+ text: `Cannot schedule plan \u2014 ${planRouting.error ?? "exact connected-account routing could not be established."}`
6212
+ }
6213
+ ],
6214
+ isError: true
5700
6215
  };
5701
6216
  }
6217
+ const verifiedPlanAccountIds = planRouting.connectedAccountIds;
5702
6218
  let scheduled = 0;
5703
6219
  let failed = 0;
5704
6220
  const results = [];
@@ -5727,7 +6243,7 @@ Created with Social Neuron`;
5727
6243
  retryable: false
5728
6244
  };
5729
6245
  }
5730
- const normalizedPlatform = PLATFORM_CASE_MAP[post.platform.toLowerCase()] ?? post.platform;
6246
+ const normalizedPlatform = PLATFORM_CASE_MAP2[post.platform.toLowerCase()] ?? post.platform;
5731
6247
  const idempotencyKey = buildIdempotencyKey(post);
5732
6248
  const { data, error } = await callEdgeFunction(
5733
6249
  "schedule-post",
@@ -5738,6 +6254,13 @@ Created with Social Neuron`;
5738
6254
  mediaUrl: post.media_url,
5739
6255
  scheduledAt: post.schedule_at,
5740
6256
  hashtags: post.hashtags,
6257
+ ...effectiveProjectId ? {
6258
+ projectId: effectiveProjectId,
6259
+ project_id: effectiveProjectId
6260
+ } : {},
6261
+ connectedAccountIds: {
6262
+ [normalizedPlatform]: verifiedPlanAccountIds[normalizedPlatform]
6263
+ },
5741
6264
  ...effectivePlanId ? { planId: effectivePlanId } : {},
5742
6265
  idempotencyKey
5743
6266
  },
@@ -5796,7 +6319,7 @@ Created with Social Neuron`;
5796
6319
  const platformBatches = Array.from(grouped.entries()).map(
5797
6320
  async ([platform2, platformPosts]) => {
5798
6321
  const platformResults = [];
5799
- const batches = chunk(platformPosts, batch_size);
6322
+ const batches = chunk(platformPosts, effectiveBatchSize);
5800
6323
  for (const batch of batches) {
5801
6324
  const settled = await Promise.allSettled(
5802
6325
  batch.map((post) => scheduleOne(post))
@@ -5857,7 +6380,8 @@ Created with Social Neuron`;
5857
6380
  total_posts: workingPlan.posts.length,
5858
6381
  scheduled,
5859
6382
  failed
5860
- }
6383
+ },
6384
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
5861
6385
  }),
5862
6386
  null,
5863
6387
  2
@@ -5881,6 +6405,9 @@ Created with Social Neuron`;
5881
6405
  lines.push(
5882
6406
  `Scheduled: ${scheduled}/${workingPlan.posts.length} | Failed: ${failed}/${workingPlan.posts.length}`
5883
6407
  );
6408
+ if (projectAutoResolvedNote) {
6409
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
6410
+ }
5884
6411
  return {
5885
6412
  content: [{ type: "text", text: lines.join("\n") }],
5886
6413
  isError: failed > 0
@@ -5902,6 +6429,7 @@ Created with Social Neuron`;
5902
6429
  }
5903
6430
 
5904
6431
  // src/tools/media.ts
6432
+ init_edge_function();
5905
6433
  import { z as z4 } from "zod";
5906
6434
  import { readFile } from "node:fs/promises";
5907
6435
  import { basename, extname } from "node:path";
@@ -6371,6 +6899,7 @@ function registerMediaTools(server) {
6371
6899
 
6372
6900
  // src/tools/analytics.ts
6373
6901
  init_supabase();
6902
+ init_edge_function();
6374
6903
  import { z as z5 } from "zod";
6375
6904
  function asEnvelope4(data) {
6376
6905
  return {
@@ -6402,15 +6931,35 @@ function registerAnalyticsTools(server) {
6402
6931
  content_id: z5.string().uuid().optional().describe(
6403
6932
  "Filter to a specific content_history ID to see performance of one piece of content."
6404
6933
  ),
6405
- project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
6934
+ project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
6406
6935
  limit: z5.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
6407
6936
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6408
6937
  },
6409
- async ({ platform: platform2, days, content_id, project_id, limit, response_format }) => {
6938
+ async ({
6939
+ platform: platform2,
6940
+ days,
6941
+ content_id,
6942
+ project_id,
6943
+ limit,
6944
+ response_format
6945
+ }) => {
6410
6946
  const format = response_format ?? "text";
6411
6947
  const lookbackDays = days ?? 30;
6412
6948
  const maxPosts = limit ?? 20;
6413
- const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
6949
+ const projectResolution = await resolveProjectStrict(project_id);
6950
+ if (!projectResolution.projectId) {
6951
+ return {
6952
+ content: [
6953
+ {
6954
+ type: "text",
6955
+ 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."
6956
+ }
6957
+ ],
6958
+ isError: true
6959
+ };
6960
+ }
6961
+ const resolvedProjectId = projectResolution.projectId;
6962
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
6414
6963
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
6415
6964
  action: "analytics",
6416
6965
  platform: platform2,
@@ -6421,11 +6970,17 @@ function registerAnalyticsTools(server) {
6421
6970
  limit: Math.min(maxPosts * 5, 100),
6422
6971
  latestOnly: true,
6423
6972
  contentId: content_id,
6424
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
6973
+ projectId: resolvedProjectId,
6974
+ project_id: resolvedProjectId
6425
6975
  });
6426
6976
  if (efError) {
6427
6977
  return {
6428
- content: [{ type: "text", text: `Failed to fetch analytics: ${efError}` }],
6978
+ content: [
6979
+ {
6980
+ type: "text",
6981
+ text: `Failed to fetch analytics: ${efError}`
6982
+ }
6983
+ ],
6429
6984
  isError: true
6430
6985
  };
6431
6986
  }
@@ -6445,7 +7000,8 @@ function registerAnalyticsTools(server) {
6445
7000
  totalViews: 0,
6446
7001
  totalEngagement: 0,
6447
7002
  postCount: 0,
6448
- posts: []
7003
+ posts: [],
7004
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
6449
7005
  });
6450
7006
  return {
6451
7007
  structuredContent,
@@ -6461,7 +7017,9 @@ function registerAnalyticsTools(server) {
6461
7017
  content: [
6462
7018
  {
6463
7019
  type: "text",
6464
- text: `No analytics data found for the last ${lookbackDays} days${platform2 ? ` on ${platform2}` : ""}.`
7020
+ text: `No analytics data found for the last ${lookbackDays} days${platform2 ? ` on ${platform2}` : ""}.` + (projectAutoResolvedNote ? `
7021
+
7022
+ Note: ${projectAutoResolvedNote}` : "")
6465
7023
  }
6466
7024
  ]
6467
7025
  };
@@ -6493,20 +7051,27 @@ function registerAnalyticsTools(server) {
6493
7051
  postCount: posts.length,
6494
7052
  posts
6495
7053
  };
6496
- return formatAnalytics(summary, lookbackDays, format);
7054
+ return formatAnalytics(
7055
+ summary,
7056
+ lookbackDays,
7057
+ format,
7058
+ projectAutoResolvedNote
7059
+ );
6497
7060
  }
6498
7061
  );
6499
7062
  server.tool(
6500
7063
  "refresh_platform_analytics",
6501
7064
  "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.",
6502
7065
  {
6503
- project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
7066
+ project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
6504
7067
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6505
7068
  },
6506
7069
  async ({ project_id, response_format }) => {
6507
7070
  const format = response_format ?? "text";
6508
7071
  const userId = await getDefaultUserId();
6509
- const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
7072
+ const projectResolution = await resolveProjectStrict(project_id);
7073
+ const resolvedProjectId = projectResolution.projectId;
7074
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
6510
7075
  const rateLimit = checkRateLimit(
6511
7076
  "posting",
6512
7077
  `refresh_platform_analytics:${userId}:${resolvedProjectId ?? "default"}`
@@ -6522,25 +7087,48 @@ function registerAnalyticsTools(server) {
6522
7087
  isError: true
6523
7088
  };
6524
7089
  }
7090
+ if (!resolvedProjectId) {
7091
+ return {
7092
+ content: [
7093
+ {
7094
+ type: "text",
7095
+ 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."
7096
+ }
7097
+ ],
7098
+ isError: true
7099
+ };
7100
+ }
6525
7101
  const { data, error } = await callEdgeFunction("fetch-analytics", {
6526
7102
  userId,
6527
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
7103
+ projectId: resolvedProjectId,
7104
+ project_id: resolvedProjectId
6528
7105
  });
6529
7106
  if (error) {
6530
7107
  return {
6531
- content: [{ type: "text", text: `Error refreshing analytics: ${error}` }],
7108
+ content: [
7109
+ {
7110
+ type: "text",
7111
+ text: `Error refreshing analytics: ${error}`
7112
+ }
7113
+ ],
6532
7114
  isError: true
6533
7115
  };
6534
7116
  }
6535
7117
  const result = data;
6536
7118
  if (!result.success) {
6537
7119
  return {
6538
- content: [{ type: "text", text: "Analytics refresh failed." }],
7120
+ content: [
7121
+ { type: "text", text: "Analytics refresh failed." }
7122
+ ],
6539
7123
  isError: true
6540
7124
  };
6541
7125
  }
6542
- const queued = (result.results ?? []).filter((r) => r.status === "queued").length;
6543
- const errored = (result.results ?? []).filter((r) => r.status === "error").length;
7126
+ const queued = (result.results ?? []).filter(
7127
+ (r) => r.status === "queued"
7128
+ ).length;
7129
+ const errored = (result.results ?? []).filter(
7130
+ (r) => r.status === "error"
7131
+ ).length;
6544
7132
  const lines = [
6545
7133
  `Analytics refresh triggered successfully.`,
6546
7134
  ` Posts processed: ${result.postsProcessed}`,
@@ -6549,13 +7137,17 @@ function registerAnalyticsTools(server) {
6549
7137
  if (errored > 0) {
6550
7138
  lines.push(` Errors: ${errored}`);
6551
7139
  }
7140
+ if (projectAutoResolvedNote) {
7141
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
7142
+ }
6552
7143
  if (format === "json") {
6553
7144
  const structuredContent = asEnvelope4({
6554
7145
  success: true,
6555
7146
  postsProcessed: result.postsProcessed,
6556
7147
  queued,
6557
7148
  errored,
6558
- projectId: resolvedProjectId ?? null
7149
+ projectId: resolvedProjectId ?? null,
7150
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
6559
7151
  });
6560
7152
  return {
6561
7153
  structuredContent,
@@ -6571,12 +7163,21 @@ function registerAnalyticsTools(server) {
6571
7163
  }
6572
7164
  );
6573
7165
  }
6574
- function formatAnalytics(summary, days, format) {
6575
- const structuredContent = asEnvelope4({ ...summary, days });
7166
+ function formatAnalytics(summary, days, format, projectAutoResolvedNote) {
7167
+ const structuredContent = asEnvelope4({
7168
+ ...summary,
7169
+ days,
7170
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
7171
+ });
6576
7172
  if (format === "json") {
6577
7173
  return {
6578
7174
  structuredContent,
6579
- content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }]
7175
+ content: [
7176
+ {
7177
+ type: "text",
7178
+ text: JSON.stringify(structuredContent, null, 2)
7179
+ }
7180
+ ]
6580
7181
  };
6581
7182
  }
6582
7183
  const lines = [
@@ -6600,6 +7201,9 @@ function formatAnalytics(summary, days, format) {
6600
7201
  lines.push(line);
6601
7202
  }
6602
7203
  }
7204
+ if (projectAutoResolvedNote) {
7205
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
7206
+ }
6603
7207
  return {
6604
7208
  structuredContent,
6605
7209
  content: [{ type: "text", text: lines.join("\n") }]
@@ -6607,8 +7211,9 @@ function formatAnalytics(summary, days, format) {
6607
7211
  }
6608
7212
 
6609
7213
  // src/tools/brand.ts
6610
- import { z as z6 } from "zod";
7214
+ init_edge_function();
6611
7215
  init_supabase();
7216
+ import { z as z6 } from "zod";
6612
7217
 
6613
7218
  // src/lib/brandUrlInput.ts
6614
7219
  var PLATFORM_ALIASES = {
@@ -7348,6 +7953,7 @@ import { z as z8 } from "zod";
7348
7953
  import { resolve as resolve2 } from "node:path";
7349
7954
  import { mkdir as mkdir2 } from "node:fs/promises";
7350
7955
  init_supabase();
7956
+ init_edge_function();
7351
7957
  var COMPOSITIONS = [
7352
7958
  {
7353
7959
  id: "CaptionedClip",
@@ -7699,8 +8305,9 @@ function registerRemotionTools(server) {
7699
8305
  }
7700
8306
 
7701
8307
  // src/tools/insights.ts
7702
- import { z as z9 } from "zod";
8308
+ init_edge_function();
7703
8309
  init_supabase();
8310
+ import { z as z9 } from "zod";
7704
8311
  var MAX_INSIGHT_AGE_DAYS = 30;
7705
8312
  var PLATFORM_ENUM = [
7706
8313
  "youtube",
@@ -7956,6 +8563,8 @@ function registerInsightsTools(server) {
7956
8563
  }
7957
8564
 
7958
8565
  // src/tools/youtube-analytics.ts
8566
+ init_edge_function();
8567
+ init_supabase();
7959
8568
  import { z as z10 } from "zod";
7960
8569
  function asEnvelope7(data) {
7961
8570
  return {
@@ -7977,15 +8586,64 @@ function registerYouTubeAnalyticsTools(server) {
7977
8586
  start_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Start date in YYYY-MM-DD format."),
7978
8587
  end_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("End date in YYYY-MM-DD format."),
7979
8588
  video_id: z10.string().optional().describe('YouTube video ID. Required when action is "video".'),
7980
- max_results: z10.number().min(1).max(50).optional().describe('Max videos to return for "topVideos" action. Defaults to 10.'),
8589
+ max_results: z10.number().min(1).max(50).optional().describe(
8590
+ 'Max videos to return for "topVideos" action. Defaults to 10.'
8591
+ ),
8592
+ connected_account_id: z10.string().uuid().optional().describe(
8593
+ "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."
8594
+ ),
8595
+ project_id: z10.string().uuid().optional().describe(
8596
+ "Exact brand/project ID. Defaults only when the authenticated user has one project."
8597
+ ),
7981
8598
  response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7982
8599
  },
7983
- async ({ action, start_date, end_date, video_id, max_results, response_format }) => {
8600
+ async ({
8601
+ action,
8602
+ start_date,
8603
+ end_date,
8604
+ video_id,
8605
+ max_results,
8606
+ connected_account_id,
8607
+ project_id,
8608
+ response_format
8609
+ }) => {
7984
8610
  const format = response_format ?? "text";
7985
8611
  if (action === "video" && !video_id) {
7986
8612
  return {
7987
8613
  content: [
7988
- { type: "text", text: 'Error: video_id is required when action is "video".' }
8614
+ {
8615
+ type: "text",
8616
+ text: 'Error: video_id is required when action is "video".'
8617
+ }
8618
+ ],
8619
+ isError: true
8620
+ };
8621
+ }
8622
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
8623
+ if (!resolvedProjectId) {
8624
+ return {
8625
+ content: [
8626
+ {
8627
+ type: "text",
8628
+ text: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
8629
+ }
8630
+ ],
8631
+ isError: true
8632
+ };
8633
+ }
8634
+ const routing = await resolveConnectedAccountRouting({
8635
+ projectId: resolvedProjectId,
8636
+ platforms: ["youtube"],
8637
+ requestedAccountIds: connected_account_id ? { youtube: connected_account_id } : void 0
8638
+ });
8639
+ const resolvedAccountId = routing.connectedAccountIds?.YouTube;
8640
+ if (routing.error || !resolvedAccountId) {
8641
+ return {
8642
+ content: [
8643
+ {
8644
+ type: "text",
8645
+ text: routing.error ?? "YouTube: exact connected-account routing could not be established."
8646
+ }
7989
8647
  ],
7990
8648
  isError: true
7991
8649
  };
@@ -7995,11 +8653,19 @@ function registerYouTubeAnalyticsTools(server) {
7995
8653
  startDate: start_date,
7996
8654
  endDate: end_date,
7997
8655
  videoId: video_id,
7998
- maxResults: max_results ?? 10
8656
+ maxResults: max_results ?? 10,
8657
+ projectId: resolvedProjectId,
8658
+ project_id: resolvedProjectId,
8659
+ connectedAccountId: resolvedAccountId
7999
8660
  });
8000
8661
  if (error) {
8001
8662
  return {
8002
- content: [{ type: "text", text: `YouTube Analytics error: ${error}` }],
8663
+ content: [
8664
+ {
8665
+ type: "text",
8666
+ text: `YouTube Analytics error: ${error}`
8667
+ }
8668
+ ],
8003
8669
  isError: true
8004
8670
  };
8005
8671
  }
@@ -8012,7 +8678,12 @@ function registerYouTubeAnalyticsTools(server) {
8012
8678
  {
8013
8679
  type: "text",
8014
8680
  text: JSON.stringify(
8015
- asEnvelope7({ action, startDate: start_date, endDate: end_date, analytics: a }),
8681
+ asEnvelope7({
8682
+ action,
8683
+ startDate: start_date,
8684
+ endDate: end_date,
8685
+ analytics: a
8686
+ }),
8016
8687
  null,
8017
8688
  2
8018
8689
  )
@@ -8039,7 +8710,10 @@ function registerYouTubeAnalyticsTools(server) {
8039
8710
  if (days.length === 0) {
8040
8711
  return {
8041
8712
  content: [
8042
- { type: "text", text: "No daily analytics data found for this period." }
8713
+ {
8714
+ type: "text",
8715
+ text: "No daily analytics data found for this period."
8716
+ }
8043
8717
  ]
8044
8718
  };
8045
8719
  }
@@ -8062,7 +8736,10 @@ function registerYouTubeAnalyticsTools(server) {
8062
8736
  ]
8063
8737
  };
8064
8738
  }
8065
- const lines = [`YouTube Daily Analytics (${start_date} to ${end_date}):`, ""];
8739
+ const lines = [
8740
+ `YouTube Daily Analytics (${start_date} to ${end_date}):`,
8741
+ ""
8742
+ ];
8066
8743
  for (const d of days) {
8067
8744
  lines.push(
8068
8745
  ` ${d.date}: ${d.views.toLocaleString()} views, ${d.watchTimeMinutes.toLocaleString()} min watch, +${d.subscribersGained} subs, ${d.likes} likes, ${d.comments} comments`
@@ -8109,7 +8786,12 @@ function registerYouTubeAnalyticsTools(server) {
8109
8786
  const videos = result.topVideos ?? [];
8110
8787
  if (videos.length === 0) {
8111
8788
  return {
8112
- content: [{ type: "text", text: "No top videos found for this period." }]
8789
+ content: [
8790
+ {
8791
+ type: "text",
8792
+ text: "No top videos found for this period."
8793
+ }
8794
+ ]
8113
8795
  };
8114
8796
  }
8115
8797
  if (format === "json") {
@@ -8131,7 +8813,10 @@ function registerYouTubeAnalyticsTools(server) {
8131
8813
  ]
8132
8814
  };
8133
8815
  }
8134
- const lines = [`Top ${videos.length} YouTube Videos (${start_date} to ${end_date}):`, ""];
8816
+ const lines = [
8817
+ `Top ${videos.length} YouTube Videos (${start_date} to ${end_date}):`,
8818
+ ""
8819
+ ];
8135
8820
  for (let i = 0; i < videos.length; i++) {
8136
8821
  const v = videos[i];
8137
8822
  lines.push(
@@ -8144,17 +8829,25 @@ function registerYouTubeAnalyticsTools(server) {
8144
8829
  }
8145
8830
  if (format === "json") {
8146
8831
  return {
8147
- content: [{ type: "text", text: JSON.stringify(asEnvelope7(result), null, 2) }]
8832
+ content: [
8833
+ {
8834
+ type: "text",
8835
+ text: JSON.stringify(asEnvelope7(result), null, 2)
8836
+ }
8837
+ ]
8148
8838
  };
8149
8839
  }
8150
8840
  return {
8151
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
8841
+ content: [
8842
+ { type: "text", text: JSON.stringify(result, null, 2) }
8843
+ ]
8152
8844
  };
8153
8845
  }
8154
8846
  );
8155
8847
  }
8156
8848
 
8157
8849
  // src/tools/comments.ts
8850
+ init_edge_function();
8158
8851
  import { z as z11 } from "zod";
8159
8852
  init_supabase();
8160
8853
  function asEnvelope8(data) {
@@ -8166,6 +8859,32 @@ function asEnvelope8(data) {
8166
8859
  data
8167
8860
  };
8168
8861
  }
8862
+ async function exactYouTubeRoute(projectId, connectedAccountId) {
8863
+ const resolvedProjectId = projectId ?? await getDefaultProjectId() ?? void 0;
8864
+ if (!resolvedProjectId) {
8865
+ return {
8866
+ error: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
8867
+ };
8868
+ }
8869
+ const routing = await resolveConnectedAccountRouting({
8870
+ projectId: resolvedProjectId,
8871
+ platforms: ["youtube"],
8872
+ requestedAccountIds: connectedAccountId ? { youtube: connectedAccountId } : void 0
8873
+ });
8874
+ const resolvedAccountId = routing.connectedAccountIds?.YouTube;
8875
+ if (routing.error || !resolvedAccountId) {
8876
+ return {
8877
+ error: routing.error ?? "YouTube: exact connected-account routing could not be established."
8878
+ };
8879
+ }
8880
+ return {
8881
+ projectId: resolvedProjectId,
8882
+ connectedAccountId: resolvedAccountId
8883
+ };
8884
+ }
8885
+ var PROJECT_ID_SCHEMA = z11.string().uuid().optional().describe(
8886
+ "Exact brand/project ID. Defaults only when the authenticated user has one project."
8887
+ );
8169
8888
  function registerCommentsTools(server) {
8170
8889
  server.tool(
8171
8890
  "list_comments",
@@ -8178,19 +8897,42 @@ function registerCommentsTools(server) {
8178
8897
  page_token: z11.string().optional().describe(
8179
8898
  "Pagination cursor from previous list_comments response nextPageToken field. Omit for first page of results."
8180
8899
  ),
8900
+ connected_account_id: z11.string().uuid().optional().describe(
8901
+ "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."
8902
+ ),
8903
+ project_id: PROJECT_ID_SCHEMA,
8181
8904
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8182
8905
  },
8183
- async ({ video_id, max_results, page_token, response_format }) => {
8906
+ async ({
8907
+ video_id,
8908
+ max_results,
8909
+ page_token,
8910
+ connected_account_id,
8911
+ project_id,
8912
+ response_format
8913
+ }) => {
8184
8914
  const format = response_format ?? "text";
8915
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
8916
+ if ("error" in route) {
8917
+ return {
8918
+ content: [{ type: "text", text: route.error }],
8919
+ isError: true
8920
+ };
8921
+ }
8185
8922
  const { data, error } = await callEdgeFunction("youtube-comments", {
8186
8923
  action: "list",
8187
8924
  videoId: video_id,
8188
8925
  maxResults: max_results ?? 50,
8189
- pageToken: page_token
8926
+ pageToken: page_token,
8927
+ projectId: route.projectId,
8928
+ project_id: route.projectId,
8929
+ connectedAccountId: route.connectedAccountId
8190
8930
  });
8191
8931
  if (error) {
8192
8932
  return {
8193
- content: [{ type: "text", text: `Error listing comments: ${error}` }],
8933
+ content: [
8934
+ { type: "text", text: `Error listing comments: ${error}` }
8935
+ ],
8194
8936
  isError: true
8195
8937
  };
8196
8938
  }
@@ -8202,7 +8944,10 @@ function registerCommentsTools(server) {
8202
8944
  {
8203
8945
  type: "text",
8204
8946
  text: JSON.stringify(
8205
- asEnvelope8({ comments, nextPageToken: result.nextPageToken ?? null }),
8947
+ asEnvelope8({
8948
+ comments,
8949
+ nextPageToken: result.nextPageToken ?? null
8950
+ }),
8206
8951
  null,
8207
8952
  2
8208
8953
  )
@@ -8239,12 +8984,31 @@ function registerCommentsTools(server) {
8239
8984
  "reply_to_comment",
8240
8985
  "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.",
8241
8986
  {
8242
- parent_id: z11.string().describe("The ID of the parent comment to reply to (from list_comments)."),
8987
+ parent_id: z11.string().describe(
8988
+ "The ID of the parent comment to reply to (from list_comments)."
8989
+ ),
8243
8990
  text: z11.string().min(1).describe("The reply text."),
8991
+ connected_account_id: z11.string().uuid().optional().describe(
8992
+ "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."
8993
+ ),
8994
+ project_id: PROJECT_ID_SCHEMA,
8244
8995
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8245
8996
  },
8246
- async ({ parent_id, text, response_format }) => {
8997
+ async ({
8998
+ parent_id,
8999
+ text,
9000
+ connected_account_id,
9001
+ project_id,
9002
+ response_format
9003
+ }) => {
8247
9004
  const format = response_format ?? "text";
9005
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
9006
+ if ("error" in route) {
9007
+ return {
9008
+ content: [{ type: "text", text: route.error }],
9009
+ isError: true
9010
+ };
9011
+ }
8248
9012
  const userId = await getDefaultUserId();
8249
9013
  const rateLimit = checkRateLimit("posting", `reply_to_comment:${userId}`);
8250
9014
  if (!rateLimit.allowed) {
@@ -8261,18 +9025,31 @@ function registerCommentsTools(server) {
8261
9025
  const { data, error } = await callEdgeFunction("youtube-comments", {
8262
9026
  action: "reply",
8263
9027
  parentId: parent_id,
8264
- text
9028
+ text,
9029
+ projectId: route.projectId,
9030
+ project_id: route.projectId,
9031
+ connectedAccountId: route.connectedAccountId
8265
9032
  });
8266
9033
  if (error) {
8267
9034
  return {
8268
- content: [{ type: "text", text: `Error replying to comment: ${error}` }],
9035
+ content: [
9036
+ {
9037
+ type: "text",
9038
+ text: `Error replying to comment: ${error}`
9039
+ }
9040
+ ],
8269
9041
  isError: true
8270
9042
  };
8271
9043
  }
8272
9044
  const result = data;
8273
9045
  if (format === "json") {
8274
9046
  return {
8275
- content: [{ type: "text", text: JSON.stringify(asEnvelope8(result), null, 2) }]
9047
+ content: [
9048
+ {
9049
+ type: "text",
9050
+ text: JSON.stringify(asEnvelope8(result), null, 2)
9051
+ }
9052
+ ]
8276
9053
  };
8277
9054
  }
8278
9055
  return {
@@ -8293,10 +9070,27 @@ function registerCommentsTools(server) {
8293
9070
  {
8294
9071
  video_id: z11.string().describe("The YouTube video ID to comment on."),
8295
9072
  text: z11.string().min(1).describe("The comment text."),
9073
+ connected_account_id: z11.string().uuid().optional().describe(
9074
+ "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."
9075
+ ),
9076
+ project_id: PROJECT_ID_SCHEMA,
8296
9077
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8297
9078
  },
8298
- async ({ video_id, text, response_format }) => {
9079
+ async ({
9080
+ video_id,
9081
+ text,
9082
+ connected_account_id,
9083
+ project_id,
9084
+ response_format
9085
+ }) => {
8299
9086
  const format = response_format ?? "text";
9087
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
9088
+ if ("error" in route) {
9089
+ return {
9090
+ content: [{ type: "text", text: route.error }],
9091
+ isError: true
9092
+ };
9093
+ }
8300
9094
  const userId = await getDefaultUserId();
8301
9095
  const rateLimit = checkRateLimit("posting", `post_comment:${userId}`);
8302
9096
  if (!rateLimit.allowed) {
@@ -8313,18 +9107,28 @@ function registerCommentsTools(server) {
8313
9107
  const { data, error } = await callEdgeFunction("youtube-comments", {
8314
9108
  action: "post",
8315
9109
  videoId: video_id,
8316
- text
9110
+ text,
9111
+ projectId: route.projectId,
9112
+ project_id: route.projectId,
9113
+ connectedAccountId: route.connectedAccountId
8317
9114
  });
8318
9115
  if (error) {
8319
9116
  return {
8320
- content: [{ type: "text", text: `Error posting comment: ${error}` }],
9117
+ content: [
9118
+ { type: "text", text: `Error posting comment: ${error}` }
9119
+ ],
8321
9120
  isError: true
8322
9121
  };
8323
9122
  }
8324
9123
  const result = data;
8325
9124
  if (format === "json") {
8326
9125
  return {
8327
- content: [{ type: "text", text: JSON.stringify(asEnvelope8(result), null, 2) }]
9126
+ content: [
9127
+ {
9128
+ type: "text",
9129
+ text: JSON.stringify(asEnvelope8(result), null, 2)
9130
+ }
9131
+ ]
8328
9132
  };
8329
9133
  }
8330
9134
  return {
@@ -8345,10 +9149,27 @@ function registerCommentsTools(server) {
8345
9149
  {
8346
9150
  comment_id: z11.string().describe("The comment ID to moderate."),
8347
9151
  moderation_status: z11.enum(["published", "rejected"]).describe('"published" to approve, "rejected" to hide.'),
9152
+ connected_account_id: z11.string().uuid().optional().describe(
9153
+ "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."
9154
+ ),
9155
+ project_id: PROJECT_ID_SCHEMA,
8348
9156
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8349
9157
  },
8350
- async ({ comment_id, moderation_status, response_format }) => {
9158
+ async ({
9159
+ comment_id,
9160
+ moderation_status,
9161
+ connected_account_id,
9162
+ project_id,
9163
+ response_format
9164
+ }) => {
8351
9165
  const format = response_format ?? "text";
9166
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
9167
+ if ("error" in route) {
9168
+ return {
9169
+ content: [{ type: "text", text: route.error }],
9170
+ isError: true
9171
+ };
9172
+ }
8352
9173
  const userId = await getDefaultUserId();
8353
9174
  const rateLimit = checkRateLimit("posting", `moderate_comment:${userId}`);
8354
9175
  if (!rateLimit.allowed) {
@@ -8365,11 +9186,19 @@ function registerCommentsTools(server) {
8365
9186
  const { error } = await callEdgeFunction("youtube-comments", {
8366
9187
  action: "moderate",
8367
9188
  commentId: comment_id,
8368
- moderationStatus: moderation_status
9189
+ moderationStatus: moderation_status,
9190
+ projectId: route.projectId,
9191
+ project_id: route.projectId,
9192
+ connectedAccountId: route.connectedAccountId
8369
9193
  });
8370
9194
  if (error) {
8371
9195
  return {
8372
- content: [{ type: "text", text: `Error moderating comment: ${error}` }],
9196
+ content: [
9197
+ {
9198
+ type: "text",
9199
+ text: `Error moderating comment: ${error}`
9200
+ }
9201
+ ],
8373
9202
  isError: true
8374
9203
  };
8375
9204
  }
@@ -8406,10 +9235,26 @@ function registerCommentsTools(server) {
8406
9235
  "Delete a YouTube comment. Only works for comments owned by the authenticated channel.",
8407
9236
  {
8408
9237
  comment_id: z11.string().describe("The comment ID to delete."),
9238
+ connected_account_id: z11.string().uuid().optional().describe(
9239
+ "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."
9240
+ ),
9241
+ project_id: PROJECT_ID_SCHEMA,
8409
9242
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8410
9243
  },
8411
- async ({ comment_id, response_format }) => {
9244
+ async ({
9245
+ comment_id,
9246
+ connected_account_id,
9247
+ project_id,
9248
+ response_format
9249
+ }) => {
8412
9250
  const format = response_format ?? "text";
9251
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
9252
+ if ("error" in route) {
9253
+ return {
9254
+ content: [{ type: "text", text: route.error }],
9255
+ isError: true
9256
+ };
9257
+ }
8413
9258
  const userId = await getDefaultUserId();
8414
9259
  const rateLimit = checkRateLimit("posting", `delete_comment:${userId}`);
8415
9260
  if (!rateLimit.allowed) {
@@ -8425,11 +9270,16 @@ function registerCommentsTools(server) {
8425
9270
  }
8426
9271
  const { error } = await callEdgeFunction("youtube-comments", {
8427
9272
  action: "delete",
8428
- commentId: comment_id
9273
+ commentId: comment_id,
9274
+ projectId: route.projectId,
9275
+ project_id: route.projectId,
9276
+ connectedAccountId: route.connectedAccountId
8429
9277
  });
8430
9278
  if (error) {
8431
9279
  return {
8432
- content: [{ type: "text", text: `Error deleting comment: ${error}` }],
9280
+ content: [
9281
+ { type: "text", text: `Error deleting comment: ${error}` }
9282
+ ],
8433
9283
  isError: true
8434
9284
  };
8435
9285
  }
@@ -8438,13 +9288,22 @@ function registerCommentsTools(server) {
8438
9288
  content: [
8439
9289
  {
8440
9290
  type: "text",
8441
- text: JSON.stringify(asEnvelope8({ success: true, commentId: comment_id }), null, 2)
9291
+ text: JSON.stringify(
9292
+ asEnvelope8({ success: true, commentId: comment_id }),
9293
+ null,
9294
+ 2
9295
+ )
8442
9296
  }
8443
9297
  ]
8444
9298
  };
8445
9299
  }
8446
9300
  return {
8447
- content: [{ type: "text", text: `Comment ${comment_id} deleted successfully.` }]
9301
+ content: [
9302
+ {
9303
+ type: "text",
9304
+ text: `Comment ${comment_id} deleted successfully.`
9305
+ }
9306
+ ]
8448
9307
  };
8449
9308
  }
8450
9309
  );
@@ -8452,6 +9311,7 @@ function registerCommentsTools(server) {
8452
9311
 
8453
9312
  // src/tools/ideation-context.ts
8454
9313
  init_supabase();
9314
+ init_edge_function();
8455
9315
  import { z as z12 } from "zod";
8456
9316
  function asEnvelope9(data) {
8457
9317
  return {
@@ -8524,6 +9384,7 @@ function registerIdeationContextTools(server) {
8524
9384
  }
8525
9385
 
8526
9386
  // src/tools/credits.ts
9387
+ init_edge_function();
8527
9388
  import { z as z13 } from "zod";
8528
9389
  function asEnvelope10(data) {
8529
9390
  return {
@@ -8619,6 +9480,7 @@ Assets remaining: ${payload.remainingAssets ?? "unlimited"}`
8619
9480
 
8620
9481
  // src/tools/loop-summary.ts
8621
9482
  init_supabase();
9483
+ init_edge_function();
8622
9484
  import { z as z14 } from "zod";
8623
9485
  function asEnvelope11(data) {
8624
9486
  return {
@@ -8692,6 +9554,7 @@ Next Action: ${payload.recommendedNextAction}`
8692
9554
  }
8693
9555
 
8694
9556
  // src/tools/usage.ts
9557
+ init_edge_function();
8695
9558
  import { z as z15 } from "zod";
8696
9559
  function asEnvelope12(data) {
8697
9560
  return {
@@ -8764,6 +9627,7 @@ ${"=".repeat(40)}
8764
9627
  }
8765
9628
 
8766
9629
  // src/tools/autopilot.ts
9630
+ init_edge_function();
8767
9631
  import { z as z16 } from "zod";
8768
9632
  function asEnvelope13(data) {
8769
9633
  return {
@@ -9062,6 +9926,7 @@ Active: ${is_active}`
9062
9926
  }
9063
9927
 
9064
9928
  // src/tools/recipes.ts
9929
+ init_edge_function();
9065
9930
  import { z as z17 } from "zod";
9066
9931
  function asEnvelope14(data) {
9067
9932
  return {
@@ -9331,6 +10196,7 @@ ${JSON.stringify(run.outputs, null, 2)}
9331
10196
  import { z as z18 } from "zod";
9332
10197
 
9333
10198
  // src/lib/urlExtraction.ts
10199
+ init_edge_function();
9334
10200
  function classifyYouTubeUrl(url) {
9335
10201
  if (/youtube\.com\/watch|youtu\.be\//.test(url)) return "video";
9336
10202
  if (/youtube\.com\/@/.test(url)) return "channel";
@@ -10194,6 +11060,7 @@ function registerVisualQualityTools(server) {
10194
11060
  }
10195
11061
 
10196
11062
  // src/tools/planning.ts
11063
+ init_edge_function();
10197
11064
  import { z as z21 } from "zod";
10198
11065
  import { randomUUID as randomUUID2 } from "node:crypto";
10199
11066
  init_supabase();
@@ -10835,8 +11702,9 @@ ${rawText.slice(0, 1e3)}`
10835
11702
  }
10836
11703
 
10837
11704
  // src/tools/plan-approvals.ts
10838
- import { z as z22 } from "zod";
11705
+ init_edge_function();
10839
11706
  init_supabase();
11707
+ import { z as z22 } from "zod";
10840
11708
  function asEnvelope19(data) {
10841
11709
  return {
10842
11710
  _meta: {
@@ -11065,6 +11933,14 @@ init_supabase();
11065
11933
  init_request_context();
11066
11934
  var KNOWLEDGE_BASE_URL = "https://socialneuron.com/for-developers";
11067
11935
  var KNOWLEDGE_SEARCH_LIMIT = 10;
11936
+ function isHostedTransport() {
11937
+ return process.env.MCP_TRANSPORT === "http";
11938
+ }
11939
+ function isPubliclyDiscoverable(tool) {
11940
+ if (tool.internal || tool.hiddenFromPublicCount) return false;
11941
+ if (tool.localOnly && isHostedTransport()) return false;
11942
+ return true;
11943
+ }
11068
11944
  var SearchOutputSchema = {
11069
11945
  results: z23.array(
11070
11946
  z23.object({
@@ -11137,7 +12013,8 @@ function toolKnowledgeDocument(tool) {
11137
12013
  if (tool.task_intent) lines.push(`Task intent: ${tool.task_intent}`);
11138
12014
  if (tool.use_when) lines.push(`Use when: ${tool.use_when}`);
11139
12015
  if (tool.avoid_when) lines.push(`Avoid when: ${tool.avoid_when}`);
11140
- if (tool.next_tools?.length) lines.push(`Common next tools: ${tool.next_tools.join(", ")}`);
12016
+ if (tool.next_tools?.length)
12017
+ lines.push(`Common next tools: ${tool.next_tools.join(", ")}`);
11141
12018
  return {
11142
12019
  id: `tool:${tool.name}`,
11143
12020
  title: `MCP tool: ${tool.name}`,
@@ -11154,9 +12031,7 @@ function toolKnowledgeDocument(tool) {
11154
12031
  function getKnowledgeDocuments() {
11155
12032
  return [
11156
12033
  ...STATIC_KNOWLEDGE_DOCUMENTS,
11157
- ...TOOL_CATALOG.filter((t) => !t.internal && !t.hiddenFromPublicCount).map(
11158
- toolKnowledgeDocument
11159
- )
12034
+ ...TOOL_CATALOG.filter(isPubliclyDiscoverable).map(toolKnowledgeDocument)
11160
12035
  ];
11161
12036
  }
11162
12037
  function tokenize(input) {
@@ -11210,7 +12085,9 @@ function registerDiscoveryTools(server) {
11210
12085
  };
11211
12086
  return {
11212
12087
  structuredContent,
11213
- content: [{ type: "text", text: JSON.stringify(structuredContent) }]
12088
+ content: [
12089
+ { type: "text", text: JSON.stringify(structuredContent) }
12090
+ ]
11214
12091
  };
11215
12092
  }
11216
12093
  );
@@ -11231,10 +12108,14 @@ function registerDiscoveryTools(server) {
11231
12108
  }
11232
12109
  },
11233
12110
  async ({ id }) => {
11234
- const doc = getKnowledgeDocuments().find((candidate) => candidate.id === id);
12111
+ const doc = getKnowledgeDocuments().find(
12112
+ (candidate) => candidate.id === id
12113
+ );
11235
12114
  if (!doc) {
11236
12115
  return {
11237
- content: [{ type: "text", text: `Document not found: ${id}` }],
12116
+ content: [
12117
+ { type: "text", text: `Document not found: ${id}` }
12118
+ ],
11238
12119
  isError: true
11239
12120
  };
11240
12121
  }
@@ -11247,7 +12128,9 @@ function registerDiscoveryTools(server) {
11247
12128
  };
11248
12129
  return {
11249
12130
  structuredContent,
11250
- content: [{ type: "text", text: JSON.stringify(structuredContent) }]
12131
+ content: [
12132
+ { type: "text", text: JSON.stringify(structuredContent) }
12133
+ ]
11251
12134
  };
11252
12135
  }
11253
12136
  );
@@ -11256,7 +12139,9 @@ function registerDiscoveryTools(server) {
11256
12139
  '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.',
11257
12140
  {
11258
12141
  query: z23.string().optional().describe("Search query to filter tools by name or description"),
11259
- module: z23.string().optional().describe('Filter by module name (e.g. "planning", "content", "analytics")'),
12142
+ module: z23.string().optional().describe(
12143
+ 'Filter by module name (e.g. "planning", "content", "analytics")'
12144
+ ),
11260
12145
  scope: z23.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
11261
12146
  detail: z23.enum(["name", "summary", "full"]).default("summary").describe(
11262
12147
  'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
@@ -11273,14 +12158,18 @@ function registerDiscoveryTools(server) {
11273
12158
  if (query) {
11274
12159
  results = searchTools(query);
11275
12160
  }
11276
- results = results.filter((t) => !t.internal && !t.hiddenFromPublicCount);
12161
+ results = results.filter(isPubliclyDiscoverable);
11277
12162
  if (module) {
11278
12163
  const moduleTools = getToolsByModule(module);
11279
- results = results.filter((t) => moduleTools.some((mt) => mt.name === t.name));
12164
+ results = results.filter(
12165
+ (t) => moduleTools.some((mt) => mt.name === t.name)
12166
+ );
11280
12167
  }
11281
12168
  if (scope) {
11282
12169
  const scopeTools = getToolsByScope(scope);
11283
- results = results.filter((t) => scopeTools.some((st) => st.name === t.name));
12170
+ results = results.filter(
12171
+ (t) => scopeTools.some((st) => st.name === t.name)
12172
+ );
11284
12173
  }
11285
12174
  if (available_only) {
11286
12175
  results = results.filter(isAvailable);
@@ -11318,7 +12207,12 @@ function registerDiscoveryTools(server) {
11318
12207
  text: JSON.stringify(
11319
12208
  {
11320
12209
  toolCount: results.length,
11321
- ...hasKnownScopes ? { scopes: { available: currentScopes, unavailable_matches: unavailableCount } } : {},
12210
+ ...hasKnownScopes ? {
12211
+ scopes: {
12212
+ available: currentScopes,
12213
+ unavailable_matches: unavailableCount
12214
+ }
12215
+ } : {},
11322
12216
  tools: output
11323
12217
  },
11324
12218
  null,
@@ -11332,11 +12226,15 @@ function registerDiscoveryTools(server) {
11332
12226
  }
11333
12227
 
11334
12228
  // src/tools/pipeline.ts
12229
+ init_edge_function();
11335
12230
  import { z as z24 } from "zod";
11336
12231
  import { randomUUID as randomUUID3 } from "node:crypto";
11337
12232
  init_supabase();
11338
12233
  function asEnvelope20(data) {
11339
- return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
12234
+ return {
12235
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
12236
+ data
12237
+ };
11340
12238
  }
11341
12239
  var PLATFORM_ENUM2 = z24.enum([
11342
12240
  "youtube",
@@ -11372,7 +12270,10 @@ function registerPipelineTools(server) {
11372
12270
  action: "pipeline-readiness",
11373
12271
  platforms,
11374
12272
  estimated_posts,
11375
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
12273
+ ...resolvedProjectId ? {
12274
+ projectId: resolvedProjectId,
12275
+ project_id: resolvedProjectId
12276
+ } : {}
11376
12277
  },
11377
12278
  { timeoutMs: 15e3 }
11378
12279
  );
@@ -11390,16 +12291,24 @@ function registerPipelineTools(server) {
11390
12291
  const blockers = [];
11391
12292
  const warnings = [];
11392
12293
  if (!isUnlimited && credits < estimatedCost) {
11393
- blockers.push(`Insufficient credits: ${credits} available, ~${estimatedCost} needed`);
12294
+ blockers.push(
12295
+ `Insufficient credits: ${credits} available, ~${estimatedCost} needed`
12296
+ );
11394
12297
  }
11395
12298
  if (missingPlatforms.length > 0) {
11396
- blockers.push(`Missing connected accounts: ${missingPlatforms.join(", ")}`);
12299
+ blockers.push(
12300
+ `Missing connected accounts: ${missingPlatforms.join(", ")}`
12301
+ );
11397
12302
  }
11398
12303
  if (!hasBrand) {
11399
- warnings.push("No brand profile found. Content will use generic voice.");
12304
+ warnings.push(
12305
+ "No brand profile found. Content will use generic voice."
12306
+ );
11400
12307
  }
11401
12308
  if (pendingApprovals > 0) {
11402
- warnings.push(`${pendingApprovals} pending approval(s) from previous runs.`);
12309
+ warnings.push(
12310
+ `${pendingApprovals} pending approval(s) from previous runs.`
12311
+ );
11403
12312
  }
11404
12313
  if (!insightsFresh) {
11405
12314
  warnings.push(
@@ -11414,7 +12323,10 @@ function registerPipelineTools(server) {
11414
12323
  estimated_cost: estimatedCost,
11415
12324
  sufficient: credits >= estimatedCost
11416
12325
  },
11417
- connected_accounts: { platforms: connectedPlatforms, missing: missingPlatforms },
12326
+ connected_accounts: {
12327
+ platforms: connectedPlatforms,
12328
+ missing: missingPlatforms
12329
+ },
11418
12330
  brand_profile: { exists: hasBrand },
11419
12331
  pending_approvals: { count: pendingApprovals },
11420
12332
  insights_available: {
@@ -11428,11 +12340,18 @@ function registerPipelineTools(server) {
11428
12340
  };
11429
12341
  if (format === "json") {
11430
12342
  return {
11431
- content: [{ type: "text", text: JSON.stringify(asEnvelope20(result), null, 2) }]
12343
+ content: [
12344
+ {
12345
+ type: "text",
12346
+ text: JSON.stringify(asEnvelope20(result), null, 2)
12347
+ }
12348
+ ]
11432
12349
  };
11433
12350
  }
11434
12351
  const lines = [];
11435
- lines.push(`Pipeline Readiness: ${result.ready ? "READY" : "NOT READY"}`);
12352
+ lines.push(
12353
+ `Pipeline Readiness: ${result.ready ? "READY" : "NOT READY"}`
12354
+ );
11436
12355
  lines.push("=".repeat(40));
11437
12356
  lines.push(
11438
12357
  `Credits: ${credits} available, ~${estimatedCost} needed \u2014 ${credits >= estimatedCost ? "OK" : "BLOCKED"}`
@@ -11440,7 +12359,9 @@ function registerPipelineTools(server) {
11440
12359
  lines.push(
11441
12360
  `Accounts: ${connectedPlatforms.length} connected${missingPlatforms.length > 0 ? ` (missing: ${missingPlatforms.join(", ")})` : " \u2014 OK"}`
11442
12361
  );
11443
- lines.push(`Brand: ${hasBrand ? "OK" : "Missing (will use generic voice)"}`);
12362
+ lines.push(
12363
+ `Brand: ${hasBrand ? "OK" : "Missing (will use generic voice)"}`
12364
+ );
11444
12365
  lines.push(`Pending Approvals: ${pendingApprovals}`);
11445
12366
  lines.push(
11446
12367
  `Insights: ${insightsFresh ? "Fresh" : insightAge === null ? "None available" : `${insightAge} days old`}`
@@ -11459,7 +12380,12 @@ function registerPipelineTools(server) {
11459
12380
  } catch (err) {
11460
12381
  const message = sanitizeError(err);
11461
12382
  return {
11462
- content: [{ type: "text", text: `Readiness check failed: ${message}` }],
12383
+ content: [
12384
+ {
12385
+ type: "text",
12386
+ text: `Readiness check failed: ${message}`
12387
+ }
12388
+ ],
11463
12389
  isError: true
11464
12390
  };
11465
12391
  }
@@ -11473,6 +12399,9 @@ function registerPipelineTools(server) {
11473
12399
  topic: z24.string().optional().describe("Content topic (required if no source_url)"),
11474
12400
  source_url: z24.string().optional().describe("URL to extract content from"),
11475
12401
  platforms: z24.array(PLATFORM_ENUM2).min(1).describe("Target platforms"),
12402
+ account_ids: z24.record(z24.string(), z24.string().uuid()).optional().describe(
12403
+ "Exact connected-account ID per target platform. Required when a project has multiple accounts on one platform."
12404
+ ),
11476
12405
  days: z24.number().min(1).max(7).default(5).describe("Days to plan"),
11477
12406
  posts_per_day: z24.number().min(1).max(3).default(1).describe("Posts per platform per day"),
11478
12407
  approval_mode: z24.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe(
@@ -11494,6 +12423,7 @@ function registerPipelineTools(server) {
11494
12423
  topic,
11495
12424
  source_url,
11496
12425
  platforms,
12426
+ account_ids,
11497
12427
  days,
11498
12428
  posts_per_day,
11499
12429
  approval_mode,
@@ -11511,7 +12441,12 @@ function registerPipelineTools(server) {
11511
12441
  let creditsUsed = 0;
11512
12442
  if (!topic && !source_url) {
11513
12443
  return {
11514
- content: [{ type: "text", text: "Either topic or source_url is required." }],
12444
+ content: [
12445
+ {
12446
+ type: "text",
12447
+ text: "Either topic or source_url is required."
12448
+ }
12449
+ ],
11515
12450
  isError: true
11516
12451
  };
11517
12452
  }
@@ -11541,6 +12476,35 @@ function registerPipelineTools(server) {
11541
12476
  }
11542
12477
  try {
11543
12478
  const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
12479
+ if (schedulingRequested && !resolvedProjectId) {
12480
+ return {
12481
+ content: [
12482
+ {
12483
+ type: "text",
12484
+ text: "A project_id is required to schedule pipeline output. Configure an explicit project or use an API key scoped to exactly one project."
12485
+ }
12486
+ ],
12487
+ isError: true
12488
+ };
12489
+ }
12490
+ if (schedulingRequested) {
12491
+ const preflightRouting = await resolveConnectedAccountRouting({
12492
+ projectId: resolvedProjectId,
12493
+ platforms,
12494
+ requestedAccountIds: account_ids
12495
+ });
12496
+ if (preflightRouting.error || !preflightRouting.connectedAccountIds) {
12497
+ return {
12498
+ content: [
12499
+ {
12500
+ type: "text",
12501
+ text: `Cannot run publishing pipeline \u2014 ${preflightRouting.error ?? "exact connected-account routing could not be established."} (checked before any credits were spent.)`
12502
+ }
12503
+ ],
12504
+ isError: true
12505
+ };
12506
+ }
12507
+ }
11544
12508
  const estimatedCost = BASE_PLAN_CREDITS + (source_url ? SOURCE_EXTRACTION_CREDITS : 0);
11545
12509
  const { data: budgetData } = await callEdgeFunction(
11546
12510
  "mcp-data",
@@ -11594,7 +12558,13 @@ function registerPipelineTools(server) {
11594
12558
  "social-neuron-ai",
11595
12559
  {
11596
12560
  type: "generation",
11597
- prompt: buildPlanPrompt(resolvedTopic, platforms, days, posts_per_day, source_url),
12561
+ prompt: buildPlanPrompt(
12562
+ resolvedTopic,
12563
+ platforms,
12564
+ days,
12565
+ posts_per_day,
12566
+ source_url
12567
+ ),
11598
12568
  model: "gemini-2.5-flash",
11599
12569
  responseFormat: "json",
11600
12570
  ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
@@ -11602,7 +12572,10 @@ function registerPipelineTools(server) {
11602
12572
  { timeoutMs: 6e4 }
11603
12573
  );
11604
12574
  if (planError || !planData) {
11605
- errors.push({ stage: "planning", message: planError ?? "No AI response" });
12575
+ errors.push({
12576
+ stage: "planning",
12577
+ message: planError ?? "No AI response"
12578
+ });
11606
12579
  await callEdgeFunction(
11607
12580
  "mcp-data",
11608
12581
  {
@@ -11619,7 +12592,10 @@ function registerPipelineTools(server) {
11619
12592
  );
11620
12593
  return {
11621
12594
  content: [
11622
- { type: "text", text: `Planning failed: ${planError ?? "No AI response"}` }
12595
+ {
12596
+ type: "text",
12597
+ text: `Planning failed: ${planError ?? "No AI response"}`
12598
+ }
11623
12599
  ],
11624
12600
  isError: true
11625
12601
  };
@@ -11649,20 +12625,22 @@ function registerPipelineTools(server) {
11649
12625
  const postsArray = extractJsonArray(rawText);
11650
12626
  const requestedPlatformSet = new Set(platforms);
11651
12627
  const maxPosts = platforms.length * days * posts_per_day;
11652
- const parsedPosts = (postsArray ?? []).map((p) => ({
11653
- id: String(p.id ?? randomUUID3().slice(0, 8)),
11654
- day: Number(p.day ?? 1),
11655
- date: String(p.date ?? ""),
11656
- platform: String(p.platform ?? ""),
11657
- content_type: p.content_type ?? "caption",
11658
- caption: String(p.caption ?? ""),
11659
- title: p.title ? String(p.title) : void 0,
11660
- hashtags: Array.isArray(p.hashtags) ? p.hashtags.map(String) : void 0,
11661
- hook: String(p.hook ?? ""),
11662
- angle: String(p.angle ?? ""),
11663
- visual_direction: p.visual_direction ? String(p.visual_direction) : void 0,
11664
- media_type: p.media_type ? String(p.media_type) : void 0
11665
- }));
12628
+ const parsedPosts = (postsArray ?? []).map(
12629
+ (p) => ({
12630
+ id: String(p.id ?? randomUUID3().slice(0, 8)),
12631
+ day: Number(p.day ?? 1),
12632
+ date: String(p.date ?? ""),
12633
+ platform: String(p.platform ?? ""),
12634
+ content_type: p.content_type ?? "caption",
12635
+ caption: String(p.caption ?? ""),
12636
+ title: p.title ? String(p.title) : void 0,
12637
+ hashtags: Array.isArray(p.hashtags) ? p.hashtags.map(String) : void 0,
12638
+ hook: String(p.hook ?? ""),
12639
+ angle: String(p.angle ?? ""),
12640
+ visual_direction: p.visual_direction ? String(p.visual_direction) : void 0,
12641
+ media_type: p.media_type ? String(p.media_type) : void 0
12642
+ })
12643
+ );
11666
12644
  const platformFilteredPosts = parsedPosts.filter(
11667
12645
  (post) => requestedPlatformSet.has(post.platform)
11668
12646
  );
@@ -11745,7 +12723,10 @@ function registerPipelineTools(server) {
11745
12723
  estimated_credits: estimatedCost,
11746
12724
  generated_at: (/* @__PURE__ */ new Date()).toISOString()
11747
12725
  },
11748
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
12726
+ ...resolvedProjectId ? {
12727
+ projectId: resolvedProjectId,
12728
+ project_id: resolvedProjectId
12729
+ } : {}
11749
12730
  },
11750
12731
  { timeoutMs: 1e4 }
11751
12732
  );
@@ -11798,6 +12779,22 @@ function registerPipelineTools(server) {
11798
12779
  }
11799
12780
  }
11800
12781
  let postsScheduled = 0;
12782
+ const pipelineRouting = schedulingRequested && postsApproved > 0 ? await resolveConnectedAccountRouting({
12783
+ projectId: resolvedProjectId,
12784
+ platforms,
12785
+ requestedAccountIds: account_ids
12786
+ }) : void 0;
12787
+ if (pipelineRouting?.error || schedulingRequested && postsApproved > 0 && !pipelineRouting?.connectedAccountIds) {
12788
+ return {
12789
+ content: [
12790
+ {
12791
+ type: "text",
12792
+ text: `Cannot run publishing pipeline \u2014 ${pipelineRouting?.error ?? "exact connected-account routing could not be established."}`
12793
+ }
12794
+ ],
12795
+ isError: true
12796
+ };
12797
+ }
11801
12798
  if (!dry_run && !skipSet.has("schedule") && postsApproved > 0) {
11802
12799
  const approvedPosts = posts.filter((p) => p.status === "approved");
11803
12800
  const scheduleBase = /* @__PURE__ */ new Date();
@@ -11805,10 +12802,27 @@ function registerPipelineTools(server) {
11805
12802
  const scheduleBaseMs = scheduleBase.getTime();
11806
12803
  for (const post of approvedPosts) {
11807
12804
  if (creditsUsed >= creditLimit) {
11808
- errors.push({ stage: "schedule", message: "Credit limit reached" });
12805
+ errors.push({
12806
+ stage: "schedule",
12807
+ message: "Credit limit reached"
12808
+ });
11809
12809
  break;
11810
12810
  }
11811
- const scheduledAt = post.schedule_at ?? new Date(scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5).toISOString();
12811
+ const scheduledAt = post.schedule_at ?? new Date(
12812
+ scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5
12813
+ ).toISOString();
12814
+ const route = Object.entries(
12815
+ pipelineRouting.connectedAccountIds
12816
+ ).find(
12817
+ ([platform2]) => platform2.toLowerCase() === post.platform.toLowerCase()
12818
+ );
12819
+ if (!route) {
12820
+ errors.push({
12821
+ stage: "schedule",
12822
+ message: `No verified connected account route for ${post.platform}`
12823
+ });
12824
+ continue;
12825
+ }
11812
12826
  try {
11813
12827
  const { error: schedError } = await callEdgeFunction(
11814
12828
  "schedule-post",
@@ -11821,7 +12835,11 @@ function registerPipelineTools(server) {
11821
12835
  scheduledAt,
11822
12836
  planId,
11823
12837
  idempotencyKey: `pipeline-${planId}-${post.id}`,
11824
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
12838
+ ...resolvedProjectId ? {
12839
+ projectId: resolvedProjectId,
12840
+ project_id: resolvedProjectId
12841
+ } : {},
12842
+ connectedAccountIds: { [route[0]]: route[1] }
11825
12843
  },
11826
12844
  { timeoutMs: 15e3 }
11827
12845
  );
@@ -11887,12 +12905,17 @@ function registerPipelineTools(server) {
11887
12905
  if (response_format === "json") {
11888
12906
  return {
11889
12907
  content: [
11890
- { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
12908
+ {
12909
+ type: "text",
12910
+ text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
12911
+ }
11891
12912
  ]
11892
12913
  };
11893
12914
  }
11894
12915
  const lines = [];
11895
- lines.push(`Pipeline ${pipelineId.slice(0, 8)}... ${finalStatus.toUpperCase()}`);
12916
+ lines.push(
12917
+ `Pipeline ${pipelineId.slice(0, 8)}... ${finalStatus.toUpperCase()}`
12918
+ );
11896
12919
  lines.push("=".repeat(40));
11897
12920
  lines.push(`Posts generated: ${posts.length}`);
11898
12921
  lines.push(`Posts approved: ${postsApproved}`);
@@ -11931,7 +12954,9 @@ function registerPipelineTools(server) {
11931
12954
  } catch {
11932
12955
  }
11933
12956
  return {
11934
- content: [{ type: "text", text: `Pipeline failed: ${message}` }],
12957
+ content: [
12958
+ { type: "text", text: `Pipeline failed: ${message}` }
12959
+ ],
11935
12960
  isError: true
11936
12961
  };
11937
12962
  }
@@ -11968,7 +12993,12 @@ function registerPipelineTools(server) {
11968
12993
  }
11969
12994
  if (format === "json") {
11970
12995
  return {
11971
- content: [{ type: "text", text: JSON.stringify(asEnvelope20(data), null, 2) }]
12996
+ content: [
12997
+ {
12998
+ type: "text",
12999
+ text: JSON.stringify(asEnvelope20(data), null, 2)
13000
+ }
13001
+ ]
11972
13002
  };
11973
13003
  }
11974
13004
  const lines = [];
@@ -12008,10 +13038,19 @@ function registerPipelineTools(server) {
12008
13038
  },
12009
13039
  async ({ plan_id, quality_threshold, response_format }) => {
12010
13040
  try {
12011
- const { data: loadResult, error: loadError } = await callEdgeFunction("mcp-data", { action: "auto-approve-plan", plan_id }, { timeoutMs: 1e4 });
13041
+ const { data: loadResult, error: loadError } = await callEdgeFunction(
13042
+ "mcp-data",
13043
+ { action: "auto-approve-plan", plan_id },
13044
+ { timeoutMs: 1e4 }
13045
+ );
12012
13046
  if (loadError) {
12013
13047
  return {
12014
- content: [{ type: "text", text: `Failed to load plan: ${loadError}` }],
13048
+ content: [
13049
+ {
13050
+ type: "text",
13051
+ text: `Failed to load plan: ${loadError}`
13052
+ }
13053
+ ],
12015
13054
  isError: true
12016
13055
  };
12017
13056
  }
@@ -12019,7 +13058,10 @@ function registerPipelineTools(server) {
12019
13058
  if (!stored?.plan_payload) {
12020
13059
  return {
12021
13060
  content: [
12022
- { type: "text", text: `No content plan found for plan_id=${plan_id}` }
13061
+ {
13062
+ type: "text",
13063
+ text: `No content plan found for plan_id=${plan_id}`
13064
+ }
12023
13065
  ],
12024
13066
  isError: true
12025
13067
  };
@@ -12046,7 +13088,11 @@ function registerPipelineTools(server) {
12046
13088
  blockers: []
12047
13089
  };
12048
13090
  autoApproved++;
12049
- details.push({ post_id: post.id, action: "approved", score: quality.total });
13091
+ details.push({
13092
+ post_id: post.id,
13093
+ action: "approved",
13094
+ score: quality.total
13095
+ });
12050
13096
  } else if (quality.total >= quality_threshold - 5) {
12051
13097
  post.status = "needs_edit";
12052
13098
  post.quality = {
@@ -12056,7 +13102,11 @@ function registerPipelineTools(server) {
12056
13102
  blockers: quality.blockers
12057
13103
  };
12058
13104
  flagged++;
12059
- details.push({ post_id: post.id, action: "flagged", score: quality.total });
13105
+ details.push({
13106
+ post_id: post.id,
13107
+ action: "flagged",
13108
+ score: quality.total
13109
+ });
12060
13110
  } else {
12061
13111
  post.status = "rejected";
12062
13112
  post.quality = {
@@ -12066,7 +13116,11 @@ function registerPipelineTools(server) {
12066
13116
  blockers: quality.blockers
12067
13117
  };
12068
13118
  rejected++;
12069
- details.push({ post_id: post.id, action: "rejected", score: quality.total });
13119
+ details.push({
13120
+ post_id: post.id,
13121
+ action: "rejected",
13122
+ score: quality.total
13123
+ });
12070
13124
  }
12071
13125
  }
12072
13126
  const newStatus = flagged === 0 && rejected === 0 ? "approved" : "in_review";
@@ -12102,7 +13156,10 @@ function registerPipelineTools(server) {
12102
13156
  if (response_format === "json") {
12103
13157
  return {
12104
13158
  content: [
12105
- { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
13159
+ {
13160
+ type: "text",
13161
+ text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
13162
+ }
12106
13163
  ]
12107
13164
  };
12108
13165
  }
@@ -12123,7 +13180,9 @@ function registerPipelineTools(server) {
12123
13180
  } catch (err) {
12124
13181
  const message = sanitizeError(err);
12125
13182
  return {
12126
- content: [{ type: "text", text: `Auto-approve failed: ${message}` }],
13183
+ content: [
13184
+ { type: "text", text: `Auto-approve failed: ${message}` }
13185
+ ],
12127
13186
  isError: true
12128
13187
  };
12129
13188
  }
@@ -12149,6 +13208,7 @@ function buildPlanPrompt(topic, platforms, days, postsPerDay, sourceUrl) {
12149
13208
  }
12150
13209
 
12151
13210
  // src/tools/suggest.ts
13211
+ init_edge_function();
12152
13212
  import { z as z25 } from "zod";
12153
13213
  function asEnvelope21(data) {
12154
13214
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
@@ -12290,6 +13350,7 @@ ${i + 1}. ${s.topic}`);
12290
13350
  }
12291
13351
 
12292
13352
  // src/tools/digest.ts
13353
+ init_edge_function();
12293
13354
  import { z as z26 } from "zod";
12294
13355
 
12295
13356
  // src/lib/anomaly-detector.ts
@@ -12695,8 +13756,9 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
12695
13756
  }
12696
13757
 
12697
13758
  // src/tools/brandRuntime.ts
12698
- import { z as z27 } from "zod";
13759
+ init_edge_function();
12699
13760
  init_supabase();
13761
+ import { z as z27 } from "zod";
12700
13762
 
12701
13763
  // src/lib/brandScoring.ts
12702
13764
  var WEIGHTS = {
@@ -13553,6 +14615,7 @@ function registerBrandRuntimeTools(server) {
13553
14615
  }
13554
14616
 
13555
14617
  // src/tools/carousel.ts
14618
+ init_edge_function();
13556
14619
  import { z as z28 } from "zod";
13557
14620
  init_supabase();
13558
14621
  var IMAGE_CREDIT_ESTIMATES2 = {
@@ -13630,25 +14693,39 @@ function registerCarouselTools(server) {
13630
14693
  ]).optional().describe("Carousel template. Default: hormozi-authority."),
13631
14694
  slide_count: z28.number().min(3).max(10).optional().describe("Number of slides (3-10). Default: 7."),
13632
14695
  aspect_ratio: z28.enum(["1:1", "4:5", "9:16"]).optional().describe("Aspect ratio for both carousel and images. Default: 1:1."),
13633
- style: z28.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe("Visual style. Default: hormozi for hormozi-authority template."),
14696
+ style: z28.enum(["minimal", "bold", "professional", "playful", "hormozi"]).optional().describe(
14697
+ "Visual style. Default: hormozi for hormozi-authority template."
14698
+ ),
13634
14699
  image_style_suffix: z28.string().max(500).optional().describe(
13635
14700
  'Style suffix appended to every image prompt for visual consistency across slides. Example: "dark moody lighting, cinematic, 35mm film grain".'
13636
14701
  ),
13637
14702
  hook: z28.string().max(300).optional().describe(
13638
14703
  "Explicit hook/opener for slide 1. Overrides any hook derived from topic. Keep under 15 words for strongest scroll-stop."
13639
14704
  ),
13640
- hook_family: z28.enum(["curiosity", "authority", "pain_point", "contrarian", "data_driven"]).optional().describe(
14705
+ hook_family: z28.enum([
14706
+ "curiosity",
14707
+ "authority",
14708
+ "pain_point",
14709
+ "contrarian",
14710
+ "data_driven"
14711
+ ]).optional().describe(
13641
14712
  "Hook family tag. Recorded on the carousel so downstream analytics can attribute engagement to hook pattern."
13642
14713
  ),
13643
- cta_text: z28.string().max(200).optional().describe("Explicit CTA copy for the final slide. If omitted, derived from topic."),
13644
- cta_url: z28.string().url().optional().describe("URL promoted on the CTA slide. Defaults to the project landing page."),
14714
+ cta_text: z28.string().max(200).optional().describe(
14715
+ "Explicit CTA copy for the final slide. If omitted, derived from topic."
14716
+ ),
14717
+ cta_url: z28.string().url().optional().describe(
14718
+ "URL promoted on the CTA slide. Defaults to the project landing page."
14719
+ ),
13645
14720
  tone: z28.string().max(200).optional().describe(
13646
14721
  'Voice/tone override. Composes with the brand profile voice when present. Example: "educational, confident, not arrogant".'
13647
14722
  ),
13648
14723
  constraints: z28.string().max(500).optional().describe(
13649
14724
  'Content constraints applied at generation time. Example: "No fabricated statistics. Sentence case only. No ALL CAPS."'
13650
14725
  ),
13651
- platform: z28.enum(["linkedin", "instagram", "tiktok", "x"]).optional().describe("Target platform. Affects tone conventions and slide-count guardrails."),
14726
+ platform: z28.enum(["linkedin", "instagram", "tiktok", "x"]).optional().describe(
14727
+ "Target platform. Affects tone conventions and slide-count guardrails."
14728
+ ),
13652
14729
  brand_id: z28.string().optional().describe(
13653
14730
  "Brand/project ID to pull visual context from (colors, logo, mood). Falls back to project_id, then default project."
13654
14731
  ),
@@ -13680,7 +14757,9 @@ function registerCarouselTools(server) {
13680
14757
  const slideCount = slide_count ?? 7;
13681
14758
  const ratio = aspect_ratio ?? "1:1";
13682
14759
  let brandContext = null;
13683
- const brandProjectId = brand_id || project_id || await getDefaultProjectId();
14760
+ const defaultProjectId = await getDefaultProjectId();
14761
+ const brandProjectId = brand_id || project_id || defaultProjectId;
14762
+ const resolvedProjectId = project_id || defaultProjectId;
13684
14763
  if (brandProjectId) {
13685
14764
  brandContext = await fetchBrandVisualContext(brandProjectId);
13686
14765
  }
@@ -13689,10 +14768,7 @@ function registerCarouselTools(server) {
13689
14768
  const totalEstimatedCost = carouselTextCost + slideCount * perImageCost;
13690
14769
  const budgetCheck = checkCreditBudget(totalEstimatedCost);
13691
14770
  if (!budgetCheck.ok) {
13692
- return {
13693
- content: [{ type: "text", text: budgetCheck.message }],
13694
- isError: true
13695
- };
14771
+ return budgetCheck.error;
13696
14772
  }
13697
14773
  const assetBudget = checkAssetBudget(slideCount);
13698
14774
  if (!assetBudget.ok) {
@@ -13702,7 +14778,10 @@ function registerCarouselTools(server) {
13702
14778
  };
13703
14779
  }
13704
14780
  const userId = await getDefaultUserId();
13705
- const rateLimit = checkRateLimit("generation", `create_carousel:${userId}`);
14781
+ const rateLimit = checkRateLimit(
14782
+ "generation",
14783
+ `create_carousel:${userId}`
14784
+ );
13706
14785
  if (!rateLimit.allowed) {
13707
14786
  return {
13708
14787
  content: [
@@ -13722,7 +14801,7 @@ function registerCarouselTools(server) {
13722
14801
  slideCount,
13723
14802
  aspectRatio: ratio,
13724
14803
  style: resolvedStyle,
13725
- projectId: project_id,
14804
+ projectId: resolvedProjectId,
13726
14805
  hook,
13727
14806
  hookFamily: hook_family,
13728
14807
  ctaText: cta_text,
@@ -13736,7 +14815,12 @@ function registerCarouselTools(server) {
13736
14815
  if (carouselError || !carouselData?.carousel) {
13737
14816
  const errMsg = carouselError ?? "No carousel data returned";
13738
14817
  return {
13739
- content: [{ type: "text", text: `Carousel text generation failed: ${errMsg}` }],
14818
+ content: [
14819
+ {
14820
+ type: "text",
14821
+ text: `Carousel text generation failed: ${errMsg}`
14822
+ }
14823
+ ],
13740
14824
  isError: true
13741
14825
  };
13742
14826
  }
@@ -13765,7 +14849,8 @@ function registerCarouselTools(server) {
13765
14849
  {
13766
14850
  prompt: imagePrompt,
13767
14851
  model: image_model,
13768
- aspectRatio: ratio
14852
+ aspectRatio: ratio,
14853
+ ...resolvedProjectId && { projectId: resolvedProjectId }
13769
14854
  },
13770
14855
  { timeoutMs: 3e4 }
13771
14856
  );
@@ -13822,14 +14907,19 @@ function registerCarouselTools(server) {
13822
14907
  type: "text",
13823
14908
  text: JSON.stringify(
13824
14909
  {
13825
- _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
14910
+ _meta: {
14911
+ version: MCP_VERSION,
14912
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
14913
+ },
13826
14914
  data: {
13827
14915
  carouselId: carousel.id,
13828
14916
  templateId,
13829
14917
  style: resolvedStyle,
13830
14918
  slideCount: carousel.slides.length,
13831
14919
  slides: carousel.slides.map((s) => {
13832
- const job = imageJobs.find((j) => j.slideNumber === s.slideNumber);
14920
+ const job = imageJobs.find(
14921
+ (j) => j.slideNumber === s.slideNumber
14922
+ );
13833
14923
  return {
13834
14924
  ...s,
13835
14925
  imageJobId: job?.jobId ?? null,
@@ -13856,9 +14946,17 @@ function registerCarouselTools(server) {
13856
14946
  credits: {
13857
14947
  textGeneration: textCredits,
13858
14948
  imagesEstimated: successfulJobs.length * perImageCost,
13859
- imagesCharged: imageJobs.reduce((sum, job) => sum + (job.creditsCharged ?? 0), 0),
13860
- imagesRefunded: imageJobs.reduce((sum, job) => sum + (job.creditsRefunded ?? 0), 0),
13861
- billingUnknownSlides: imageJobs.filter((job) => job.billingStatus === "unknown").length,
14949
+ imagesCharged: imageJobs.reduce(
14950
+ (sum, job) => sum + (job.creditsCharged ?? 0),
14951
+ 0
14952
+ ),
14953
+ imagesRefunded: imageJobs.reduce(
14954
+ (sum, job) => sum + (job.creditsRefunded ?? 0),
14955
+ 0
14956
+ ),
14957
+ billingUnknownSlides: imageJobs.filter(
14958
+ (job) => job.billingStatus === "unknown"
14959
+ ).length,
13862
14960
  totalEstimated: textCredits + successfulJobs.length * perImageCost
13863
14961
  }
13864
14962
  }
@@ -13886,7 +14984,9 @@ function registerCarouselTools(server) {
13886
14984
  for (const slide of carousel.slides) {
13887
14985
  const job = imageJobs.find((j) => j.slideNumber === slide.slideNumber);
13888
14986
  const status = job?.jobId ? `image: ${job.jobId}` : `image FAILED: ${job?.error}`;
13889
- lines.push(` ${slide.slideNumber}. ${slide.headline || "(no headline)"} [${status}]`);
14987
+ lines.push(
14988
+ ` ${slide.slideNumber}. ${slide.headline || "(no headline)"} [${status}]`
14989
+ );
13890
14990
  }
13891
14991
  if (failedJobs.length > 0) {
13892
14992
  lines.push("");
@@ -13897,7 +14997,9 @@ function registerCarouselTools(server) {
13897
14997
  const jobIdList = successfulJobs.map((j) => j.jobId).join(", ");
13898
14998
  lines.push("");
13899
14999
  lines.push("Next steps:");
13900
- lines.push(` 1. Poll each job: check_status with job_id for each of: ${jobIdList}`);
15000
+ lines.push(
15001
+ ` 1. Poll each job: check_status with job_id for each of: ${jobIdList}`
15002
+ );
13901
15003
  lines.push(
13902
15004
  " 2. When all complete: schedule_post with job_ids=[...] and media_type=CAROUSEL_ALBUM"
13903
15005
  );
@@ -13909,6 +15011,7 @@ function registerCarouselTools(server) {
13909
15011
  }
13910
15012
 
13911
15013
  // src/tools/niche-research.ts
15014
+ init_edge_function();
13912
15015
  import { z as z29 } from "zod";
13913
15016
  function asEnvelope24(data) {
13914
15017
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
@@ -13995,6 +15098,7 @@ function registerNicheResearchTools(server) {
13995
15098
  // src/tools/hyperframes.ts
13996
15099
  import { z as z30 } from "zod";
13997
15100
  init_supabase();
15101
+ init_edge_function();
13998
15102
  var HYPERFRAMES_BLOCKS = [
13999
15103
  // Transitions
14000
15104
  {
@@ -14249,6 +15353,9 @@ function registerHyperframesTools(server) {
14249
15353
  }
14250
15354
 
14251
15355
  // src/apps/content-calendar.ts
15356
+ init_edge_function();
15357
+ init_request_context();
15358
+ init_supabase();
14252
15359
  import {
14253
15360
  registerAppTool,
14254
15361
  registerAppResource,
@@ -14258,8 +15365,6 @@ import { z as z31 } from "zod";
14258
15365
  import fs from "node:fs/promises";
14259
15366
  import path from "node:path";
14260
15367
  import { fileURLToPath } from "node:url";
14261
- init_request_context();
14262
- init_supabase();
14263
15368
  var CALENDAR_URI = "ui://content-calendar/v1/mcp-app.html";
14264
15369
  var CALENDAR_CSP = {
14265
15370
  // The HTML is fully self-contained. Tool calls travel over the host bridge,
@@ -14470,6 +15575,8 @@ function registerContentCalendarApp(server) {
14470
15575
  }
14471
15576
 
14472
15577
  // src/apps/analytics-pulse.ts
15578
+ init_edge_function();
15579
+ init_supabase();
14473
15580
  import {
14474
15581
  registerAppResource as registerAppResource2,
14475
15582
  registerAppTool as registerAppTool2,
@@ -14479,7 +15586,6 @@ import { z as z32 } from "zod";
14479
15586
  import fs2 from "node:fs/promises";
14480
15587
  import path2 from "node:path";
14481
15588
  import { fileURLToPath as fileURLToPath2 } from "node:url";
14482
- init_supabase();
14483
15589
  var ANALYTICS_URI = "ui://analytics-pulse/v1/mcp-app.html";
14484
15590
  var ANALYTICS_CSP = {
14485
15591
  connectDomains: [],
@@ -14712,6 +15818,7 @@ function registerAnalyticsPulseApp(server) {
14712
15818
  }
14713
15819
 
14714
15820
  // src/tools/connections.ts
15821
+ init_edge_function();
14715
15822
  import { z as z33 } from "zod";
14716
15823
  init_supabase();
14717
15824
  var PLATFORM_ENUM4 = [
@@ -14726,12 +15833,12 @@ var PLATFORM_ENUM4 = [
14726
15833
  "shopify",
14727
15834
  "etsy"
14728
15835
  ];
14729
- function findActiveAccount(accounts, platform2) {
15836
+ function findActiveAccounts(accounts, platform2, projectId) {
14730
15837
  const target = platform2.toLowerCase();
14731
- return accounts.find((a) => {
15838
+ return accounts.filter((a) => {
14732
15839
  const effectiveStatus = a.effective_status || a.status;
14733
- return a.platform.toLowerCase() === target && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
14734
- }) ?? null;
15840
+ return a.platform.toLowerCase() === target && a.project_id === projectId && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
15841
+ });
14735
15842
  }
14736
15843
  var MAX_CONCURRENT_WAITS_PER_USER = 3;
14737
15844
  var inFlightWaitsByUser = /* @__PURE__ */ new Map();
@@ -14740,16 +15847,21 @@ function registerConnectionTools(server) {
14740
15847
  "start_platform_connection",
14741
15848
  "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.",
14742
15849
  {
14743
- platform: z33.enum(PLATFORM_ENUM4).describe("Platform to connect. Lower-case: instagram, tiktok, youtube, etc."),
14744
- project_id: z33.string().optional().describe(
14745
- "Brand/project ID to bind the new social account to. Use this when connecting a brand-specific account."
15850
+ platform: z33.enum(PLATFORM_ENUM4).describe(
15851
+ "Platform to connect. Lower-case: instagram, tiktok, youtube, etc."
15852
+ ),
15853
+ project_id: z33.string().uuid().optional().describe(
15854
+ "Brand/project ID to bind the new social account to. Required when the account has multiple brands."
14746
15855
  ),
14747
15856
  response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
14748
15857
  },
14749
15858
  async ({ platform: platform2, project_id, response_format }) => {
14750
15859
  const format = response_format ?? "text";
14751
15860
  const userId = await getDefaultUserId();
14752
- const rl = checkRateLimit("posting", `start_platform_connection:${userId}`);
15861
+ const rl = checkRateLimit(
15862
+ "posting",
15863
+ `start_platform_connection:${userId}`
15864
+ );
14753
15865
  if (!rl.allowed) {
14754
15866
  return {
14755
15867
  content: [
@@ -14761,12 +15873,27 @@ function registerConnectionTools(server) {
14761
15873
  isError: true
14762
15874
  };
14763
15875
  }
15876
+ const projectResolution = await resolveProjectStrict(project_id);
15877
+ if (!projectResolution.projectId) {
15878
+ return {
15879
+ content: [
15880
+ {
15881
+ type: "text",
15882
+ 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."
15883
+ }
15884
+ ],
15885
+ isError: true
15886
+ };
15887
+ }
15888
+ const resolvedProjectId = projectResolution.projectId;
15889
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
14764
15890
  const { data, error } = await callEdgeFunction(
14765
15891
  "mcp-data",
14766
15892
  {
14767
15893
  action: "mint-connection-nonce",
14768
15894
  platform: platform2,
14769
- ...project_id ? { projectId: project_id, project_id } : {}
15895
+ projectId: resolvedProjectId,
15896
+ project_id: resolvedProjectId
14770
15897
  },
14771
15898
  { timeoutMs: 1e4 }
14772
15899
  );
@@ -14782,6 +15909,17 @@ function registerConnectionTools(server) {
14782
15909
  isError: true
14783
15910
  };
14784
15911
  }
15912
+ if (data.project_id !== resolvedProjectId) {
15913
+ return {
15914
+ content: [
15915
+ {
15916
+ type: "text",
15917
+ text: "Connection link project attestation failed. No OAuth link was returned."
15918
+ }
15919
+ ],
15920
+ isError: true
15921
+ };
15922
+ }
14785
15923
  if (format === "json") {
14786
15924
  return {
14787
15925
  content: [
@@ -14790,10 +15928,11 @@ function registerConnectionTools(server) {
14790
15928
  text: JSON.stringify(
14791
15929
  {
14792
15930
  platform: data.platform,
14793
- project_id: project_id ?? null,
15931
+ project_id: resolvedProjectId,
14794
15932
  deep_link: data.deep_link,
14795
15933
  expires_at: data.expires_at,
14796
- next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection."
15934
+ next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection.",
15935
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
14797
15936
  },
14798
15937
  null,
14799
15938
  2
@@ -14809,7 +15948,7 @@ function registerConnectionTools(server) {
14809
15948
  type: "text",
14810
15949
  text: [
14811
15950
  `${data.platform} connection ready.`,
14812
- ...project_id ? [`Brand/project: ${project_id}`] : [],
15951
+ `Brand/project: ${resolvedProjectId}`,
14813
15952
  "",
14814
15953
  'Ask the user to open this link in a browser and click "Connect" on the platform:',
14815
15954
  ` ${data.deep_link}`,
@@ -14817,7 +15956,8 @@ function registerConnectionTools(server) {
14817
15956
  `Link expires at: ${data.expires_at} (~2 minutes).`,
14818
15957
  "",
14819
15958
  "After they approve, call `wait_for_connection` with the same platform to confirm.",
14820
- "This is a one-time browser setup \u2014 not another OAuth flow inside Claude."
15959
+ "This is a one-time browser setup \u2014 not another OAuth flow inside Claude.",
15960
+ ...projectAutoResolvedNote ? ["", `Note: ${projectAutoResolvedNote}`] : []
14821
15961
  ].join("\n")
14822
15962
  }
14823
15963
  ],
@@ -14830,14 +15970,20 @@ function registerConnectionTools(server) {
14830
15970
  "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.",
14831
15971
  {
14832
15972
  platform: z33.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
14833
- project_id: z33.string().optional().describe(
15973
+ project_id: z33.string().uuid().optional().describe(
14834
15974
  "Brand/project ID to scope the connection poll. Use the same project_id passed to start_platform_connection."
14835
15975
  ),
14836
15976
  timeout_s: z33.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
14837
15977
  poll_interval_s: z33.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
14838
15978
  response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
14839
15979
  },
14840
- async ({ platform: platform2, project_id, timeout_s, poll_interval_s, response_format }) => {
15980
+ async ({
15981
+ platform: platform2,
15982
+ project_id,
15983
+ timeout_s,
15984
+ poll_interval_s,
15985
+ response_format
15986
+ }) => {
14841
15987
  const format = response_format ?? "text";
14842
15988
  const startedAt = Date.now();
14843
15989
  const timeoutMs = (timeout_s ?? 120) * 1e3;
@@ -14856,6 +16002,18 @@ function registerConnectionTools(server) {
14856
16002
  isError: true
14857
16003
  };
14858
16004
  }
16005
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
16006
+ if (!resolvedProjectId) {
16007
+ return {
16008
+ content: [
16009
+ {
16010
+ type: "text",
16011
+ text: "A project_id is required to wait for a connection. Use the same project_id used to start the OAuth flow."
16012
+ }
16013
+ ],
16014
+ isError: true
16015
+ };
16016
+ }
14859
16017
  const activeWaits = inFlightWaitsByUser.get(userId) ?? 0;
14860
16018
  if (activeWaits >= MAX_CONCURRENT_WAITS_PER_USER) {
14861
16019
  return {
@@ -14877,12 +16035,62 @@ function registerConnectionTools(server) {
14877
16035
  "mcp-data",
14878
16036
  {
14879
16037
  action: "connected-accounts",
14880
- ...project_id ? { projectId: project_id, project_id } : {}
16038
+ projectId: resolvedProjectId,
16039
+ project_id: resolvedProjectId
14881
16040
  },
14882
16041
  { timeoutMs: 1e4 }
14883
16042
  );
14884
16043
  if (!error && data?.success) {
14885
- const found = findActiveAccount(data.accounts ?? [], platform2);
16044
+ const foundAccounts = findActiveAccounts(
16045
+ data.accounts ?? [],
16046
+ platform2,
16047
+ resolvedProjectId
16048
+ );
16049
+ if (foundAccounts.length > 1) {
16050
+ if (format === "json") {
16051
+ return {
16052
+ content: [
16053
+ {
16054
+ type: "text",
16055
+ text: JSON.stringify(
16056
+ {
16057
+ connected: true,
16058
+ platform: platform2,
16059
+ project_id: resolvedProjectId,
16060
+ accounts: foundAccounts.map((a) => ({
16061
+ id: a.id,
16062
+ username: a.username,
16063
+ connected_at: a.created_at
16064
+ })),
16065
+ attempts,
16066
+ message: `${platform2} has ${foundAccounts.length} active accounts for this project. Pass the exact account_id to schedule_post.`
16067
+ },
16068
+ null,
16069
+ 2
16070
+ )
16071
+ }
16072
+ ],
16073
+ isError: false
16074
+ };
16075
+ }
16076
+ return {
16077
+ content: [
16078
+ {
16079
+ type: "text",
16080
+ text: [
16081
+ `${platform2} is connected \u2014 ${foundAccounts.length} active accounts found for project ${resolvedProjectId}:`,
16082
+ ...foundAccounts.map(
16083
+ (a) => ` ${a.username || "(unnamed)"} (id=${a.id})`
16084
+ ),
16085
+ "",
16086
+ "Call schedule_post with the exact account_id (or account_ids) for the one you mean."
16087
+ ].join("\n")
16088
+ }
16089
+ ],
16090
+ isError: false
16091
+ };
16092
+ }
16093
+ const found = foundAccounts[0];
14886
16094
  if (found) {
14887
16095
  if (format === "json") {
14888
16096
  return {
@@ -14893,7 +16101,7 @@ function registerConnectionTools(server) {
14893
16101
  {
14894
16102
  connected: true,
14895
16103
  platform: found.platform,
14896
- project_id: found.project_id ?? project_id ?? null,
16104
+ project_id: resolvedProjectId,
14897
16105
  account_id: found.id,
14898
16106
  username: found.username,
14899
16107
  connected_at: found.created_at,
@@ -14913,7 +16121,7 @@ function registerConnectionTools(server) {
14913
16121
  type: "text",
14914
16122
  text: [
14915
16123
  `${found.platform} is connected.`,
14916
- ...found.project_id || project_id ? [`Brand/project: ${found.project_id ?? project_id}`] : [],
16124
+ `Brand/project: ${resolvedProjectId}`,
14917
16125
  `Account: ${found.username || "(unnamed)"} (id=${found.id})`,
14918
16126
  `Detected after ${attempts} poll(s) in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s.`,
14919
16127
  "Ready to call `schedule_post`."
@@ -14926,7 +16134,9 @@ function registerConnectionTools(server) {
14926
16134
  }
14927
16135
  const remaining = deadline - Date.now();
14928
16136
  if (remaining <= 0) break;
14929
- await new Promise((resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining)));
16137
+ await new Promise(
16138
+ (resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining))
16139
+ );
14930
16140
  }
14931
16141
  const message = `${platform2} 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.`;
14932
16142
  if (format === "json") {
@@ -14938,7 +16148,7 @@ function registerConnectionTools(server) {
14938
16148
  {
14939
16149
  connected: false,
14940
16150
  platform: platform2,
14941
- project_id: project_id ?? null,
16151
+ project_id: resolvedProjectId,
14942
16152
  attempts,
14943
16153
  timed_out: true,
14944
16154
  message
@@ -14968,6 +16178,7 @@ function registerConnectionTools(server) {
14968
16178
  }
14969
16179
 
14970
16180
  // src/tools/harness.ts
16181
+ init_edge_function();
14971
16182
  import { z as z34 } from "zod";
14972
16183
  var ALLOWED_AGENTS = [
14973
16184
  "conductor",
@@ -15105,6 +16316,7 @@ function registerHarnessTools(server, _ctx) {
15105
16316
  }
15106
16317
 
15107
16318
  // src/tools/hermes.ts
16319
+ init_edge_function();
15108
16320
  import { z as z35 } from "zod";
15109
16321
  function asEnvelope25(data) {
15110
16322
  return {
@@ -15382,6 +16594,7 @@ ${"=".repeat(40)}
15382
16594
 
15383
16595
  // src/tools/skills.ts
15384
16596
  import { z as z36 } from "zod";
16597
+ init_edge_function();
15385
16598
 
15386
16599
  // src/lib/skills-manifest.ts
15387
16600
  var SKILLS_MANIFEST = [
@@ -15724,6 +16937,7 @@ ${skill.compiled_section}` : "";
15724
16937
  }
15725
16938
 
15726
16939
  // src/tools/loopPulse.ts
16940
+ init_edge_function();
15727
16941
  import { z as z37 } from "zod";
15728
16942
  function asEnvelope27(data) {
15729
16943
  return {
@@ -15779,8 +16993,9 @@ function registerLoopPulseTools(server) {
15779
16993
  }
15780
16994
 
15781
16995
  // src/tools/banditState.ts
15782
- import { z as z38 } from "zod";
16996
+ init_edge_function();
15783
16997
  init_supabase();
16998
+ import { z as z38 } from "zod";
15784
16999
  function asEnvelope28(data) {
15785
17000
  return {
15786
17001
  _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
@@ -15876,8 +17091,9 @@ function registerBanditStateTools(server) {
15876
17091
  }
15877
17092
 
15878
17093
  // src/tools/lifecycle.ts
15879
- import { z as z39 } from "zod";
17094
+ init_edge_function();
15880
17095
  init_supabase();
17096
+ import { z as z39 } from "zod";
15881
17097
  function envelope(data) {
15882
17098
  return {
15883
17099
  _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
@@ -16440,6 +17656,7 @@ Deliver a report covering:
16440
17656
  }
16441
17657
 
16442
17658
  // src/resources.ts
17659
+ init_edge_function();
16443
17660
  function registerResources(server) {
16444
17661
  server.resource(
16445
17662
  "brand-profile",
@@ -17341,6 +18558,7 @@ var MAX_METADATA_BYTES = 8192;
17341
18558
  var MAX_CACHED_CLIENTS = 1e3;
17342
18559
  var MAX_PERSISTED_CLIENTS = 5e3;
17343
18560
  var OAUTH_CLIENT_RETENTION_DAYS = 90;
18561
+ var OAUTH_CLIENT_TOUCH_INTERVAL_MS = 24 * 60 * 60 * 1e3;
17344
18562
  function byteLength(value) {
17345
18563
  return Buffer.byteLength(value, "utf8");
17346
18564
  }
@@ -17363,6 +18581,7 @@ function assertRegistrationWithinBounds(client3) {
17363
18581
  }
17364
18582
  }
17365
18583
  function cacheClient(cache, clientId, client3) {
18584
+ const evictedClientIds = [];
17366
18585
  if (cache.has(clientId)) {
17367
18586
  cache.delete(clientId);
17368
18587
  }
@@ -17371,7 +18590,9 @@ function cacheClient(cache, clientId, client3) {
17371
18590
  const oldestClientId = cache.keys().next().value;
17372
18591
  if (!oldestClientId) break;
17373
18592
  cache.delete(oldestClientId);
18593
+ evictedClientIds.push(oldestClientId);
17374
18594
  }
18595
+ return evictedClientIds;
17375
18596
  }
17376
18597
  function rowToClient(row) {
17377
18598
  return {
@@ -17406,6 +18627,7 @@ function clientToRow(c) {
17406
18627
  }
17407
18628
  function createClientsStore() {
17408
18629
  const cache = /* @__PURE__ */ new Map();
18630
+ const lastClientTouch = /* @__PURE__ */ new Map();
17409
18631
  let supabaseAvailable = true;
17410
18632
  function markUnavailable(reason) {
17411
18633
  if (supabaseAvailable) {
@@ -17415,14 +18637,50 @@ function createClientsStore() {
17415
18637
  supabaseAvailable = false;
17416
18638
  }
17417
18639
  }
18640
+ function clearEvictedClientTouches(evictedClientIds) {
18641
+ for (const evictedClientId of evictedClientIds) {
18642
+ lastClientTouch.delete(evictedClientId);
18643
+ }
18644
+ }
18645
+ function touchClientActivity(clientId) {
18646
+ if (!supabaseAvailable) return;
18647
+ const now = Date.now();
18648
+ const lastTouchedAt = lastClientTouch.get(clientId);
18649
+ if (lastTouchedAt !== void 0 && now - lastTouchedAt < OAUTH_CLIENT_TOUCH_INTERVAL_MS) {
18650
+ return;
18651
+ }
18652
+ lastClientTouch.set(clientId, now);
18653
+ const releaseTouchReservation = () => {
18654
+ if (lastClientTouch.get(clientId) === now) {
18655
+ lastClientTouch.delete(clientId);
18656
+ }
18657
+ };
18658
+ void (async () => {
18659
+ try {
18660
+ const supabase = getSupabaseClient();
18661
+ const { error: touchError } = await supabase.from("mcp_oauth_clients").update({ last_used_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("client_id", clientId);
18662
+ if (touchError) {
18663
+ releaseTouchReservation();
18664
+ console.error(
18665
+ `[oauth] failed to update client activity: ${sanitizeError(touchError)}`
18666
+ );
18667
+ }
18668
+ } catch (touchError) {
18669
+ releaseTouchReservation();
18670
+ console.error(`[oauth] failed to update client activity: ${sanitizeError(touchError)}`);
18671
+ }
18672
+ })();
18673
+ }
17418
18674
  return {
17419
18675
  async getClient(clientId) {
17420
18676
  const cached3 = cache.get(clientId);
17421
18677
  if (cached3) {
17422
18678
  if (!hasAllowedRedirectUris(cached3)) {
17423
18679
  cache.delete(clientId);
18680
+ lastClientTouch.delete(clientId);
17424
18681
  return void 0;
17425
18682
  }
18683
+ touchClientActivity(clientId);
17426
18684
  return cached3;
17427
18685
  }
17428
18686
  if (!supabaseAvailable) return void 0;
@@ -17444,8 +18702,8 @@ function createClientsStore() {
17444
18702
  }
17445
18703
  return void 0;
17446
18704
  }
17447
- cacheClient(cache, clientId, client3);
17448
- void supabase.from("mcp_oauth_clients").update({ last_used_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("client_id", clientId);
18705
+ clearEvictedClientTouches(cacheClient(cache, clientId, client3));
18706
+ touchClientActivity(clientId);
17449
18707
  return client3;
17450
18708
  } catch (err) {
17451
18709
  markUnavailable(err instanceof Error ? err.message : "unknown error");
@@ -17491,7 +18749,8 @@ function createClientsStore() {
17491
18749
  markUnavailable(err instanceof Error ? err.message : "unknown error");
17492
18750
  }
17493
18751
  }
17494
- cacheClient(cache, client3.client_id, client3);
18752
+ lastClientTouch.delete(client3.client_id);
18753
+ clearEvictedClientTouches(cacheClient(cache, client3.client_id, client3));
17495
18754
  return client3;
17496
18755
  }
17497
18756
  };