@socialneuron/mcp-server 1.8.2 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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;
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
+ );
236
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
 
@@ -1959,255 +2336,63 @@ function scrubPii(text, _role) {
1959
2336
  const matched = re.test(out);
1960
2337
  re.lastIndex = 0;
1961
2338
  if (matched) {
1962
- out = out.replace(re, `[REDACTED:${name}]`);
1963
- hits.push(name);
1964
- }
1965
- }
1966
- return { text: out, redacted: hits.length > 0, patterns: hits };
1967
- }
1968
-
1969
- // src/lib/agent-harness/scanner.ts
1970
- function scan(text, options) {
1971
- const flagged = /* @__PURE__ */ new Set();
1972
- let risk = 0;
1973
- const maxLength = options.source === "mcp_tool_output" ? CONSTANTS.MAX_OUTPUT_LENGTH : CONSTANTS.MAX_LENGTH;
1974
- if (text.length > maxLength) {
1975
- return {
1976
- passed: false,
1977
- risk_score: 1,
1978
- flagged_patterns: ["excessive_length"],
1979
- pii_redacted: false
1980
- };
1981
- }
1982
- const zw = detectZeroWidth(text);
1983
- if (zw.found) {
1984
- flagged.add(zw.pattern);
1985
- risk = Math.max(risk, 0.95);
1986
- }
1987
- const ipRaw = detectInstructionPhrase(text);
1988
- if (ipRaw.found) {
1989
- flagged.add(ipRaw.pattern);
1990
- risk = Math.max(risk, 0.9);
1991
- }
1992
- const normalized = normalize(text);
1993
- const ipNorm = detectInstructionPhrase(normalized);
1994
- if (ipNorm.found) {
1995
- flagged.add(ipNorm.pattern);
1996
- risk = Math.max(risk, 0.9);
1997
- }
1998
- const flaggedArr = Array.from(flagged);
1999
- if ((options.mode === "block" || options.mode === "sanitize") && flaggedArr.length > 0) {
2000
- return { passed: false, risk_score: risk, flagged_patterns: flaggedArr, pii_redacted: false };
2001
- }
2002
- const pii = scrubPii(normalized, options.source);
2003
- if (pii.redacted) {
2004
- return {
2005
- passed: true,
2006
- risk_score: Math.max(risk, 0.3),
2007
- flagged_patterns: [...flaggedArr, ...pii.patterns.map((p) => `pii_${p}`)],
2008
- sanitized_text: pii.text,
2009
- pii_redacted: true
2010
- };
2011
- }
2012
- return { passed: true, risk_score: risk, flagged_patterns: flaggedArr, pii_redacted: false };
2013
- }
2014
-
2015
- // src/tools/ideation.ts
2016
- import { z } from "zod";
2017
-
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
- };
2339
+ out = out.replace(re, `[REDACTED:${name}]`);
2340
+ hits.push(name);
2206
2341
  }
2207
- return { data: null, error: "Network request failed. Please retry." };
2208
2342
  }
2343
+ return { text: out, redacted: hits.length > 0, patterns: hits };
2344
+ }
2345
+
2346
+ // src/lib/agent-harness/scanner.ts
2347
+ function scan(text, options) {
2348
+ const flagged = /* @__PURE__ */ new Set();
2349
+ let risk = 0;
2350
+ const maxLength = options.source === "mcp_tool_output" ? CONSTANTS.MAX_OUTPUT_LENGTH : CONSTANTS.MAX_LENGTH;
2351
+ if (text.length > maxLength) {
2352
+ return {
2353
+ passed: false,
2354
+ risk_score: 1,
2355
+ flagged_patterns: ["excessive_length"],
2356
+ pii_redacted: false
2357
+ };
2358
+ }
2359
+ const zw = detectZeroWidth(text);
2360
+ if (zw.found) {
2361
+ flagged.add(zw.pattern);
2362
+ risk = Math.max(risk, 0.95);
2363
+ }
2364
+ const ipRaw = detectInstructionPhrase(text);
2365
+ if (ipRaw.found) {
2366
+ flagged.add(ipRaw.pattern);
2367
+ risk = Math.max(risk, 0.9);
2368
+ }
2369
+ const normalized = normalize(text);
2370
+ const ipNorm = detectInstructionPhrase(normalized);
2371
+ if (ipNorm.found) {
2372
+ flagged.add(ipNorm.pattern);
2373
+ risk = Math.max(risk, 0.9);
2374
+ }
2375
+ const flaggedArr = Array.from(flagged);
2376
+ if ((options.mode === "block" || options.mode === "sanitize") && flaggedArr.length > 0) {
2377
+ return { passed: false, risk_score: risk, flagged_patterns: flaggedArr, pii_redacted: false };
2378
+ }
2379
+ const pii = scrubPii(normalized, options.source);
2380
+ if (pii.redacted) {
2381
+ return {
2382
+ passed: true,
2383
+ risk_score: Math.max(risk, 0.3),
2384
+ flagged_patterns: [...flaggedArr, ...pii.patterns.map((p) => `pii_${p}`)],
2385
+ sanitized_text: pii.text,
2386
+ pii_redacted: true
2387
+ };
2388
+ }
2389
+ return { passed: true, risk_score: risk, flagged_patterns: flaggedArr, pii_redacted: false };
2209
2390
  }
2210
2391
 
2392
+ // src/tools/ideation.ts
2393
+ init_edge_function();
2394
+ import { z } from "zod";
2395
+
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.0";
2328
2513
 
2329
2514
  // src/tools/ideation.ts
2330
2515
  function asEnvelope(data) {
@@ -2725,6 +2910,7 @@ ${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
 
@@ -2959,7 +3145,10 @@ function registerContentTools(server) {
2959
3145
  isError: true
2960
3146
  };
2961
3147
  }
2962
- const rateLimit = checkRateLimit("generation", `generate_video:${userId}`);
3148
+ const rateLimit = checkRateLimit(
3149
+ "generation",
3150
+ `generate_video:${userId}`
3151
+ );
2963
3152
  if (!rateLimit.allowed) {
2964
3153
  return {
2965
3154
  content: [
@@ -3082,7 +3271,14 @@ function registerContentTools(server) {
3082
3271
  project_id: z2.string().optional().describe("Project ID to associate the generated image with."),
3083
3272
  response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
3084
3273
  },
3085
- async ({ prompt, model, aspect_ratio, image_url, project_id, response_format }) => {
3274
+ async ({
3275
+ prompt,
3276
+ model,
3277
+ aspect_ratio,
3278
+ image_url,
3279
+ project_id,
3280
+ response_format
3281
+ }) => {
3086
3282
  const format = response_format ?? "text";
3087
3283
  const userId = await getDefaultUserId();
3088
3284
  const assetBudget = checkAssetBudget();
@@ -3100,7 +3296,10 @@ function registerContentTools(server) {
3100
3296
  isError: true
3101
3297
  };
3102
3298
  }
3103
- const rateLimit = checkRateLimit("generation", `generate_image:${userId}`);
3299
+ const rateLimit = checkRateLimit(
3300
+ "generation",
3301
+ `generate_image:${userId}`
3302
+ );
3104
3303
  if (!rateLimit.allowed) {
3105
3304
  return {
3106
3305
  content: [
@@ -3231,6 +3430,15 @@ function registerContentTools(server) {
3231
3430
  isError: true
3232
3431
  };
3233
3432
  }
3433
+ let projectsDisclosure;
3434
+ const ownScopedProjectId = await getDefaultProjectId();
3435
+ if (!ownScopedProjectId) {
3436
+ const ownUserId = await getDefaultUserId().catch(() => null);
3437
+ if (ownUserId) {
3438
+ const list = await listAccessibleProjectsWithAccountStatus(ownUserId);
3439
+ if (list.length > 0) projectsDisclosure = list;
3440
+ }
3441
+ }
3234
3442
  if (job.external_id && (job.status === "pending" || job.status === "processing")) {
3235
3443
  const { data: liveStatus } = await callEdgeFunction(
3236
3444
  "kie-task-status",
@@ -3254,7 +3462,9 @@ function registerContentTools(server) {
3254
3462
  if (livePayload.error) {
3255
3463
  lines2.push(`Error: ${livePayload.error}`);
3256
3464
  }
3257
- const fallbackDisclosure2 = buildFallbackDisclosureLine(job.result_metadata);
3465
+ const fallbackDisclosure2 = buildFallbackDisclosureLine(
3466
+ job.result_metadata
3467
+ );
3258
3468
  if (fallbackDisclosure2) lines2.push(fallbackDisclosure2);
3259
3469
  lines2.push(`Credits: ${job.credits_cost}`);
3260
3470
  if (job.billing_status) {
@@ -3263,6 +3473,14 @@ function registerContentTools(server) {
3263
3473
  );
3264
3474
  }
3265
3475
  lines2.push(`Created: ${job.created_at}`);
3476
+ if (projectsDisclosure) {
3477
+ lines2.push(
3478
+ "",
3479
+ `Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
3480
+ (p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
3481
+ ).join("; ")}`
3482
+ );
3483
+ }
3266
3484
  if (format === "json") {
3267
3485
  return {
3268
3486
  content: [
@@ -3279,7 +3497,8 @@ function registerContentTools(server) {
3279
3497
  // `job` + `liveStatus`; do not duplicate them here.
3280
3498
  asEnvelope2({
3281
3499
  ...liveStatus,
3282
- ...livePayload
3500
+ ...livePayload,
3501
+ ...projectsDisclosure ? { projects: projectsDisclosure } : {}
3283
3502
  }),
3284
3503
  null,
3285
3504
  2
@@ -3322,7 +3541,9 @@ function registerContentTools(server) {
3322
3541
  if (job.error_message) {
3323
3542
  lines.push(`Error: ${job.error_message}`);
3324
3543
  }
3325
- const fallbackDisclosure = buildFallbackDisclosureLine(job.result_metadata);
3544
+ const fallbackDisclosure = buildFallbackDisclosureLine(
3545
+ job.result_metadata
3546
+ );
3326
3547
  if (fallbackDisclosure) lines.push(fallbackDisclosure);
3327
3548
  lines.push(`Credits: ${job.credits_cost}`);
3328
3549
  if (job.billing_status) {
@@ -3334,10 +3555,19 @@ function registerContentTools(server) {
3334
3555
  if (job.completed_at) {
3335
3556
  lines.push(`Completed: ${job.completed_at}`);
3336
3557
  }
3558
+ if (projectsDisclosure) {
3559
+ lines.push(
3560
+ "",
3561
+ `Projects (project_id required elsewhere? pick one): ${projectsDisclosure.map(
3562
+ (p) => `${p.name} (${p.id}${p.hasConnectedAccounts ? ", has connected accounts" : ""})`
3563
+ ).join("; ")}`
3564
+ );
3565
+ }
3337
3566
  if (format === "json") {
3338
3567
  const enriched = {
3339
3568
  ...job,
3340
- ...buildCheckStatusPayload(job)
3569
+ ...buildCheckStatusPayload(job),
3570
+ ...projectsDisclosure ? { projects: projectsDisclosure } : {}
3341
3571
  };
3342
3572
  return {
3343
3573
  content: [
@@ -3827,6 +4057,7 @@ Return ONLY valid JSON in this exact format:
3827
4057
  }
3828
4058
 
3829
4059
  // src/tools/distribution.ts
4060
+ init_edge_function();
3830
4061
  import { z as z3 } from "zod";
3831
4062
  import { createHash } from "node:crypto";
3832
4063
 
@@ -4153,6 +4384,116 @@ function evaluateQuality(input) {
4153
4384
  };
4154
4385
  }
4155
4386
 
4387
+ // src/lib/connected-account-routing.ts
4388
+ init_edge_function();
4389
+ var PLATFORM_CASE_MAP = {
4390
+ youtube: "YouTube",
4391
+ tiktok: "TikTok",
4392
+ instagram: "Instagram",
4393
+ twitter: "Twitter",
4394
+ x: "Twitter",
4395
+ linkedin: "LinkedIn",
4396
+ facebook: "Facebook",
4397
+ threads: "Threads",
4398
+ bluesky: "Bluesky"
4399
+ };
4400
+ function canonicalPlatform(value) {
4401
+ const normalized = value.trim().toLowerCase();
4402
+ return normalized === "x" ? "twitter" : normalized;
4403
+ }
4404
+ function providerPlatform(value) {
4405
+ return PLATFORM_CASE_MAP[canonicalPlatform(value)] ?? value;
4406
+ }
4407
+ function isUsable(account) {
4408
+ const status = account.effective_status ?? account.status;
4409
+ return status === "active" || status === "expires_soon";
4410
+ }
4411
+ function normalizeRequestedIds(requested) {
4412
+ const ids = /* @__PURE__ */ new Map();
4413
+ for (const [platform2, accountId] of Object.entries(requested ?? {})) {
4414
+ const canonical = canonicalPlatform(platform2);
4415
+ const existing = ids.get(canonical);
4416
+ if (existing && existing !== accountId) {
4417
+ return {
4418
+ error: `Conflicting account IDs were supplied for ${platform2} and its platform alias.`
4419
+ };
4420
+ }
4421
+ ids.set(canonical, accountId);
4422
+ }
4423
+ return { ids };
4424
+ }
4425
+ async function resolveConnectedAccountRouting(input) {
4426
+ const normalizedRequested = normalizeRequestedIds(input.requestedAccountIds);
4427
+ if (normalizedRequested.error) return { error: normalizedRequested.error };
4428
+ const targetPlatforms = new Set(input.platforms.map(canonicalPlatform));
4429
+ for (const requestedPlatform of normalizedRequested.ids?.keys() ?? []) {
4430
+ if (!targetPlatforms.has(requestedPlatform)) {
4431
+ return {
4432
+ error: `An account ID was supplied for untargeted platform ${requestedPlatform}.`
4433
+ };
4434
+ }
4435
+ }
4436
+ const { data, error } = await callEdgeFunction(
4437
+ "mcp-data",
4438
+ {
4439
+ action: "connected-accounts",
4440
+ projectId: input.projectId,
4441
+ project_id: input.projectId
4442
+ },
4443
+ { timeoutMs: 1e4 }
4444
+ );
4445
+ if (error || !Array.isArray(data?.accounts)) {
4446
+ return {
4447
+ error: `Connected-account verification failed: ${error ?? data?.error ?? "no account inventory returned"}.`
4448
+ };
4449
+ }
4450
+ const connectedAccountIds = {};
4451
+ for (const platform2 of input.platforms) {
4452
+ const canonical = canonicalPlatform(platform2);
4453
+ const displayPlatform = providerPlatform(platform2);
4454
+ const platformAccounts = data.accounts.filter(
4455
+ (account) => canonicalPlatform(account.platform) === canonical && account.project_id === input.projectId && isUsable(account)
4456
+ );
4457
+ const requestedId = normalizedRequested.ids?.get(canonical);
4458
+ let selected;
4459
+ if (requestedId) {
4460
+ selected = data.accounts.find((account) => account.id === requestedId);
4461
+ if (!selected) {
4462
+ return {
4463
+ error: `${displayPlatform}: account ${requestedId} is not available for project_id ${input.projectId}.`
4464
+ };
4465
+ }
4466
+ if (canonicalPlatform(selected.platform) !== canonical) {
4467
+ return {
4468
+ error: `${displayPlatform}: account ${requestedId} belongs to ${selected.platform}.`
4469
+ };
4470
+ }
4471
+ if (selected.project_id !== input.projectId) {
4472
+ return {
4473
+ error: `${displayPlatform}: account ${requestedId} is not bound to project_id ${input.projectId}.`
4474
+ };
4475
+ }
4476
+ if (!isUsable(selected)) {
4477
+ return {
4478
+ error: `${displayPlatform}: account ${requestedId} is ${selected.effective_status ?? selected.status}.`
4479
+ };
4480
+ }
4481
+ } else if (platformAccounts.length === 1) {
4482
+ selected = platformAccounts[0];
4483
+ } else if (platformAccounts.length === 0) {
4484
+ return {
4485
+ error: `${displayPlatform}: no active account is bound to project_id ${input.projectId}.`
4486
+ };
4487
+ } else {
4488
+ return {
4489
+ error: `${displayPlatform}: multiple active accounts are bound to project_id ${input.projectId}; pass the exact account ID returned by list_connected_accounts.`
4490
+ };
4491
+ }
4492
+ connectedAccountIds[displayPlatform] = selected.id;
4493
+ }
4494
+ return { connectedAccountIds };
4495
+ }
4496
+
4156
4497
  // src/tools/distribution.ts
4157
4498
  function snakeToCamel(obj) {
4158
4499
  const result = {};
@@ -4170,11 +4511,16 @@ function convertPlatformMetadata(meta) {
4170
4511
  }
4171
4512
  return converted;
4172
4513
  }
4173
- var PLATFORM_CASE_MAP = {
4514
+ var PLATFORM_CASE_MAP2 = {
4174
4515
  youtube: "YouTube",
4175
4516
  tiktok: "TikTok",
4176
4517
  instagram: "Instagram",
4177
4518
  twitter: "Twitter",
4519
+ // 'x' is the platform's current branding but connected_account_routing.ts
4520
+ // (and the DB convention) still key on 'Twitter' — keep both aliases
4521
+ // resolving to the same case so schedule_content_plan's platform:'x' posts
4522
+ // don't fall through to an undefined binding (F8, 2026-07-15).
4523
+ x: "Twitter",
4178
4524
  linkedin: "LinkedIn",
4179
4525
  facebook: "Facebook",
4180
4526
  threads: "Threads",
@@ -4296,21 +4642,6 @@ async function validatePublishMediaUrl(url) {
4296
4642
  function accountEffectiveStatus(account) {
4297
4643
  return account.effective_status || account.status;
4298
4644
  }
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
4645
  async function rehostExternalUrl(mediaUrl, projectId) {
4315
4646
  const ssrf = await validateUrlForSSRF(mediaUrl);
4316
4647
  if (!ssrf.isValid) {
@@ -4446,17 +4777,17 @@ function registerDistributionTools(server) {
4446
4777
  schedule_at: z3.string().optional().describe(
4447
4778
  'ISO 8601 UTC datetime for scheduled posting (e.g. "2026-03-20T14:00:00Z"). Omit to post immediately. Must be in the future.'
4448
4779
  ),
4449
- project_id: z3.string().optional().describe(
4780
+ project_id: z3.string().uuid().optional().describe(
4450
4781
  "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
4782
  ),
4452
4783
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text."),
4453
4784
  attribution: z3.boolean().optional().describe(
4454
4785
  'If true, appends "Created with Social Neuron" to the caption. Default: false.'
4455
4786
  ),
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."
4787
+ account_id: z3.string().uuid().optional().describe(
4788
+ "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
4789
  ),
4459
- account_ids: z3.record(z3.string(), z3.string()).optional().describe(
4790
+ account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
4460
4791
  '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
4792
  ),
4462
4793
  auto_rehost: z3.boolean().optional().describe(
@@ -4500,6 +4831,45 @@ function registerDistributionTools(server) {
4500
4831
  isError: true
4501
4832
  };
4502
4833
  }
4834
+ const projectResolution = await resolveProjectForConnectedAccountTool(
4835
+ project_id,
4836
+ platforms
4837
+ );
4838
+ if (!projectResolution.projectId) {
4839
+ return {
4840
+ content: [
4841
+ {
4842
+ type: "text",
4843
+ 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."
4844
+ }
4845
+ ],
4846
+ isError: true
4847
+ };
4848
+ }
4849
+ const resolvedProjectId = projectResolution.projectId;
4850
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
4851
+ if (account_id && account_ids) {
4852
+ return {
4853
+ content: [
4854
+ {
4855
+ type: "text",
4856
+ text: "Pass either account_id or account_ids, not both."
4857
+ }
4858
+ ],
4859
+ isError: true
4860
+ };
4861
+ }
4862
+ if (account_id && platforms.length !== 1) {
4863
+ return {
4864
+ content: [
4865
+ {
4866
+ type: "text",
4867
+ text: "account_id is valid only for a single target platform. Use account_ids for multi-platform publishing."
4868
+ }
4869
+ ],
4870
+ isError: true
4871
+ };
4872
+ }
4503
4873
  const userId = await getDefaultUserId();
4504
4874
  const rateLimit = checkRateLimit("posting", `schedule_post:${userId}`);
4505
4875
  if (!rateLimit.allowed) {
@@ -4604,11 +4974,16 @@ function registerDistributionTools(server) {
4604
4974
  }
4605
4975
  const resolvedJobs = resolved;
4606
4976
  resolvedMediaUrls = resolvedJobs.map((item) => item.url);
4607
- resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map((item) => item.trustedR2);
4977
+ resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map(
4978
+ (item) => item.trustedR2
4979
+ );
4608
4980
  }
4609
4981
  const shouldRehost = auto_rehost !== false;
4610
4982
  if (shouldRehost && resolvedMediaUrl && !resolvedMediaUrlIsTrustedR2) {
4611
- const rehost = await rehostExternalUrl(resolvedMediaUrl, project_id);
4983
+ const rehost = await rehostExternalUrl(
4984
+ resolvedMediaUrl,
4985
+ resolvedProjectId
4986
+ );
4612
4987
  if ("error" in rehost) {
4613
4988
  return {
4614
4989
  content: [
@@ -4626,7 +5001,7 @@ function registerDistributionTools(server) {
4626
5001
  if (shouldRehost && resolvedMediaUrls && resolvedMediaUrls.length > 0) {
4627
5002
  const rehosted = await Promise.all(
4628
5003
  resolvedMediaUrls.map(
4629
- (u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, project_id)
5004
+ (u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, resolvedProjectId)
4630
5005
  )
4631
5006
  );
4632
5007
  const failIdx = rehosted.findIndex((r) => "error" in r);
@@ -4693,7 +5068,7 @@ function registerDistributionTools(server) {
4693
5068
  }
4694
5069
  }
4695
5070
  const normalizedPlatforms = platforms.map(
4696
- (p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
5071
+ (p) => PLATFORM_CASE_MAP2[p.toLowerCase()] || p
4697
5072
  );
4698
5073
  const blockedPlatforms = normalizedPlatforms.filter(
4699
5074
  (p) => MCP_NOT_LIVE_FOR_POSTING.has(p)
@@ -4709,92 +5084,27 @@ function registerDistributionTools(server) {
4709
5084
  isError: true
4710
5085
  };
4711
5086
  }
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;
5087
+ let requestedAccountIds;
4787
5088
  if (account_id) {
4788
- connectedAccountIds = {};
4789
- for (const p of normalizedPlatforms) {
4790
- connectedAccountIds[p] = account_id;
4791
- }
5089
+ requestedAccountIds = { [normalizedPlatforms[0]]: account_id };
4792
5090
  } 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
- }
5091
+ requestedAccountIds = account_ids;
5092
+ }
5093
+ const routing = await resolveConnectedAccountRouting({
5094
+ projectId: resolvedProjectId,
5095
+ platforms: normalizedPlatforms,
5096
+ requestedAccountIds
5097
+ });
5098
+ if (routing.error || !routing.connectedAccountIds) {
5099
+ return {
5100
+ content: [
5101
+ {
5102
+ type: "text",
5103
+ text: `Cannot post \u2014 ${routing.error ?? "exact connected-account routing could not be established."}`
5104
+ }
5105
+ ],
5106
+ isError: true
5107
+ };
4798
5108
  }
4799
5109
  let finalCaption = caption;
4800
5110
  if (attribution && finalCaption) {
@@ -4848,8 +5158,9 @@ Created with Social Neuron`;
4848
5158
  title,
4849
5159
  hashtags,
4850
5160
  scheduledAt: schedule_at,
4851
- projectId: project_id,
4852
- ...connectedAccountIds ? { connectedAccountIds } : {},
5161
+ projectId: resolvedProjectId,
5162
+ project_id: resolvedProjectId,
5163
+ connectedAccountIds: routing.connectedAccountIds,
4853
5164
  ...normalizedPlatformMetadata ? {
4854
5165
  platformMetadata: convertPlatformMetadata(
4855
5166
  normalizedPlatformMetadata
@@ -4884,10 +5195,14 @@ Created with Social Neuron`;
4884
5195
  isError: true
4885
5196
  };
4886
5197
  }
5198
+ const responseData = projectAutoResolvedNote ? { ...data, project_auto_resolved: projectAutoResolvedNote } : data;
4887
5199
  const lines = [
4888
5200
  data.success ? "Post scheduled successfully." : "Post scheduling had errors.",
4889
5201
  `Scheduled for: ${data.scheduledAt}`
4890
5202
  ];
5203
+ if (projectAutoResolvedNote) {
5204
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
5205
+ }
4891
5206
  if (tiktokAutoInboxApplied) {
4892
5207
  lines.push(
4893
5208
  "",
@@ -4905,7 +5220,7 @@ Created with Social Neuron`;
4905
5220
  }
4906
5221
  }
4907
5222
  if (format === "json") {
4908
- const structuredContent = asEnvelope3(data);
5223
+ const structuredContent = asEnvelope3(responseData);
4909
5224
  return {
4910
5225
  structuredContent,
4911
5226
  content: [
@@ -4918,7 +5233,7 @@ Created with Social Neuron`;
4918
5233
  };
4919
5234
  }
4920
5235
  return {
4921
- structuredContent: asEnvelope3(data),
5236
+ structuredContent: asEnvelope3(responseData),
4922
5237
  content: [{ type: "text", text: lines.join("\n") }],
4923
5238
  isError: !data.success
4924
5239
  };
@@ -4932,7 +5247,9 @@ Created with Social Neuron`;
4932
5247
  project_id: z3.string().uuid().optional().describe(
4933
5248
  "Brand/project ID that owns the post. Defaults to the authenticated key's project or the account default."
4934
5249
  ),
4935
- scheduled_at: z3.string().datetime({ offset: true }).describe("New future publish time as an ISO 8601 datetime with timezone."),
5250
+ scheduled_at: z3.string().datetime({ offset: true }).describe(
5251
+ "New future publish time as an ISO 8601 datetime with timezone."
5252
+ ),
4936
5253
  expected_scheduled_at: z3.string().datetime({ offset: true }).optional().describe(
4937
5254
  "Optional current schedule timestamp. If it changed since you read it, the update is rejected instead of silently overwriting it."
4938
5255
  ),
@@ -4988,7 +5305,11 @@ Created with Social Neuron`;
4988
5305
  projectId: resolvedProjectId,
4989
5306
  project_id: resolvedProjectId,
4990
5307
  scheduled_at: next.toISOString(),
4991
- ...expected_scheduled_at ? { expected_scheduled_at: new Date(expected_scheduled_at).toISOString() } : {}
5308
+ ...expected_scheduled_at ? {
5309
+ expected_scheduled_at: new Date(
5310
+ expected_scheduled_at
5311
+ ).toISOString()
5312
+ } : {}
4992
5313
  });
4993
5314
  if (error || !result?.success) {
4994
5315
  const code = result?.error ?? error ?? "reschedule_failed";
@@ -5021,17 +5342,34 @@ Created with Social Neuron`;
5021
5342
  "list_connected_accounts",
5022
5343
  "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
5344
  {
5024
- project_id: z3.string().optional().describe(
5345
+ project_id: z3.string().uuid().optional().describe(
5025
5346
  "Brand/project ID to scope connected accounts. Use the same project_id when calling schedule_post."
5026
5347
  ),
5027
- include_all: z3.boolean().optional().describe("If true, include expired or inactive accounts as well as usable accounts."),
5348
+ include_all: z3.boolean().optional().describe(
5349
+ "If true, include expired or inactive accounts as well as usable accounts."
5350
+ ),
5028
5351
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
5029
5352
  },
5030
5353
  async ({ project_id, include_all, response_format }) => {
5031
5354
  const format = response_format ?? "text";
5355
+ const projectResolution = await resolveProjectForConnectedAccountTool(project_id);
5356
+ if (!projectResolution.projectId) {
5357
+ return {
5358
+ content: [
5359
+ {
5360
+ type: "text",
5361
+ 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."
5362
+ }
5363
+ ],
5364
+ isError: true
5365
+ };
5366
+ }
5367
+ const resolvedProjectId = projectResolution.projectId;
5368
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
5032
5369
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
5033
5370
  action: "connected-accounts",
5034
- ...project_id ? { projectId: project_id, project_id } : {},
5371
+ projectId: resolvedProjectId,
5372
+ project_id: resolvedProjectId,
5035
5373
  ...include_all ? { includeAll: true } : {}
5036
5374
  });
5037
5375
  if (efError || !result?.success) {
@@ -5045,10 +5383,27 @@ Created with Social Neuron`;
5045
5383
  isError: true
5046
5384
  };
5047
5385
  }
5048
- const accounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
5386
+ const parsedAccounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
5387
+ if (parsedAccounts.some(
5388
+ (account) => account.project_id !== resolvedProjectId
5389
+ )) {
5390
+ return {
5391
+ content: [
5392
+ {
5393
+ type: "text",
5394
+ text: "Connected-account project attestation failed. No account inventory was returned."
5395
+ }
5396
+ ],
5397
+ isError: true
5398
+ };
5399
+ }
5400
+ const accounts = parsedAccounts;
5049
5401
  if (accounts.length === 0) {
5050
5402
  if (format === "json") {
5051
- const structuredContent = asEnvelope3({ accounts: [] });
5403
+ const structuredContent = asEnvelope3({
5404
+ accounts: [],
5405
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
5406
+ });
5052
5407
  return {
5053
5408
  structuredContent,
5054
5409
  content: [
@@ -5063,13 +5418,15 @@ Created with Social Neuron`;
5063
5418
  content: [
5064
5419
  {
5065
5420
  type: "text",
5066
- text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections."
5421
+ text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections." + (projectAutoResolvedNote ? `
5422
+
5423
+ Note: ${projectAutoResolvedNote}` : "")
5067
5424
  }
5068
5425
  ]
5069
5426
  };
5070
5427
  }
5071
5428
  const lines = [
5072
- `${accounts.length} connected account(s)${project_id ? ` for project ${project_id}` : ""}:`,
5429
+ `${accounts.length} connected account(s) for project ${resolvedProjectId}:`,
5073
5430
  ""
5074
5431
  ];
5075
5432
  for (const account of accounts) {
@@ -5081,8 +5438,14 @@ Created with Social Neuron`;
5081
5438
  ` ${platformLower}: ${name} | id=${account.id} | ${project} | status=${status} (connected ${account.created_at.split("T")[0]})`
5082
5439
  );
5083
5440
  }
5441
+ if (projectAutoResolvedNote) {
5442
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
5443
+ }
5084
5444
  if (format === "json") {
5085
- const structuredContent = asEnvelope3({ accounts });
5445
+ const structuredContent = asEnvelope3({
5446
+ accounts,
5447
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
5448
+ });
5086
5449
  return {
5087
5450
  structuredContent,
5088
5451
  content: [
@@ -5277,7 +5640,10 @@ Created with Social Neuron`;
5277
5640
  if (!Number.isFinite(startDate.getTime())) {
5278
5641
  return {
5279
5642
  content: [
5280
- { type: "text", text: "start_after must be a valid ISO datetime." }
5643
+ {
5644
+ type: "text",
5645
+ text: "start_after must be a valid ISO datetime."
5646
+ }
5281
5647
  ],
5282
5648
  isError: true
5283
5649
  };
@@ -5378,11 +5744,14 @@ Created with Social Neuron`;
5378
5744
  "Schedule all posts in a content plan. Optionally auto-assigns time slots and runs quality checks before scheduling. Supports dry-run mode.",
5379
5745
  {
5380
5746
  plan: z3.object({
5747
+ project_id: z3.string().uuid().optional(),
5748
+ account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe("Exact connected-account ID per platform."),
5381
5749
  posts: z3.array(
5382
5750
  z3.object({
5383
5751
  id: z3.string(),
5384
5752
  caption: z3.string(),
5385
5753
  platform: z3.string(),
5754
+ connected_account_id: z3.string().uuid().optional(),
5386
5755
  title: z3.string().optional(),
5387
5756
  media_url: z3.string().optional(),
5388
5757
  schedule_at: z3.string().optional(),
@@ -5390,6 +5759,12 @@ Created with Social Neuron`;
5390
5759
  })
5391
5760
  )
5392
5761
  }).passthrough().optional(),
5762
+ project_id: z3.string().uuid().optional().describe(
5763
+ "Exact brand/project ID. Defaults only when the authenticated user has one project."
5764
+ ),
5765
+ account_ids: z3.record(z3.string(), z3.string().uuid()).optional().describe(
5766
+ "Exact connected-account ID per platform for every post in the plan."
5767
+ ),
5393
5768
  plan_id: z3.string().uuid().optional().describe("Persisted content plan ID from content_plans table"),
5394
5769
  auto_slot: z3.boolean().default(true).describe("Auto-assign time slots for posts without schedule_at"),
5395
5770
  dry_run: z3.boolean().default(false).describe("Preview without actually scheduling"),
@@ -5406,6 +5781,8 @@ Created with Social Neuron`;
5406
5781
  async ({
5407
5782
  plan,
5408
5783
  plan_id,
5784
+ project_id,
5785
+ account_ids,
5409
5786
  auto_slot,
5410
5787
  dry_run,
5411
5788
  response_format,
@@ -5415,9 +5792,11 @@ Created with Social Neuron`;
5415
5792
  idempotency_seed
5416
5793
  }) => {
5417
5794
  try {
5795
+ const effectiveBatchSize = batch_size ?? 4;
5418
5796
  let workingPlan = plan;
5419
5797
  let effectivePlanId = plan_id;
5420
- let effectiveProjectId;
5798
+ let effectiveProjectId = project_id;
5799
+ let projectAutoResolvedNote;
5421
5800
  let approvalSummary;
5422
5801
  if (!workingPlan && plan_id) {
5423
5802
  const { data: planResult, error: planError } = await callEdgeFunction("mcp-data", { action: "get-content-plan", plan_id });
@@ -5464,7 +5843,18 @@ Created with Social Neuron`;
5464
5843
  posts: postsFromPayload
5465
5844
  };
5466
5845
  effectivePlanId = stored.id;
5467
- effectiveProjectId = stored.project_id ?? void 0;
5846
+ if (effectiveProjectId && stored.project_id && effectiveProjectId !== stored.project_id) {
5847
+ return {
5848
+ content: [
5849
+ {
5850
+ type: "text",
5851
+ text: `project_id ${effectiveProjectId} does not own plan ${plan_id}.`
5852
+ }
5853
+ ],
5854
+ isError: true
5855
+ };
5856
+ }
5857
+ effectiveProjectId = stored.project_id ?? effectiveProjectId;
5468
5858
  }
5469
5859
  if (!workingPlan) {
5470
5860
  return {
@@ -5477,10 +5867,35 @@ Created with Social Neuron`;
5477
5867
  isError: true
5478
5868
  };
5479
5869
  }
5870
+ const planProjectId = workingPlan.project_id;
5871
+ if (effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0 && effectiveProjectId !== planProjectId) {
5872
+ return {
5873
+ content: [
5874
+ {
5875
+ type: "text",
5876
+ text: `Conflicting project_id values were supplied for the plan (${planProjectId}) and request (${effectiveProjectId}).`
5877
+ }
5878
+ ],
5879
+ isError: true
5880
+ };
5881
+ }
5882
+ if (!effectiveProjectId && typeof planProjectId === "string" && planProjectId.length > 0) {
5883
+ effectiveProjectId = planProjectId;
5884
+ }
5480
5885
  if (!effectiveProjectId) {
5481
- const planProjectId = workingPlan.project_id;
5482
- if (typeof planProjectId === "string" && planProjectId.length > 0) {
5483
- effectiveProjectId = planProjectId;
5886
+ const projectResolution = await resolveProjectForConnectedAccountTool();
5887
+ effectiveProjectId = projectResolution.projectId;
5888
+ projectAutoResolvedNote = projectResolution.autoResolvedNote;
5889
+ if (!effectiveProjectId) {
5890
+ return {
5891
+ content: [
5892
+ {
5893
+ type: "text",
5894
+ 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."
5895
+ }
5896
+ ],
5897
+ isError: true
5898
+ };
5484
5899
  }
5485
5900
  }
5486
5901
  if (effectivePlanId) {
@@ -5699,6 +6114,61 @@ Created with Social Neuron`;
5699
6114
  isError: false
5700
6115
  };
5701
6116
  }
6117
+ const embeddedAccountIds = workingPlan.account_ids ?? {};
6118
+ for (const [platform2, accountId] of Object.entries(account_ids ?? {})) {
6119
+ if (embeddedAccountIds[platform2] && embeddedAccountIds[platform2] !== accountId) {
6120
+ return {
6121
+ content: [
6122
+ {
6123
+ type: "text",
6124
+ text: `Conflicting account_ids values were supplied for ${platform2}.`
6125
+ }
6126
+ ],
6127
+ isError: true
6128
+ };
6129
+ }
6130
+ }
6131
+ const requestedPlanAccountIds = {
6132
+ ...embeddedAccountIds,
6133
+ ...account_ids ?? {}
6134
+ };
6135
+ for (const post of workingPlan.posts) {
6136
+ if (!post.connected_account_id) continue;
6137
+ const key = post.platform.toLowerCase() === "x" ? "twitter" : post.platform.toLowerCase();
6138
+ const existing = requestedPlanAccountIds[key];
6139
+ if (existing && existing !== post.connected_account_id) {
6140
+ return {
6141
+ content: [
6142
+ {
6143
+ type: "text",
6144
+ text: `Plan contains conflicting connected_account_id values for ${post.platform}. Use separate plans when scheduling the same platform through different accounts.`
6145
+ }
6146
+ ],
6147
+ isError: true
6148
+ };
6149
+ }
6150
+ requestedPlanAccountIds[key] = post.connected_account_id;
6151
+ }
6152
+ const planPlatforms = Array.from(
6153
+ new Set(workingPlan.posts.map((post) => post.platform))
6154
+ );
6155
+ const planRouting = await resolveConnectedAccountRouting({
6156
+ projectId: effectiveProjectId,
6157
+ platforms: planPlatforms,
6158
+ requestedAccountIds: requestedPlanAccountIds
6159
+ });
6160
+ if (planRouting.error || !planRouting.connectedAccountIds) {
6161
+ return {
6162
+ content: [
6163
+ {
6164
+ type: "text",
6165
+ text: `Cannot schedule plan \u2014 ${planRouting.error ?? "exact connected-account routing could not be established."}`
6166
+ }
6167
+ ],
6168
+ isError: true
6169
+ };
6170
+ }
6171
+ const verifiedPlanAccountIds = planRouting.connectedAccountIds;
5702
6172
  let scheduled = 0;
5703
6173
  let failed = 0;
5704
6174
  const results = [];
@@ -5727,7 +6197,7 @@ Created with Social Neuron`;
5727
6197
  retryable: false
5728
6198
  };
5729
6199
  }
5730
- const normalizedPlatform = PLATFORM_CASE_MAP[post.platform.toLowerCase()] ?? post.platform;
6200
+ const normalizedPlatform = PLATFORM_CASE_MAP2[post.platform.toLowerCase()] ?? post.platform;
5731
6201
  const idempotencyKey = buildIdempotencyKey(post);
5732
6202
  const { data, error } = await callEdgeFunction(
5733
6203
  "schedule-post",
@@ -5738,6 +6208,13 @@ Created with Social Neuron`;
5738
6208
  mediaUrl: post.media_url,
5739
6209
  scheduledAt: post.schedule_at,
5740
6210
  hashtags: post.hashtags,
6211
+ ...effectiveProjectId ? {
6212
+ projectId: effectiveProjectId,
6213
+ project_id: effectiveProjectId
6214
+ } : {},
6215
+ connectedAccountIds: {
6216
+ [normalizedPlatform]: verifiedPlanAccountIds[normalizedPlatform]
6217
+ },
5741
6218
  ...effectivePlanId ? { planId: effectivePlanId } : {},
5742
6219
  idempotencyKey
5743
6220
  },
@@ -5796,7 +6273,7 @@ Created with Social Neuron`;
5796
6273
  const platformBatches = Array.from(grouped.entries()).map(
5797
6274
  async ([platform2, platformPosts]) => {
5798
6275
  const platformResults = [];
5799
- const batches = chunk(platformPosts, batch_size);
6276
+ const batches = chunk(platformPosts, effectiveBatchSize);
5800
6277
  for (const batch of batches) {
5801
6278
  const settled = await Promise.allSettled(
5802
6279
  batch.map((post) => scheduleOne(post))
@@ -5857,7 +6334,8 @@ Created with Social Neuron`;
5857
6334
  total_posts: workingPlan.posts.length,
5858
6335
  scheduled,
5859
6336
  failed
5860
- }
6337
+ },
6338
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
5861
6339
  }),
5862
6340
  null,
5863
6341
  2
@@ -5881,6 +6359,9 @@ Created with Social Neuron`;
5881
6359
  lines.push(
5882
6360
  `Scheduled: ${scheduled}/${workingPlan.posts.length} | Failed: ${failed}/${workingPlan.posts.length}`
5883
6361
  );
6362
+ if (projectAutoResolvedNote) {
6363
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
6364
+ }
5884
6365
  return {
5885
6366
  content: [{ type: "text", text: lines.join("\n") }],
5886
6367
  isError: failed > 0
@@ -5902,6 +6383,7 @@ Created with Social Neuron`;
5902
6383
  }
5903
6384
 
5904
6385
  // src/tools/media.ts
6386
+ init_edge_function();
5905
6387
  import { z as z4 } from "zod";
5906
6388
  import { readFile } from "node:fs/promises";
5907
6389
  import { basename, extname } from "node:path";
@@ -6371,6 +6853,7 @@ function registerMediaTools(server) {
6371
6853
 
6372
6854
  // src/tools/analytics.ts
6373
6855
  init_supabase();
6856
+ init_edge_function();
6374
6857
  import { z as z5 } from "zod";
6375
6858
  function asEnvelope4(data) {
6376
6859
  return {
@@ -6402,15 +6885,35 @@ function registerAnalyticsTools(server) {
6402
6885
  content_id: z5.string().uuid().optional().describe(
6403
6886
  "Filter to a specific content_history ID to see performance of one piece of content."
6404
6887
  ),
6405
- project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
6888
+ project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
6406
6889
  limit: z5.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
6407
6890
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6408
6891
  },
6409
- async ({ platform: platform2, days, content_id, project_id, limit, response_format }) => {
6892
+ async ({
6893
+ platform: platform2,
6894
+ days,
6895
+ content_id,
6896
+ project_id,
6897
+ limit,
6898
+ response_format
6899
+ }) => {
6410
6900
  const format = response_format ?? "text";
6411
6901
  const lookbackDays = days ?? 30;
6412
6902
  const maxPosts = limit ?? 20;
6413
- const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
6903
+ const projectResolution = await resolveProjectStrict(project_id);
6904
+ if (!projectResolution.projectId) {
6905
+ return {
6906
+ content: [
6907
+ {
6908
+ type: "text",
6909
+ 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."
6910
+ }
6911
+ ],
6912
+ isError: true
6913
+ };
6914
+ }
6915
+ const resolvedProjectId = projectResolution.projectId;
6916
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
6414
6917
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
6415
6918
  action: "analytics",
6416
6919
  platform: platform2,
@@ -6421,11 +6924,17 @@ function registerAnalyticsTools(server) {
6421
6924
  limit: Math.min(maxPosts * 5, 100),
6422
6925
  latestOnly: true,
6423
6926
  contentId: content_id,
6424
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
6927
+ projectId: resolvedProjectId,
6928
+ project_id: resolvedProjectId
6425
6929
  });
6426
6930
  if (efError) {
6427
6931
  return {
6428
- content: [{ type: "text", text: `Failed to fetch analytics: ${efError}` }],
6932
+ content: [
6933
+ {
6934
+ type: "text",
6935
+ text: `Failed to fetch analytics: ${efError}`
6936
+ }
6937
+ ],
6429
6938
  isError: true
6430
6939
  };
6431
6940
  }
@@ -6445,7 +6954,8 @@ function registerAnalyticsTools(server) {
6445
6954
  totalViews: 0,
6446
6955
  totalEngagement: 0,
6447
6956
  postCount: 0,
6448
- posts: []
6957
+ posts: [],
6958
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
6449
6959
  });
6450
6960
  return {
6451
6961
  structuredContent,
@@ -6461,7 +6971,9 @@ function registerAnalyticsTools(server) {
6461
6971
  content: [
6462
6972
  {
6463
6973
  type: "text",
6464
- text: `No analytics data found for the last ${lookbackDays} days${platform2 ? ` on ${platform2}` : ""}.`
6974
+ text: `No analytics data found for the last ${lookbackDays} days${platform2 ? ` on ${platform2}` : ""}.` + (projectAutoResolvedNote ? `
6975
+
6976
+ Note: ${projectAutoResolvedNote}` : "")
6465
6977
  }
6466
6978
  ]
6467
6979
  };
@@ -6493,20 +7005,27 @@ function registerAnalyticsTools(server) {
6493
7005
  postCount: posts.length,
6494
7006
  posts
6495
7007
  };
6496
- return formatAnalytics(summary, lookbackDays, format);
7008
+ return formatAnalytics(
7009
+ summary,
7010
+ lookbackDays,
7011
+ format,
7012
+ projectAutoResolvedNote
7013
+ );
6497
7014
  }
6498
7015
  );
6499
7016
  server.tool(
6500
7017
  "refresh_platform_analytics",
6501
7018
  "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
7019
  {
6503
- project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
7020
+ project_id: z5.string().uuid().optional().describe("Project ID. Defaults to the active project context."),
6504
7021
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6505
7022
  },
6506
7023
  async ({ project_id, response_format }) => {
6507
7024
  const format = response_format ?? "text";
6508
7025
  const userId = await getDefaultUserId();
6509
- const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
7026
+ const projectResolution = await resolveProjectStrict(project_id);
7027
+ const resolvedProjectId = projectResolution.projectId;
7028
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
6510
7029
  const rateLimit = checkRateLimit(
6511
7030
  "posting",
6512
7031
  `refresh_platform_analytics:${userId}:${resolvedProjectId ?? "default"}`
@@ -6522,25 +7041,48 @@ function registerAnalyticsTools(server) {
6522
7041
  isError: true
6523
7042
  };
6524
7043
  }
7044
+ if (!resolvedProjectId) {
7045
+ return {
7046
+ content: [
7047
+ {
7048
+ type: "text",
7049
+ 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."
7050
+ }
7051
+ ],
7052
+ isError: true
7053
+ };
7054
+ }
6525
7055
  const { data, error } = await callEdgeFunction("fetch-analytics", {
6526
7056
  userId,
6527
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
7057
+ projectId: resolvedProjectId,
7058
+ project_id: resolvedProjectId
6528
7059
  });
6529
7060
  if (error) {
6530
7061
  return {
6531
- content: [{ type: "text", text: `Error refreshing analytics: ${error}` }],
7062
+ content: [
7063
+ {
7064
+ type: "text",
7065
+ text: `Error refreshing analytics: ${error}`
7066
+ }
7067
+ ],
6532
7068
  isError: true
6533
7069
  };
6534
7070
  }
6535
7071
  const result = data;
6536
7072
  if (!result.success) {
6537
7073
  return {
6538
- content: [{ type: "text", text: "Analytics refresh failed." }],
7074
+ content: [
7075
+ { type: "text", text: "Analytics refresh failed." }
7076
+ ],
6539
7077
  isError: true
6540
7078
  };
6541
7079
  }
6542
- const queued = (result.results ?? []).filter((r) => r.status === "queued").length;
6543
- const errored = (result.results ?? []).filter((r) => r.status === "error").length;
7080
+ const queued = (result.results ?? []).filter(
7081
+ (r) => r.status === "queued"
7082
+ ).length;
7083
+ const errored = (result.results ?? []).filter(
7084
+ (r) => r.status === "error"
7085
+ ).length;
6544
7086
  const lines = [
6545
7087
  `Analytics refresh triggered successfully.`,
6546
7088
  ` Posts processed: ${result.postsProcessed}`,
@@ -6549,13 +7091,17 @@ function registerAnalyticsTools(server) {
6549
7091
  if (errored > 0) {
6550
7092
  lines.push(` Errors: ${errored}`);
6551
7093
  }
7094
+ if (projectAutoResolvedNote) {
7095
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
7096
+ }
6552
7097
  if (format === "json") {
6553
7098
  const structuredContent = asEnvelope4({
6554
7099
  success: true,
6555
7100
  postsProcessed: result.postsProcessed,
6556
7101
  queued,
6557
7102
  errored,
6558
- projectId: resolvedProjectId ?? null
7103
+ projectId: resolvedProjectId ?? null,
7104
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
6559
7105
  });
6560
7106
  return {
6561
7107
  structuredContent,
@@ -6571,12 +7117,21 @@ function registerAnalyticsTools(server) {
6571
7117
  }
6572
7118
  );
6573
7119
  }
6574
- function formatAnalytics(summary, days, format) {
6575
- const structuredContent = asEnvelope4({ ...summary, days });
7120
+ function formatAnalytics(summary, days, format, projectAutoResolvedNote) {
7121
+ const structuredContent = asEnvelope4({
7122
+ ...summary,
7123
+ days,
7124
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
7125
+ });
6576
7126
  if (format === "json") {
6577
7127
  return {
6578
7128
  structuredContent,
6579
- content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }]
7129
+ content: [
7130
+ {
7131
+ type: "text",
7132
+ text: JSON.stringify(structuredContent, null, 2)
7133
+ }
7134
+ ]
6580
7135
  };
6581
7136
  }
6582
7137
  const lines = [
@@ -6600,6 +7155,9 @@ function formatAnalytics(summary, days, format) {
6600
7155
  lines.push(line);
6601
7156
  }
6602
7157
  }
7158
+ if (projectAutoResolvedNote) {
7159
+ lines.push("", `Note: ${projectAutoResolvedNote}`);
7160
+ }
6603
7161
  return {
6604
7162
  structuredContent,
6605
7163
  content: [{ type: "text", text: lines.join("\n") }]
@@ -6607,8 +7165,9 @@ function formatAnalytics(summary, days, format) {
6607
7165
  }
6608
7166
 
6609
7167
  // src/tools/brand.ts
6610
- import { z as z6 } from "zod";
7168
+ init_edge_function();
6611
7169
  init_supabase();
7170
+ import { z as z6 } from "zod";
6612
7171
 
6613
7172
  // src/lib/brandUrlInput.ts
6614
7173
  var PLATFORM_ALIASES = {
@@ -7348,6 +7907,7 @@ import { z as z8 } from "zod";
7348
7907
  import { resolve as resolve2 } from "node:path";
7349
7908
  import { mkdir as mkdir2 } from "node:fs/promises";
7350
7909
  init_supabase();
7910
+ init_edge_function();
7351
7911
  var COMPOSITIONS = [
7352
7912
  {
7353
7913
  id: "CaptionedClip",
@@ -7699,8 +8259,9 @@ function registerRemotionTools(server) {
7699
8259
  }
7700
8260
 
7701
8261
  // src/tools/insights.ts
7702
- import { z as z9 } from "zod";
8262
+ init_edge_function();
7703
8263
  init_supabase();
8264
+ import { z as z9 } from "zod";
7704
8265
  var MAX_INSIGHT_AGE_DAYS = 30;
7705
8266
  var PLATFORM_ENUM = [
7706
8267
  "youtube",
@@ -7956,6 +8517,8 @@ function registerInsightsTools(server) {
7956
8517
  }
7957
8518
 
7958
8519
  // src/tools/youtube-analytics.ts
8520
+ init_edge_function();
8521
+ init_supabase();
7959
8522
  import { z as z10 } from "zod";
7960
8523
  function asEnvelope7(data) {
7961
8524
  return {
@@ -7977,15 +8540,64 @@ function registerYouTubeAnalyticsTools(server) {
7977
8540
  start_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Start date in YYYY-MM-DD format."),
7978
8541
  end_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("End date in YYYY-MM-DD format."),
7979
8542
  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.'),
8543
+ max_results: z10.number().min(1).max(50).optional().describe(
8544
+ 'Max videos to return for "topVideos" action. Defaults to 10.'
8545
+ ),
8546
+ connected_account_id: z10.string().uuid().optional().describe(
8547
+ "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."
8548
+ ),
8549
+ project_id: z10.string().uuid().optional().describe(
8550
+ "Exact brand/project ID. Defaults only when the authenticated user has one project."
8551
+ ),
7981
8552
  response_format: z10.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7982
8553
  },
7983
- async ({ action, start_date, end_date, video_id, max_results, response_format }) => {
8554
+ async ({
8555
+ action,
8556
+ start_date,
8557
+ end_date,
8558
+ video_id,
8559
+ max_results,
8560
+ connected_account_id,
8561
+ project_id,
8562
+ response_format
8563
+ }) => {
7984
8564
  const format = response_format ?? "text";
7985
8565
  if (action === "video" && !video_id) {
7986
8566
  return {
7987
8567
  content: [
7988
- { type: "text", text: 'Error: video_id is required when action is "video".' }
8568
+ {
8569
+ type: "text",
8570
+ text: 'Error: video_id is required when action is "video".'
8571
+ }
8572
+ ],
8573
+ isError: true
8574
+ };
8575
+ }
8576
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
8577
+ if (!resolvedProjectId) {
8578
+ return {
8579
+ content: [
8580
+ {
8581
+ type: "text",
8582
+ text: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
8583
+ }
8584
+ ],
8585
+ isError: true
8586
+ };
8587
+ }
8588
+ const routing = await resolveConnectedAccountRouting({
8589
+ projectId: resolvedProjectId,
8590
+ platforms: ["youtube"],
8591
+ requestedAccountIds: connected_account_id ? { youtube: connected_account_id } : void 0
8592
+ });
8593
+ const resolvedAccountId = routing.connectedAccountIds?.YouTube;
8594
+ if (routing.error || !resolvedAccountId) {
8595
+ return {
8596
+ content: [
8597
+ {
8598
+ type: "text",
8599
+ text: routing.error ?? "YouTube: exact connected-account routing could not be established."
8600
+ }
7989
8601
  ],
7990
8602
  isError: true
7991
8603
  };
@@ -7995,11 +8607,19 @@ function registerYouTubeAnalyticsTools(server) {
7995
8607
  startDate: start_date,
7996
8608
  endDate: end_date,
7997
8609
  videoId: video_id,
7998
- maxResults: max_results ?? 10
8610
+ maxResults: max_results ?? 10,
8611
+ projectId: resolvedProjectId,
8612
+ project_id: resolvedProjectId,
8613
+ connectedAccountId: resolvedAccountId
7999
8614
  });
8000
8615
  if (error) {
8001
8616
  return {
8002
- content: [{ type: "text", text: `YouTube Analytics error: ${error}` }],
8617
+ content: [
8618
+ {
8619
+ type: "text",
8620
+ text: `YouTube Analytics error: ${error}`
8621
+ }
8622
+ ],
8003
8623
  isError: true
8004
8624
  };
8005
8625
  }
@@ -8012,7 +8632,12 @@ function registerYouTubeAnalyticsTools(server) {
8012
8632
  {
8013
8633
  type: "text",
8014
8634
  text: JSON.stringify(
8015
- asEnvelope7({ action, startDate: start_date, endDate: end_date, analytics: a }),
8635
+ asEnvelope7({
8636
+ action,
8637
+ startDate: start_date,
8638
+ endDate: end_date,
8639
+ analytics: a
8640
+ }),
8016
8641
  null,
8017
8642
  2
8018
8643
  )
@@ -8039,7 +8664,10 @@ function registerYouTubeAnalyticsTools(server) {
8039
8664
  if (days.length === 0) {
8040
8665
  return {
8041
8666
  content: [
8042
- { type: "text", text: "No daily analytics data found for this period." }
8667
+ {
8668
+ type: "text",
8669
+ text: "No daily analytics data found for this period."
8670
+ }
8043
8671
  ]
8044
8672
  };
8045
8673
  }
@@ -8062,7 +8690,10 @@ function registerYouTubeAnalyticsTools(server) {
8062
8690
  ]
8063
8691
  };
8064
8692
  }
8065
- const lines = [`YouTube Daily Analytics (${start_date} to ${end_date}):`, ""];
8693
+ const lines = [
8694
+ `YouTube Daily Analytics (${start_date} to ${end_date}):`,
8695
+ ""
8696
+ ];
8066
8697
  for (const d of days) {
8067
8698
  lines.push(
8068
8699
  ` ${d.date}: ${d.views.toLocaleString()} views, ${d.watchTimeMinutes.toLocaleString()} min watch, +${d.subscribersGained} subs, ${d.likes} likes, ${d.comments} comments`
@@ -8109,7 +8740,12 @@ function registerYouTubeAnalyticsTools(server) {
8109
8740
  const videos = result.topVideos ?? [];
8110
8741
  if (videos.length === 0) {
8111
8742
  return {
8112
- content: [{ type: "text", text: "No top videos found for this period." }]
8743
+ content: [
8744
+ {
8745
+ type: "text",
8746
+ text: "No top videos found for this period."
8747
+ }
8748
+ ]
8113
8749
  };
8114
8750
  }
8115
8751
  if (format === "json") {
@@ -8131,7 +8767,10 @@ function registerYouTubeAnalyticsTools(server) {
8131
8767
  ]
8132
8768
  };
8133
8769
  }
8134
- const lines = [`Top ${videos.length} YouTube Videos (${start_date} to ${end_date}):`, ""];
8770
+ const lines = [
8771
+ `Top ${videos.length} YouTube Videos (${start_date} to ${end_date}):`,
8772
+ ""
8773
+ ];
8135
8774
  for (let i = 0; i < videos.length; i++) {
8136
8775
  const v = videos[i];
8137
8776
  lines.push(
@@ -8144,17 +8783,25 @@ function registerYouTubeAnalyticsTools(server) {
8144
8783
  }
8145
8784
  if (format === "json") {
8146
8785
  return {
8147
- content: [{ type: "text", text: JSON.stringify(asEnvelope7(result), null, 2) }]
8786
+ content: [
8787
+ {
8788
+ type: "text",
8789
+ text: JSON.stringify(asEnvelope7(result), null, 2)
8790
+ }
8791
+ ]
8148
8792
  };
8149
8793
  }
8150
8794
  return {
8151
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
8795
+ content: [
8796
+ { type: "text", text: JSON.stringify(result, null, 2) }
8797
+ ]
8152
8798
  };
8153
8799
  }
8154
8800
  );
8155
8801
  }
8156
8802
 
8157
8803
  // src/tools/comments.ts
8804
+ init_edge_function();
8158
8805
  import { z as z11 } from "zod";
8159
8806
  init_supabase();
8160
8807
  function asEnvelope8(data) {
@@ -8166,6 +8813,32 @@ function asEnvelope8(data) {
8166
8813
  data
8167
8814
  };
8168
8815
  }
8816
+ async function exactYouTubeRoute(projectId, connectedAccountId) {
8817
+ const resolvedProjectId = projectId ?? await getDefaultProjectId() ?? void 0;
8818
+ if (!resolvedProjectId) {
8819
+ return {
8820
+ error: "project_id is required. Configure an explicit project or use an API key scoped to exactly one project."
8821
+ };
8822
+ }
8823
+ const routing = await resolveConnectedAccountRouting({
8824
+ projectId: resolvedProjectId,
8825
+ platforms: ["youtube"],
8826
+ requestedAccountIds: connectedAccountId ? { youtube: connectedAccountId } : void 0
8827
+ });
8828
+ const resolvedAccountId = routing.connectedAccountIds?.YouTube;
8829
+ if (routing.error || !resolvedAccountId) {
8830
+ return {
8831
+ error: routing.error ?? "YouTube: exact connected-account routing could not be established."
8832
+ };
8833
+ }
8834
+ return {
8835
+ projectId: resolvedProjectId,
8836
+ connectedAccountId: resolvedAccountId
8837
+ };
8838
+ }
8839
+ var PROJECT_ID_SCHEMA = z11.string().uuid().optional().describe(
8840
+ "Exact brand/project ID. Defaults only when the authenticated user has one project."
8841
+ );
8169
8842
  function registerCommentsTools(server) {
8170
8843
  server.tool(
8171
8844
  "list_comments",
@@ -8178,19 +8851,42 @@ function registerCommentsTools(server) {
8178
8851
  page_token: z11.string().optional().describe(
8179
8852
  "Pagination cursor from previous list_comments response nextPageToken field. Omit for first page of results."
8180
8853
  ),
8854
+ connected_account_id: z11.string().uuid().optional().describe(
8855
+ "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."
8856
+ ),
8857
+ project_id: PROJECT_ID_SCHEMA,
8181
8858
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8182
8859
  },
8183
- async ({ video_id, max_results, page_token, response_format }) => {
8860
+ async ({
8861
+ video_id,
8862
+ max_results,
8863
+ page_token,
8864
+ connected_account_id,
8865
+ project_id,
8866
+ response_format
8867
+ }) => {
8184
8868
  const format = response_format ?? "text";
8869
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
8870
+ if ("error" in route) {
8871
+ return {
8872
+ content: [{ type: "text", text: route.error }],
8873
+ isError: true
8874
+ };
8875
+ }
8185
8876
  const { data, error } = await callEdgeFunction("youtube-comments", {
8186
8877
  action: "list",
8187
8878
  videoId: video_id,
8188
8879
  maxResults: max_results ?? 50,
8189
- pageToken: page_token
8880
+ pageToken: page_token,
8881
+ projectId: route.projectId,
8882
+ project_id: route.projectId,
8883
+ connectedAccountId: route.connectedAccountId
8190
8884
  });
8191
8885
  if (error) {
8192
8886
  return {
8193
- content: [{ type: "text", text: `Error listing comments: ${error}` }],
8887
+ content: [
8888
+ { type: "text", text: `Error listing comments: ${error}` }
8889
+ ],
8194
8890
  isError: true
8195
8891
  };
8196
8892
  }
@@ -8202,7 +8898,10 @@ function registerCommentsTools(server) {
8202
8898
  {
8203
8899
  type: "text",
8204
8900
  text: JSON.stringify(
8205
- asEnvelope8({ comments, nextPageToken: result.nextPageToken ?? null }),
8901
+ asEnvelope8({
8902
+ comments,
8903
+ nextPageToken: result.nextPageToken ?? null
8904
+ }),
8206
8905
  null,
8207
8906
  2
8208
8907
  )
@@ -8239,12 +8938,31 @@ function registerCommentsTools(server) {
8239
8938
  "reply_to_comment",
8240
8939
  "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
8940
  {
8242
- parent_id: z11.string().describe("The ID of the parent comment to reply to (from list_comments)."),
8941
+ parent_id: z11.string().describe(
8942
+ "The ID of the parent comment to reply to (from list_comments)."
8943
+ ),
8243
8944
  text: z11.string().min(1).describe("The reply text."),
8945
+ connected_account_id: z11.string().uuid().optional().describe(
8946
+ "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."
8947
+ ),
8948
+ project_id: PROJECT_ID_SCHEMA,
8244
8949
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8245
8950
  },
8246
- async ({ parent_id, text, response_format }) => {
8951
+ async ({
8952
+ parent_id,
8953
+ text,
8954
+ connected_account_id,
8955
+ project_id,
8956
+ response_format
8957
+ }) => {
8247
8958
  const format = response_format ?? "text";
8959
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
8960
+ if ("error" in route) {
8961
+ return {
8962
+ content: [{ type: "text", text: route.error }],
8963
+ isError: true
8964
+ };
8965
+ }
8248
8966
  const userId = await getDefaultUserId();
8249
8967
  const rateLimit = checkRateLimit("posting", `reply_to_comment:${userId}`);
8250
8968
  if (!rateLimit.allowed) {
@@ -8261,18 +8979,31 @@ function registerCommentsTools(server) {
8261
8979
  const { data, error } = await callEdgeFunction("youtube-comments", {
8262
8980
  action: "reply",
8263
8981
  parentId: parent_id,
8264
- text
8982
+ text,
8983
+ projectId: route.projectId,
8984
+ project_id: route.projectId,
8985
+ connectedAccountId: route.connectedAccountId
8265
8986
  });
8266
8987
  if (error) {
8267
8988
  return {
8268
- content: [{ type: "text", text: `Error replying to comment: ${error}` }],
8989
+ content: [
8990
+ {
8991
+ type: "text",
8992
+ text: `Error replying to comment: ${error}`
8993
+ }
8994
+ ],
8269
8995
  isError: true
8270
8996
  };
8271
8997
  }
8272
8998
  const result = data;
8273
8999
  if (format === "json") {
8274
9000
  return {
8275
- content: [{ type: "text", text: JSON.stringify(asEnvelope8(result), null, 2) }]
9001
+ content: [
9002
+ {
9003
+ type: "text",
9004
+ text: JSON.stringify(asEnvelope8(result), null, 2)
9005
+ }
9006
+ ]
8276
9007
  };
8277
9008
  }
8278
9009
  return {
@@ -8293,10 +9024,27 @@ function registerCommentsTools(server) {
8293
9024
  {
8294
9025
  video_id: z11.string().describe("The YouTube video ID to comment on."),
8295
9026
  text: z11.string().min(1).describe("The comment text."),
9027
+ connected_account_id: z11.string().uuid().optional().describe(
9028
+ "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."
9029
+ ),
9030
+ project_id: PROJECT_ID_SCHEMA,
8296
9031
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8297
9032
  },
8298
- async ({ video_id, text, response_format }) => {
9033
+ async ({
9034
+ video_id,
9035
+ text,
9036
+ connected_account_id,
9037
+ project_id,
9038
+ response_format
9039
+ }) => {
8299
9040
  const format = response_format ?? "text";
9041
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
9042
+ if ("error" in route) {
9043
+ return {
9044
+ content: [{ type: "text", text: route.error }],
9045
+ isError: true
9046
+ };
9047
+ }
8300
9048
  const userId = await getDefaultUserId();
8301
9049
  const rateLimit = checkRateLimit("posting", `post_comment:${userId}`);
8302
9050
  if (!rateLimit.allowed) {
@@ -8313,18 +9061,28 @@ function registerCommentsTools(server) {
8313
9061
  const { data, error } = await callEdgeFunction("youtube-comments", {
8314
9062
  action: "post",
8315
9063
  videoId: video_id,
8316
- text
9064
+ text,
9065
+ projectId: route.projectId,
9066
+ project_id: route.projectId,
9067
+ connectedAccountId: route.connectedAccountId
8317
9068
  });
8318
9069
  if (error) {
8319
9070
  return {
8320
- content: [{ type: "text", text: `Error posting comment: ${error}` }],
9071
+ content: [
9072
+ { type: "text", text: `Error posting comment: ${error}` }
9073
+ ],
8321
9074
  isError: true
8322
9075
  };
8323
9076
  }
8324
9077
  const result = data;
8325
9078
  if (format === "json") {
8326
9079
  return {
8327
- content: [{ type: "text", text: JSON.stringify(asEnvelope8(result), null, 2) }]
9080
+ content: [
9081
+ {
9082
+ type: "text",
9083
+ text: JSON.stringify(asEnvelope8(result), null, 2)
9084
+ }
9085
+ ]
8328
9086
  };
8329
9087
  }
8330
9088
  return {
@@ -8345,10 +9103,27 @@ function registerCommentsTools(server) {
8345
9103
  {
8346
9104
  comment_id: z11.string().describe("The comment ID to moderate."),
8347
9105
  moderation_status: z11.enum(["published", "rejected"]).describe('"published" to approve, "rejected" to hide.'),
9106
+ connected_account_id: z11.string().uuid().optional().describe(
9107
+ "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."
9108
+ ),
9109
+ project_id: PROJECT_ID_SCHEMA,
8348
9110
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8349
9111
  },
8350
- async ({ comment_id, moderation_status, response_format }) => {
9112
+ async ({
9113
+ comment_id,
9114
+ moderation_status,
9115
+ connected_account_id,
9116
+ project_id,
9117
+ response_format
9118
+ }) => {
8351
9119
  const format = response_format ?? "text";
9120
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
9121
+ if ("error" in route) {
9122
+ return {
9123
+ content: [{ type: "text", text: route.error }],
9124
+ isError: true
9125
+ };
9126
+ }
8352
9127
  const userId = await getDefaultUserId();
8353
9128
  const rateLimit = checkRateLimit("posting", `moderate_comment:${userId}`);
8354
9129
  if (!rateLimit.allowed) {
@@ -8365,11 +9140,19 @@ function registerCommentsTools(server) {
8365
9140
  const { error } = await callEdgeFunction("youtube-comments", {
8366
9141
  action: "moderate",
8367
9142
  commentId: comment_id,
8368
- moderationStatus: moderation_status
9143
+ moderationStatus: moderation_status,
9144
+ projectId: route.projectId,
9145
+ project_id: route.projectId,
9146
+ connectedAccountId: route.connectedAccountId
8369
9147
  });
8370
9148
  if (error) {
8371
9149
  return {
8372
- content: [{ type: "text", text: `Error moderating comment: ${error}` }],
9150
+ content: [
9151
+ {
9152
+ type: "text",
9153
+ text: `Error moderating comment: ${error}`
9154
+ }
9155
+ ],
8373
9156
  isError: true
8374
9157
  };
8375
9158
  }
@@ -8406,10 +9189,26 @@ function registerCommentsTools(server) {
8406
9189
  "Delete a YouTube comment. Only works for comments owned by the authenticated channel.",
8407
9190
  {
8408
9191
  comment_id: z11.string().describe("The comment ID to delete."),
9192
+ connected_account_id: z11.string().uuid().optional().describe(
9193
+ "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."
9194
+ ),
9195
+ project_id: PROJECT_ID_SCHEMA,
8409
9196
  response_format: z11.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
8410
9197
  },
8411
- async ({ comment_id, response_format }) => {
9198
+ async ({
9199
+ comment_id,
9200
+ connected_account_id,
9201
+ project_id,
9202
+ response_format
9203
+ }) => {
8412
9204
  const format = response_format ?? "text";
9205
+ const route = await exactYouTubeRoute(project_id, connected_account_id);
9206
+ if ("error" in route) {
9207
+ return {
9208
+ content: [{ type: "text", text: route.error }],
9209
+ isError: true
9210
+ };
9211
+ }
8413
9212
  const userId = await getDefaultUserId();
8414
9213
  const rateLimit = checkRateLimit("posting", `delete_comment:${userId}`);
8415
9214
  if (!rateLimit.allowed) {
@@ -8425,11 +9224,16 @@ function registerCommentsTools(server) {
8425
9224
  }
8426
9225
  const { error } = await callEdgeFunction("youtube-comments", {
8427
9226
  action: "delete",
8428
- commentId: comment_id
9227
+ commentId: comment_id,
9228
+ projectId: route.projectId,
9229
+ project_id: route.projectId,
9230
+ connectedAccountId: route.connectedAccountId
8429
9231
  });
8430
9232
  if (error) {
8431
9233
  return {
8432
- content: [{ type: "text", text: `Error deleting comment: ${error}` }],
9234
+ content: [
9235
+ { type: "text", text: `Error deleting comment: ${error}` }
9236
+ ],
8433
9237
  isError: true
8434
9238
  };
8435
9239
  }
@@ -8438,13 +9242,22 @@ function registerCommentsTools(server) {
8438
9242
  content: [
8439
9243
  {
8440
9244
  type: "text",
8441
- text: JSON.stringify(asEnvelope8({ success: true, commentId: comment_id }), null, 2)
9245
+ text: JSON.stringify(
9246
+ asEnvelope8({ success: true, commentId: comment_id }),
9247
+ null,
9248
+ 2
9249
+ )
8442
9250
  }
8443
9251
  ]
8444
9252
  };
8445
9253
  }
8446
9254
  return {
8447
- content: [{ type: "text", text: `Comment ${comment_id} deleted successfully.` }]
9255
+ content: [
9256
+ {
9257
+ type: "text",
9258
+ text: `Comment ${comment_id} deleted successfully.`
9259
+ }
9260
+ ]
8448
9261
  };
8449
9262
  }
8450
9263
  );
@@ -8452,6 +9265,7 @@ function registerCommentsTools(server) {
8452
9265
 
8453
9266
  // src/tools/ideation-context.ts
8454
9267
  init_supabase();
9268
+ init_edge_function();
8455
9269
  import { z as z12 } from "zod";
8456
9270
  function asEnvelope9(data) {
8457
9271
  return {
@@ -8524,6 +9338,7 @@ function registerIdeationContextTools(server) {
8524
9338
  }
8525
9339
 
8526
9340
  // src/tools/credits.ts
9341
+ init_edge_function();
8527
9342
  import { z as z13 } from "zod";
8528
9343
  function asEnvelope10(data) {
8529
9344
  return {
@@ -8619,6 +9434,7 @@ Assets remaining: ${payload.remainingAssets ?? "unlimited"}`
8619
9434
 
8620
9435
  // src/tools/loop-summary.ts
8621
9436
  init_supabase();
9437
+ init_edge_function();
8622
9438
  import { z as z14 } from "zod";
8623
9439
  function asEnvelope11(data) {
8624
9440
  return {
@@ -8692,6 +9508,7 @@ Next Action: ${payload.recommendedNextAction}`
8692
9508
  }
8693
9509
 
8694
9510
  // src/tools/usage.ts
9511
+ init_edge_function();
8695
9512
  import { z as z15 } from "zod";
8696
9513
  function asEnvelope12(data) {
8697
9514
  return {
@@ -8764,6 +9581,7 @@ ${"=".repeat(40)}
8764
9581
  }
8765
9582
 
8766
9583
  // src/tools/autopilot.ts
9584
+ init_edge_function();
8767
9585
  import { z as z16 } from "zod";
8768
9586
  function asEnvelope13(data) {
8769
9587
  return {
@@ -9062,6 +9880,7 @@ Active: ${is_active}`
9062
9880
  }
9063
9881
 
9064
9882
  // src/tools/recipes.ts
9883
+ init_edge_function();
9065
9884
  import { z as z17 } from "zod";
9066
9885
  function asEnvelope14(data) {
9067
9886
  return {
@@ -9331,6 +10150,7 @@ ${JSON.stringify(run.outputs, null, 2)}
9331
10150
  import { z as z18 } from "zod";
9332
10151
 
9333
10152
  // src/lib/urlExtraction.ts
10153
+ init_edge_function();
9334
10154
  function classifyYouTubeUrl(url) {
9335
10155
  if (/youtube\.com\/watch|youtu\.be\//.test(url)) return "video";
9336
10156
  if (/youtube\.com\/@/.test(url)) return "channel";
@@ -10194,6 +11014,7 @@ function registerVisualQualityTools(server) {
10194
11014
  }
10195
11015
 
10196
11016
  // src/tools/planning.ts
11017
+ init_edge_function();
10197
11018
  import { z as z21 } from "zod";
10198
11019
  import { randomUUID as randomUUID2 } from "node:crypto";
10199
11020
  init_supabase();
@@ -10835,8 +11656,9 @@ ${rawText.slice(0, 1e3)}`
10835
11656
  }
10836
11657
 
10837
11658
  // src/tools/plan-approvals.ts
10838
- import { z as z22 } from "zod";
11659
+ init_edge_function();
10839
11660
  init_supabase();
11661
+ import { z as z22 } from "zod";
10840
11662
  function asEnvelope19(data) {
10841
11663
  return {
10842
11664
  _meta: {
@@ -11332,11 +12154,15 @@ function registerDiscoveryTools(server) {
11332
12154
  }
11333
12155
 
11334
12156
  // src/tools/pipeline.ts
12157
+ init_edge_function();
11335
12158
  import { z as z24 } from "zod";
11336
12159
  import { randomUUID as randomUUID3 } from "node:crypto";
11337
12160
  init_supabase();
11338
12161
  function asEnvelope20(data) {
11339
- return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
12162
+ return {
12163
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
12164
+ data
12165
+ };
11340
12166
  }
11341
12167
  var PLATFORM_ENUM2 = z24.enum([
11342
12168
  "youtube",
@@ -11372,7 +12198,10 @@ function registerPipelineTools(server) {
11372
12198
  action: "pipeline-readiness",
11373
12199
  platforms,
11374
12200
  estimated_posts,
11375
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
12201
+ ...resolvedProjectId ? {
12202
+ projectId: resolvedProjectId,
12203
+ project_id: resolvedProjectId
12204
+ } : {}
11376
12205
  },
11377
12206
  { timeoutMs: 15e3 }
11378
12207
  );
@@ -11390,16 +12219,24 @@ function registerPipelineTools(server) {
11390
12219
  const blockers = [];
11391
12220
  const warnings = [];
11392
12221
  if (!isUnlimited && credits < estimatedCost) {
11393
- blockers.push(`Insufficient credits: ${credits} available, ~${estimatedCost} needed`);
12222
+ blockers.push(
12223
+ `Insufficient credits: ${credits} available, ~${estimatedCost} needed`
12224
+ );
11394
12225
  }
11395
12226
  if (missingPlatforms.length > 0) {
11396
- blockers.push(`Missing connected accounts: ${missingPlatforms.join(", ")}`);
12227
+ blockers.push(
12228
+ `Missing connected accounts: ${missingPlatforms.join(", ")}`
12229
+ );
11397
12230
  }
11398
12231
  if (!hasBrand) {
11399
- warnings.push("No brand profile found. Content will use generic voice.");
12232
+ warnings.push(
12233
+ "No brand profile found. Content will use generic voice."
12234
+ );
11400
12235
  }
11401
12236
  if (pendingApprovals > 0) {
11402
- warnings.push(`${pendingApprovals} pending approval(s) from previous runs.`);
12237
+ warnings.push(
12238
+ `${pendingApprovals} pending approval(s) from previous runs.`
12239
+ );
11403
12240
  }
11404
12241
  if (!insightsFresh) {
11405
12242
  warnings.push(
@@ -11414,7 +12251,10 @@ function registerPipelineTools(server) {
11414
12251
  estimated_cost: estimatedCost,
11415
12252
  sufficient: credits >= estimatedCost
11416
12253
  },
11417
- connected_accounts: { platforms: connectedPlatforms, missing: missingPlatforms },
12254
+ connected_accounts: {
12255
+ platforms: connectedPlatforms,
12256
+ missing: missingPlatforms
12257
+ },
11418
12258
  brand_profile: { exists: hasBrand },
11419
12259
  pending_approvals: { count: pendingApprovals },
11420
12260
  insights_available: {
@@ -11428,11 +12268,18 @@ function registerPipelineTools(server) {
11428
12268
  };
11429
12269
  if (format === "json") {
11430
12270
  return {
11431
- content: [{ type: "text", text: JSON.stringify(asEnvelope20(result), null, 2) }]
12271
+ content: [
12272
+ {
12273
+ type: "text",
12274
+ text: JSON.stringify(asEnvelope20(result), null, 2)
12275
+ }
12276
+ ]
11432
12277
  };
11433
12278
  }
11434
12279
  const lines = [];
11435
- lines.push(`Pipeline Readiness: ${result.ready ? "READY" : "NOT READY"}`);
12280
+ lines.push(
12281
+ `Pipeline Readiness: ${result.ready ? "READY" : "NOT READY"}`
12282
+ );
11436
12283
  lines.push("=".repeat(40));
11437
12284
  lines.push(
11438
12285
  `Credits: ${credits} available, ~${estimatedCost} needed \u2014 ${credits >= estimatedCost ? "OK" : "BLOCKED"}`
@@ -11440,7 +12287,9 @@ function registerPipelineTools(server) {
11440
12287
  lines.push(
11441
12288
  `Accounts: ${connectedPlatforms.length} connected${missingPlatforms.length > 0 ? ` (missing: ${missingPlatforms.join(", ")})` : " \u2014 OK"}`
11442
12289
  );
11443
- lines.push(`Brand: ${hasBrand ? "OK" : "Missing (will use generic voice)"}`);
12290
+ lines.push(
12291
+ `Brand: ${hasBrand ? "OK" : "Missing (will use generic voice)"}`
12292
+ );
11444
12293
  lines.push(`Pending Approvals: ${pendingApprovals}`);
11445
12294
  lines.push(
11446
12295
  `Insights: ${insightsFresh ? "Fresh" : insightAge === null ? "None available" : `${insightAge} days old`}`
@@ -11459,7 +12308,12 @@ function registerPipelineTools(server) {
11459
12308
  } catch (err) {
11460
12309
  const message = sanitizeError(err);
11461
12310
  return {
11462
- content: [{ type: "text", text: `Readiness check failed: ${message}` }],
12311
+ content: [
12312
+ {
12313
+ type: "text",
12314
+ text: `Readiness check failed: ${message}`
12315
+ }
12316
+ ],
11463
12317
  isError: true
11464
12318
  };
11465
12319
  }
@@ -11473,6 +12327,9 @@ function registerPipelineTools(server) {
11473
12327
  topic: z24.string().optional().describe("Content topic (required if no source_url)"),
11474
12328
  source_url: z24.string().optional().describe("URL to extract content from"),
11475
12329
  platforms: z24.array(PLATFORM_ENUM2).min(1).describe("Target platforms"),
12330
+ account_ids: z24.record(z24.string(), z24.string().uuid()).optional().describe(
12331
+ "Exact connected-account ID per target platform. Required when a project has multiple accounts on one platform."
12332
+ ),
11476
12333
  days: z24.number().min(1).max(7).default(5).describe("Days to plan"),
11477
12334
  posts_per_day: z24.number().min(1).max(3).default(1).describe("Posts per platform per day"),
11478
12335
  approval_mode: z24.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe(
@@ -11494,6 +12351,7 @@ function registerPipelineTools(server) {
11494
12351
  topic,
11495
12352
  source_url,
11496
12353
  platforms,
12354
+ account_ids,
11497
12355
  days,
11498
12356
  posts_per_day,
11499
12357
  approval_mode,
@@ -11511,7 +12369,12 @@ function registerPipelineTools(server) {
11511
12369
  let creditsUsed = 0;
11512
12370
  if (!topic && !source_url) {
11513
12371
  return {
11514
- content: [{ type: "text", text: "Either topic or source_url is required." }],
12372
+ content: [
12373
+ {
12374
+ type: "text",
12375
+ text: "Either topic or source_url is required."
12376
+ }
12377
+ ],
11515
12378
  isError: true
11516
12379
  };
11517
12380
  }
@@ -11541,6 +12404,35 @@ function registerPipelineTools(server) {
11541
12404
  }
11542
12405
  try {
11543
12406
  const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
12407
+ if (schedulingRequested && !resolvedProjectId) {
12408
+ return {
12409
+ content: [
12410
+ {
12411
+ type: "text",
12412
+ text: "A project_id is required to schedule pipeline output. Configure an explicit project or use an API key scoped to exactly one project."
12413
+ }
12414
+ ],
12415
+ isError: true
12416
+ };
12417
+ }
12418
+ if (schedulingRequested) {
12419
+ const preflightRouting = await resolveConnectedAccountRouting({
12420
+ projectId: resolvedProjectId,
12421
+ platforms,
12422
+ requestedAccountIds: account_ids
12423
+ });
12424
+ if (preflightRouting.error || !preflightRouting.connectedAccountIds) {
12425
+ return {
12426
+ content: [
12427
+ {
12428
+ type: "text",
12429
+ text: `Cannot run publishing pipeline \u2014 ${preflightRouting.error ?? "exact connected-account routing could not be established."} (checked before any credits were spent.)`
12430
+ }
12431
+ ],
12432
+ isError: true
12433
+ };
12434
+ }
12435
+ }
11544
12436
  const estimatedCost = BASE_PLAN_CREDITS + (source_url ? SOURCE_EXTRACTION_CREDITS : 0);
11545
12437
  const { data: budgetData } = await callEdgeFunction(
11546
12438
  "mcp-data",
@@ -11594,7 +12486,13 @@ function registerPipelineTools(server) {
11594
12486
  "social-neuron-ai",
11595
12487
  {
11596
12488
  type: "generation",
11597
- prompt: buildPlanPrompt(resolvedTopic, platforms, days, posts_per_day, source_url),
12489
+ prompt: buildPlanPrompt(
12490
+ resolvedTopic,
12491
+ platforms,
12492
+ days,
12493
+ posts_per_day,
12494
+ source_url
12495
+ ),
11598
12496
  model: "gemini-2.5-flash",
11599
12497
  responseFormat: "json",
11600
12498
  ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
@@ -11602,7 +12500,10 @@ function registerPipelineTools(server) {
11602
12500
  { timeoutMs: 6e4 }
11603
12501
  );
11604
12502
  if (planError || !planData) {
11605
- errors.push({ stage: "planning", message: planError ?? "No AI response" });
12503
+ errors.push({
12504
+ stage: "planning",
12505
+ message: planError ?? "No AI response"
12506
+ });
11606
12507
  await callEdgeFunction(
11607
12508
  "mcp-data",
11608
12509
  {
@@ -11619,7 +12520,10 @@ function registerPipelineTools(server) {
11619
12520
  );
11620
12521
  return {
11621
12522
  content: [
11622
- { type: "text", text: `Planning failed: ${planError ?? "No AI response"}` }
12523
+ {
12524
+ type: "text",
12525
+ text: `Planning failed: ${planError ?? "No AI response"}`
12526
+ }
11623
12527
  ],
11624
12528
  isError: true
11625
12529
  };
@@ -11649,20 +12553,22 @@ function registerPipelineTools(server) {
11649
12553
  const postsArray = extractJsonArray(rawText);
11650
12554
  const requestedPlatformSet = new Set(platforms);
11651
12555
  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
- }));
12556
+ const parsedPosts = (postsArray ?? []).map(
12557
+ (p) => ({
12558
+ id: String(p.id ?? randomUUID3().slice(0, 8)),
12559
+ day: Number(p.day ?? 1),
12560
+ date: String(p.date ?? ""),
12561
+ platform: String(p.platform ?? ""),
12562
+ content_type: p.content_type ?? "caption",
12563
+ caption: String(p.caption ?? ""),
12564
+ title: p.title ? String(p.title) : void 0,
12565
+ hashtags: Array.isArray(p.hashtags) ? p.hashtags.map(String) : void 0,
12566
+ hook: String(p.hook ?? ""),
12567
+ angle: String(p.angle ?? ""),
12568
+ visual_direction: p.visual_direction ? String(p.visual_direction) : void 0,
12569
+ media_type: p.media_type ? String(p.media_type) : void 0
12570
+ })
12571
+ );
11666
12572
  const platformFilteredPosts = parsedPosts.filter(
11667
12573
  (post) => requestedPlatformSet.has(post.platform)
11668
12574
  );
@@ -11745,7 +12651,10 @@ function registerPipelineTools(server) {
11745
12651
  estimated_credits: estimatedCost,
11746
12652
  generated_at: (/* @__PURE__ */ new Date()).toISOString()
11747
12653
  },
11748
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
12654
+ ...resolvedProjectId ? {
12655
+ projectId: resolvedProjectId,
12656
+ project_id: resolvedProjectId
12657
+ } : {}
11749
12658
  },
11750
12659
  { timeoutMs: 1e4 }
11751
12660
  );
@@ -11798,6 +12707,22 @@ function registerPipelineTools(server) {
11798
12707
  }
11799
12708
  }
11800
12709
  let postsScheduled = 0;
12710
+ const pipelineRouting = schedulingRequested && postsApproved > 0 ? await resolveConnectedAccountRouting({
12711
+ projectId: resolvedProjectId,
12712
+ platforms,
12713
+ requestedAccountIds: account_ids
12714
+ }) : void 0;
12715
+ if (pipelineRouting?.error || schedulingRequested && postsApproved > 0 && !pipelineRouting?.connectedAccountIds) {
12716
+ return {
12717
+ content: [
12718
+ {
12719
+ type: "text",
12720
+ text: `Cannot run publishing pipeline \u2014 ${pipelineRouting?.error ?? "exact connected-account routing could not be established."}`
12721
+ }
12722
+ ],
12723
+ isError: true
12724
+ };
12725
+ }
11801
12726
  if (!dry_run && !skipSet.has("schedule") && postsApproved > 0) {
11802
12727
  const approvedPosts = posts.filter((p) => p.status === "approved");
11803
12728
  const scheduleBase = /* @__PURE__ */ new Date();
@@ -11805,10 +12730,27 @@ function registerPipelineTools(server) {
11805
12730
  const scheduleBaseMs = scheduleBase.getTime();
11806
12731
  for (const post of approvedPosts) {
11807
12732
  if (creditsUsed >= creditLimit) {
11808
- errors.push({ stage: "schedule", message: "Credit limit reached" });
12733
+ errors.push({
12734
+ stage: "schedule",
12735
+ message: "Credit limit reached"
12736
+ });
11809
12737
  break;
11810
12738
  }
11811
- const scheduledAt = post.schedule_at ?? new Date(scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5).toISOString();
12739
+ const scheduledAt = post.schedule_at ?? new Date(
12740
+ scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5
12741
+ ).toISOString();
12742
+ const route = Object.entries(
12743
+ pipelineRouting.connectedAccountIds
12744
+ ).find(
12745
+ ([platform2]) => platform2.toLowerCase() === post.platform.toLowerCase()
12746
+ );
12747
+ if (!route) {
12748
+ errors.push({
12749
+ stage: "schedule",
12750
+ message: `No verified connected account route for ${post.platform}`
12751
+ });
12752
+ continue;
12753
+ }
11812
12754
  try {
11813
12755
  const { error: schedError } = await callEdgeFunction(
11814
12756
  "schedule-post",
@@ -11821,7 +12763,11 @@ function registerPipelineTools(server) {
11821
12763
  scheduledAt,
11822
12764
  planId,
11823
12765
  idempotencyKey: `pipeline-${planId}-${post.id}`,
11824
- ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
12766
+ ...resolvedProjectId ? {
12767
+ projectId: resolvedProjectId,
12768
+ project_id: resolvedProjectId
12769
+ } : {},
12770
+ connectedAccountIds: { [route[0]]: route[1] }
11825
12771
  },
11826
12772
  { timeoutMs: 15e3 }
11827
12773
  );
@@ -11887,12 +12833,17 @@ function registerPipelineTools(server) {
11887
12833
  if (response_format === "json") {
11888
12834
  return {
11889
12835
  content: [
11890
- { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
12836
+ {
12837
+ type: "text",
12838
+ text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
12839
+ }
11891
12840
  ]
11892
12841
  };
11893
12842
  }
11894
12843
  const lines = [];
11895
- lines.push(`Pipeline ${pipelineId.slice(0, 8)}... ${finalStatus.toUpperCase()}`);
12844
+ lines.push(
12845
+ `Pipeline ${pipelineId.slice(0, 8)}... ${finalStatus.toUpperCase()}`
12846
+ );
11896
12847
  lines.push("=".repeat(40));
11897
12848
  lines.push(`Posts generated: ${posts.length}`);
11898
12849
  lines.push(`Posts approved: ${postsApproved}`);
@@ -11931,7 +12882,9 @@ function registerPipelineTools(server) {
11931
12882
  } catch {
11932
12883
  }
11933
12884
  return {
11934
- content: [{ type: "text", text: `Pipeline failed: ${message}` }],
12885
+ content: [
12886
+ { type: "text", text: `Pipeline failed: ${message}` }
12887
+ ],
11935
12888
  isError: true
11936
12889
  };
11937
12890
  }
@@ -11968,7 +12921,12 @@ function registerPipelineTools(server) {
11968
12921
  }
11969
12922
  if (format === "json") {
11970
12923
  return {
11971
- content: [{ type: "text", text: JSON.stringify(asEnvelope20(data), null, 2) }]
12924
+ content: [
12925
+ {
12926
+ type: "text",
12927
+ text: JSON.stringify(asEnvelope20(data), null, 2)
12928
+ }
12929
+ ]
11972
12930
  };
11973
12931
  }
11974
12932
  const lines = [];
@@ -12008,10 +12966,19 @@ function registerPipelineTools(server) {
12008
12966
  },
12009
12967
  async ({ plan_id, quality_threshold, response_format }) => {
12010
12968
  try {
12011
- const { data: loadResult, error: loadError } = await callEdgeFunction("mcp-data", { action: "auto-approve-plan", plan_id }, { timeoutMs: 1e4 });
12969
+ const { data: loadResult, error: loadError } = await callEdgeFunction(
12970
+ "mcp-data",
12971
+ { action: "auto-approve-plan", plan_id },
12972
+ { timeoutMs: 1e4 }
12973
+ );
12012
12974
  if (loadError) {
12013
12975
  return {
12014
- content: [{ type: "text", text: `Failed to load plan: ${loadError}` }],
12976
+ content: [
12977
+ {
12978
+ type: "text",
12979
+ text: `Failed to load plan: ${loadError}`
12980
+ }
12981
+ ],
12015
12982
  isError: true
12016
12983
  };
12017
12984
  }
@@ -12019,7 +12986,10 @@ function registerPipelineTools(server) {
12019
12986
  if (!stored?.plan_payload) {
12020
12987
  return {
12021
12988
  content: [
12022
- { type: "text", text: `No content plan found for plan_id=${plan_id}` }
12989
+ {
12990
+ type: "text",
12991
+ text: `No content plan found for plan_id=${plan_id}`
12992
+ }
12023
12993
  ],
12024
12994
  isError: true
12025
12995
  };
@@ -12046,7 +13016,11 @@ function registerPipelineTools(server) {
12046
13016
  blockers: []
12047
13017
  };
12048
13018
  autoApproved++;
12049
- details.push({ post_id: post.id, action: "approved", score: quality.total });
13019
+ details.push({
13020
+ post_id: post.id,
13021
+ action: "approved",
13022
+ score: quality.total
13023
+ });
12050
13024
  } else if (quality.total >= quality_threshold - 5) {
12051
13025
  post.status = "needs_edit";
12052
13026
  post.quality = {
@@ -12056,7 +13030,11 @@ function registerPipelineTools(server) {
12056
13030
  blockers: quality.blockers
12057
13031
  };
12058
13032
  flagged++;
12059
- details.push({ post_id: post.id, action: "flagged", score: quality.total });
13033
+ details.push({
13034
+ post_id: post.id,
13035
+ action: "flagged",
13036
+ score: quality.total
13037
+ });
12060
13038
  } else {
12061
13039
  post.status = "rejected";
12062
13040
  post.quality = {
@@ -12066,7 +13044,11 @@ function registerPipelineTools(server) {
12066
13044
  blockers: quality.blockers
12067
13045
  };
12068
13046
  rejected++;
12069
- details.push({ post_id: post.id, action: "rejected", score: quality.total });
13047
+ details.push({
13048
+ post_id: post.id,
13049
+ action: "rejected",
13050
+ score: quality.total
13051
+ });
12070
13052
  }
12071
13053
  }
12072
13054
  const newStatus = flagged === 0 && rejected === 0 ? "approved" : "in_review";
@@ -12102,7 +13084,10 @@ function registerPipelineTools(server) {
12102
13084
  if (response_format === "json") {
12103
13085
  return {
12104
13086
  content: [
12105
- { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
13087
+ {
13088
+ type: "text",
13089
+ text: JSON.stringify(asEnvelope20(resultPayload), null, 2)
13090
+ }
12106
13091
  ]
12107
13092
  };
12108
13093
  }
@@ -12123,7 +13108,9 @@ function registerPipelineTools(server) {
12123
13108
  } catch (err) {
12124
13109
  const message = sanitizeError(err);
12125
13110
  return {
12126
- content: [{ type: "text", text: `Auto-approve failed: ${message}` }],
13111
+ content: [
13112
+ { type: "text", text: `Auto-approve failed: ${message}` }
13113
+ ],
12127
13114
  isError: true
12128
13115
  };
12129
13116
  }
@@ -12149,6 +13136,7 @@ function buildPlanPrompt(topic, platforms, days, postsPerDay, sourceUrl) {
12149
13136
  }
12150
13137
 
12151
13138
  // src/tools/suggest.ts
13139
+ init_edge_function();
12152
13140
  import { z as z25 } from "zod";
12153
13141
  function asEnvelope21(data) {
12154
13142
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
@@ -12290,6 +13278,7 @@ ${i + 1}. ${s.topic}`);
12290
13278
  }
12291
13279
 
12292
13280
  // src/tools/digest.ts
13281
+ init_edge_function();
12293
13282
  import { z as z26 } from "zod";
12294
13283
 
12295
13284
  // src/lib/anomaly-detector.ts
@@ -12695,8 +13684,9 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
12695
13684
  }
12696
13685
 
12697
13686
  // src/tools/brandRuntime.ts
12698
- import { z as z27 } from "zod";
13687
+ init_edge_function();
12699
13688
  init_supabase();
13689
+ import { z as z27 } from "zod";
12700
13690
 
12701
13691
  // src/lib/brandScoring.ts
12702
13692
  var WEIGHTS = {
@@ -13553,6 +14543,7 @@ function registerBrandRuntimeTools(server) {
13553
14543
  }
13554
14544
 
13555
14545
  // src/tools/carousel.ts
14546
+ init_edge_function();
13556
14547
  import { z as z28 } from "zod";
13557
14548
  init_supabase();
13558
14549
  var IMAGE_CREDIT_ESTIMATES2 = {
@@ -13909,6 +14900,7 @@ function registerCarouselTools(server) {
13909
14900
  }
13910
14901
 
13911
14902
  // src/tools/niche-research.ts
14903
+ init_edge_function();
13912
14904
  import { z as z29 } from "zod";
13913
14905
  function asEnvelope24(data) {
13914
14906
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
@@ -13995,6 +14987,7 @@ function registerNicheResearchTools(server) {
13995
14987
  // src/tools/hyperframes.ts
13996
14988
  import { z as z30 } from "zod";
13997
14989
  init_supabase();
14990
+ init_edge_function();
13998
14991
  var HYPERFRAMES_BLOCKS = [
13999
14992
  // Transitions
14000
14993
  {
@@ -14249,6 +15242,9 @@ function registerHyperframesTools(server) {
14249
15242
  }
14250
15243
 
14251
15244
  // src/apps/content-calendar.ts
15245
+ init_edge_function();
15246
+ init_request_context();
15247
+ init_supabase();
14252
15248
  import {
14253
15249
  registerAppTool,
14254
15250
  registerAppResource,
@@ -14258,8 +15254,6 @@ import { z as z31 } from "zod";
14258
15254
  import fs from "node:fs/promises";
14259
15255
  import path from "node:path";
14260
15256
  import { fileURLToPath } from "node:url";
14261
- init_request_context();
14262
- init_supabase();
14263
15257
  var CALENDAR_URI = "ui://content-calendar/v1/mcp-app.html";
14264
15258
  var CALENDAR_CSP = {
14265
15259
  // The HTML is fully self-contained. Tool calls travel over the host bridge,
@@ -14470,6 +15464,8 @@ function registerContentCalendarApp(server) {
14470
15464
  }
14471
15465
 
14472
15466
  // src/apps/analytics-pulse.ts
15467
+ init_edge_function();
15468
+ init_supabase();
14473
15469
  import {
14474
15470
  registerAppResource as registerAppResource2,
14475
15471
  registerAppTool as registerAppTool2,
@@ -14479,7 +15475,6 @@ import { z as z32 } from "zod";
14479
15475
  import fs2 from "node:fs/promises";
14480
15476
  import path2 from "node:path";
14481
15477
  import { fileURLToPath as fileURLToPath2 } from "node:url";
14482
- init_supabase();
14483
15478
  var ANALYTICS_URI = "ui://analytics-pulse/v1/mcp-app.html";
14484
15479
  var ANALYTICS_CSP = {
14485
15480
  connectDomains: [],
@@ -14712,6 +15707,7 @@ function registerAnalyticsPulseApp(server) {
14712
15707
  }
14713
15708
 
14714
15709
  // src/tools/connections.ts
15710
+ init_edge_function();
14715
15711
  import { z as z33 } from "zod";
14716
15712
  init_supabase();
14717
15713
  var PLATFORM_ENUM4 = [
@@ -14726,12 +15722,12 @@ var PLATFORM_ENUM4 = [
14726
15722
  "shopify",
14727
15723
  "etsy"
14728
15724
  ];
14729
- function findActiveAccount(accounts, platform2) {
15725
+ function findActiveAccounts(accounts, platform2, projectId) {
14730
15726
  const target = platform2.toLowerCase();
14731
- return accounts.find((a) => {
15727
+ return accounts.filter((a) => {
14732
15728
  const effectiveStatus = a.effective_status || a.status;
14733
- return a.platform.toLowerCase() === target && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
14734
- }) ?? null;
15729
+ return a.platform.toLowerCase() === target && a.project_id === projectId && (effectiveStatus === "active" || effectiveStatus === "expires_soon");
15730
+ });
14735
15731
  }
14736
15732
  var MAX_CONCURRENT_WAITS_PER_USER = 3;
14737
15733
  var inFlightWaitsByUser = /* @__PURE__ */ new Map();
@@ -14740,16 +15736,21 @@ function registerConnectionTools(server) {
14740
15736
  "start_platform_connection",
14741
15737
  "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
15738
  {
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."
15739
+ platform: z33.enum(PLATFORM_ENUM4).describe(
15740
+ "Platform to connect. Lower-case: instagram, tiktok, youtube, etc."
15741
+ ),
15742
+ project_id: z33.string().uuid().optional().describe(
15743
+ "Brand/project ID to bind the new social account to. Required when the account has multiple brands."
14746
15744
  ),
14747
15745
  response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
14748
15746
  },
14749
15747
  async ({ platform: platform2, project_id, response_format }) => {
14750
15748
  const format = response_format ?? "text";
14751
15749
  const userId = await getDefaultUserId();
14752
- const rl = checkRateLimit("posting", `start_platform_connection:${userId}`);
15750
+ const rl = checkRateLimit(
15751
+ "posting",
15752
+ `start_platform_connection:${userId}`
15753
+ );
14753
15754
  if (!rl.allowed) {
14754
15755
  return {
14755
15756
  content: [
@@ -14761,12 +15762,27 @@ function registerConnectionTools(server) {
14761
15762
  isError: true
14762
15763
  };
14763
15764
  }
15765
+ const projectResolution = await resolveProjectStrict(project_id);
15766
+ if (!projectResolution.projectId) {
15767
+ return {
15768
+ content: [
15769
+ {
15770
+ type: "text",
15771
+ 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."
15772
+ }
15773
+ ],
15774
+ isError: true
15775
+ };
15776
+ }
15777
+ const resolvedProjectId = projectResolution.projectId;
15778
+ const projectAutoResolvedNote = projectResolution.autoResolvedNote;
14764
15779
  const { data, error } = await callEdgeFunction(
14765
15780
  "mcp-data",
14766
15781
  {
14767
15782
  action: "mint-connection-nonce",
14768
15783
  platform: platform2,
14769
- ...project_id ? { projectId: project_id, project_id } : {}
15784
+ projectId: resolvedProjectId,
15785
+ project_id: resolvedProjectId
14770
15786
  },
14771
15787
  { timeoutMs: 1e4 }
14772
15788
  );
@@ -14782,6 +15798,17 @@ function registerConnectionTools(server) {
14782
15798
  isError: true
14783
15799
  };
14784
15800
  }
15801
+ if (data.project_id !== resolvedProjectId) {
15802
+ return {
15803
+ content: [
15804
+ {
15805
+ type: "text",
15806
+ text: "Connection link project attestation failed. No OAuth link was returned."
15807
+ }
15808
+ ],
15809
+ isError: true
15810
+ };
15811
+ }
14785
15812
  if (format === "json") {
14786
15813
  return {
14787
15814
  content: [
@@ -14790,10 +15817,11 @@ function registerConnectionTools(server) {
14790
15817
  text: JSON.stringify(
14791
15818
  {
14792
15819
  platform: data.platform,
14793
- project_id: project_id ?? null,
15820
+ project_id: resolvedProjectId,
14794
15821
  deep_link: data.deep_link,
14795
15822
  expires_at: data.expires_at,
14796
- next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection."
15823
+ next_step: "Open deep_link in a browser, approve on the platform, then call wait_for_connection.",
15824
+ ...projectAutoResolvedNote ? { project_auto_resolved: projectAutoResolvedNote } : {}
14797
15825
  },
14798
15826
  null,
14799
15827
  2
@@ -14809,7 +15837,7 @@ function registerConnectionTools(server) {
14809
15837
  type: "text",
14810
15838
  text: [
14811
15839
  `${data.platform} connection ready.`,
14812
- ...project_id ? [`Brand/project: ${project_id}`] : [],
15840
+ `Brand/project: ${resolvedProjectId}`,
14813
15841
  "",
14814
15842
  'Ask the user to open this link in a browser and click "Connect" on the platform:',
14815
15843
  ` ${data.deep_link}`,
@@ -14817,7 +15845,8 @@ function registerConnectionTools(server) {
14817
15845
  `Link expires at: ${data.expires_at} (~2 minutes).`,
14818
15846
  "",
14819
15847
  "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."
15848
+ "This is a one-time browser setup \u2014 not another OAuth flow inside Claude.",
15849
+ ...projectAutoResolvedNote ? ["", `Note: ${projectAutoResolvedNote}`] : []
14821
15850
  ].join("\n")
14822
15851
  }
14823
15852
  ],
@@ -14830,14 +15859,20 @@ function registerConnectionTools(server) {
14830
15859
  "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
15860
  {
14832
15861
  platform: z33.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
14833
- project_id: z33.string().optional().describe(
15862
+ project_id: z33.string().uuid().optional().describe(
14834
15863
  "Brand/project ID to scope the connection poll. Use the same project_id passed to start_platform_connection."
14835
15864
  ),
14836
15865
  timeout_s: z33.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
14837
15866
  poll_interval_s: z33.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
14838
15867
  response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
14839
15868
  },
14840
- async ({ platform: platform2, project_id, timeout_s, poll_interval_s, response_format }) => {
15869
+ async ({
15870
+ platform: platform2,
15871
+ project_id,
15872
+ timeout_s,
15873
+ poll_interval_s,
15874
+ response_format
15875
+ }) => {
14841
15876
  const format = response_format ?? "text";
14842
15877
  const startedAt = Date.now();
14843
15878
  const timeoutMs = (timeout_s ?? 120) * 1e3;
@@ -14856,6 +15891,18 @@ function registerConnectionTools(server) {
14856
15891
  isError: true
14857
15892
  };
14858
15893
  }
15894
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
15895
+ if (!resolvedProjectId) {
15896
+ return {
15897
+ content: [
15898
+ {
15899
+ type: "text",
15900
+ text: "A project_id is required to wait for a connection. Use the same project_id used to start the OAuth flow."
15901
+ }
15902
+ ],
15903
+ isError: true
15904
+ };
15905
+ }
14859
15906
  const activeWaits = inFlightWaitsByUser.get(userId) ?? 0;
14860
15907
  if (activeWaits >= MAX_CONCURRENT_WAITS_PER_USER) {
14861
15908
  return {
@@ -14877,12 +15924,62 @@ function registerConnectionTools(server) {
14877
15924
  "mcp-data",
14878
15925
  {
14879
15926
  action: "connected-accounts",
14880
- ...project_id ? { projectId: project_id, project_id } : {}
15927
+ projectId: resolvedProjectId,
15928
+ project_id: resolvedProjectId
14881
15929
  },
14882
15930
  { timeoutMs: 1e4 }
14883
15931
  );
14884
15932
  if (!error && data?.success) {
14885
- const found = findActiveAccount(data.accounts ?? [], platform2);
15933
+ const foundAccounts = findActiveAccounts(
15934
+ data.accounts ?? [],
15935
+ platform2,
15936
+ resolvedProjectId
15937
+ );
15938
+ if (foundAccounts.length > 1) {
15939
+ if (format === "json") {
15940
+ return {
15941
+ content: [
15942
+ {
15943
+ type: "text",
15944
+ text: JSON.stringify(
15945
+ {
15946
+ connected: true,
15947
+ platform: platform2,
15948
+ project_id: resolvedProjectId,
15949
+ accounts: foundAccounts.map((a) => ({
15950
+ id: a.id,
15951
+ username: a.username,
15952
+ connected_at: a.created_at
15953
+ })),
15954
+ attempts,
15955
+ message: `${platform2} has ${foundAccounts.length} active accounts for this project. Pass the exact account_id to schedule_post.`
15956
+ },
15957
+ null,
15958
+ 2
15959
+ )
15960
+ }
15961
+ ],
15962
+ isError: false
15963
+ };
15964
+ }
15965
+ return {
15966
+ content: [
15967
+ {
15968
+ type: "text",
15969
+ text: [
15970
+ `${platform2} is connected \u2014 ${foundAccounts.length} active accounts found for project ${resolvedProjectId}:`,
15971
+ ...foundAccounts.map(
15972
+ (a) => ` ${a.username || "(unnamed)"} (id=${a.id})`
15973
+ ),
15974
+ "",
15975
+ "Call schedule_post with the exact account_id (or account_ids) for the one you mean."
15976
+ ].join("\n")
15977
+ }
15978
+ ],
15979
+ isError: false
15980
+ };
15981
+ }
15982
+ const found = foundAccounts[0];
14886
15983
  if (found) {
14887
15984
  if (format === "json") {
14888
15985
  return {
@@ -14893,7 +15990,7 @@ function registerConnectionTools(server) {
14893
15990
  {
14894
15991
  connected: true,
14895
15992
  platform: found.platform,
14896
- project_id: found.project_id ?? project_id ?? null,
15993
+ project_id: resolvedProjectId,
14897
15994
  account_id: found.id,
14898
15995
  username: found.username,
14899
15996
  connected_at: found.created_at,
@@ -14913,7 +16010,7 @@ function registerConnectionTools(server) {
14913
16010
  type: "text",
14914
16011
  text: [
14915
16012
  `${found.platform} is connected.`,
14916
- ...found.project_id || project_id ? [`Brand/project: ${found.project_id ?? project_id}`] : [],
16013
+ `Brand/project: ${resolvedProjectId}`,
14917
16014
  `Account: ${found.username || "(unnamed)"} (id=${found.id})`,
14918
16015
  `Detected after ${attempts} poll(s) in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s.`,
14919
16016
  "Ready to call `schedule_post`."
@@ -14926,7 +16023,9 @@ function registerConnectionTools(server) {
14926
16023
  }
14927
16024
  const remaining = deadline - Date.now();
14928
16025
  if (remaining <= 0) break;
14929
- await new Promise((resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining)));
16026
+ await new Promise(
16027
+ (resolve3) => setTimeout(resolve3, Math.min(intervalMs, remaining))
16028
+ );
14930
16029
  }
14931
16030
  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
16031
  if (format === "json") {
@@ -14938,7 +16037,7 @@ function registerConnectionTools(server) {
14938
16037
  {
14939
16038
  connected: false,
14940
16039
  platform: platform2,
14941
- project_id: project_id ?? null,
16040
+ project_id: resolvedProjectId,
14942
16041
  attempts,
14943
16042
  timed_out: true,
14944
16043
  message
@@ -14968,6 +16067,7 @@ function registerConnectionTools(server) {
14968
16067
  }
14969
16068
 
14970
16069
  // src/tools/harness.ts
16070
+ init_edge_function();
14971
16071
  import { z as z34 } from "zod";
14972
16072
  var ALLOWED_AGENTS = [
14973
16073
  "conductor",
@@ -15105,6 +16205,7 @@ function registerHarnessTools(server, _ctx) {
15105
16205
  }
15106
16206
 
15107
16207
  // src/tools/hermes.ts
16208
+ init_edge_function();
15108
16209
  import { z as z35 } from "zod";
15109
16210
  function asEnvelope25(data) {
15110
16211
  return {
@@ -15382,6 +16483,7 @@ ${"=".repeat(40)}
15382
16483
 
15383
16484
  // src/tools/skills.ts
15384
16485
  import { z as z36 } from "zod";
16486
+ init_edge_function();
15385
16487
 
15386
16488
  // src/lib/skills-manifest.ts
15387
16489
  var SKILLS_MANIFEST = [
@@ -15724,6 +16826,7 @@ ${skill.compiled_section}` : "";
15724
16826
  }
15725
16827
 
15726
16828
  // src/tools/loopPulse.ts
16829
+ init_edge_function();
15727
16830
  import { z as z37 } from "zod";
15728
16831
  function asEnvelope27(data) {
15729
16832
  return {
@@ -15779,8 +16882,9 @@ function registerLoopPulseTools(server) {
15779
16882
  }
15780
16883
 
15781
16884
  // src/tools/banditState.ts
15782
- import { z as z38 } from "zod";
16885
+ init_edge_function();
15783
16886
  init_supabase();
16887
+ import { z as z38 } from "zod";
15784
16888
  function asEnvelope28(data) {
15785
16889
  return {
15786
16890
  _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
@@ -15876,8 +16980,9 @@ function registerBanditStateTools(server) {
15876
16980
  }
15877
16981
 
15878
16982
  // src/tools/lifecycle.ts
15879
- import { z as z39 } from "zod";
16983
+ init_edge_function();
15880
16984
  init_supabase();
16985
+ import { z as z39 } from "zod";
15881
16986
  function envelope(data) {
15882
16987
  return {
15883
16988
  _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
@@ -16440,6 +17545,7 @@ Deliver a report covering:
16440
17545
  }
16441
17546
 
16442
17547
  // src/resources.ts
17548
+ init_edge_function();
16443
17549
  function registerResources(server) {
16444
17550
  server.resource(
16445
17551
  "brand-profile",
@@ -17341,6 +18447,7 @@ var MAX_METADATA_BYTES = 8192;
17341
18447
  var MAX_CACHED_CLIENTS = 1e3;
17342
18448
  var MAX_PERSISTED_CLIENTS = 5e3;
17343
18449
  var OAUTH_CLIENT_RETENTION_DAYS = 90;
18450
+ var OAUTH_CLIENT_TOUCH_INTERVAL_MS = 24 * 60 * 60 * 1e3;
17344
18451
  function byteLength(value) {
17345
18452
  return Buffer.byteLength(value, "utf8");
17346
18453
  }
@@ -17363,6 +18470,7 @@ function assertRegistrationWithinBounds(client3) {
17363
18470
  }
17364
18471
  }
17365
18472
  function cacheClient(cache, clientId, client3) {
18473
+ const evictedClientIds = [];
17366
18474
  if (cache.has(clientId)) {
17367
18475
  cache.delete(clientId);
17368
18476
  }
@@ -17371,7 +18479,9 @@ function cacheClient(cache, clientId, client3) {
17371
18479
  const oldestClientId = cache.keys().next().value;
17372
18480
  if (!oldestClientId) break;
17373
18481
  cache.delete(oldestClientId);
18482
+ evictedClientIds.push(oldestClientId);
17374
18483
  }
18484
+ return evictedClientIds;
17375
18485
  }
17376
18486
  function rowToClient(row) {
17377
18487
  return {
@@ -17406,6 +18516,7 @@ function clientToRow(c) {
17406
18516
  }
17407
18517
  function createClientsStore() {
17408
18518
  const cache = /* @__PURE__ */ new Map();
18519
+ const lastClientTouch = /* @__PURE__ */ new Map();
17409
18520
  let supabaseAvailable = true;
17410
18521
  function markUnavailable(reason) {
17411
18522
  if (supabaseAvailable) {
@@ -17415,14 +18526,50 @@ function createClientsStore() {
17415
18526
  supabaseAvailable = false;
17416
18527
  }
17417
18528
  }
18529
+ function clearEvictedClientTouches(evictedClientIds) {
18530
+ for (const evictedClientId of evictedClientIds) {
18531
+ lastClientTouch.delete(evictedClientId);
18532
+ }
18533
+ }
18534
+ function touchClientActivity(clientId) {
18535
+ if (!supabaseAvailable) return;
18536
+ const now = Date.now();
18537
+ const lastTouchedAt = lastClientTouch.get(clientId);
18538
+ if (lastTouchedAt !== void 0 && now - lastTouchedAt < OAUTH_CLIENT_TOUCH_INTERVAL_MS) {
18539
+ return;
18540
+ }
18541
+ lastClientTouch.set(clientId, now);
18542
+ const releaseTouchReservation = () => {
18543
+ if (lastClientTouch.get(clientId) === now) {
18544
+ lastClientTouch.delete(clientId);
18545
+ }
18546
+ };
18547
+ void (async () => {
18548
+ try {
18549
+ const supabase = getSupabaseClient();
18550
+ const { error: touchError } = await supabase.from("mcp_oauth_clients").update({ last_used_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("client_id", clientId);
18551
+ if (touchError) {
18552
+ releaseTouchReservation();
18553
+ console.error(
18554
+ `[oauth] failed to update client activity: ${sanitizeError(touchError)}`
18555
+ );
18556
+ }
18557
+ } catch (touchError) {
18558
+ releaseTouchReservation();
18559
+ console.error(`[oauth] failed to update client activity: ${sanitizeError(touchError)}`);
18560
+ }
18561
+ })();
18562
+ }
17418
18563
  return {
17419
18564
  async getClient(clientId) {
17420
18565
  const cached3 = cache.get(clientId);
17421
18566
  if (cached3) {
17422
18567
  if (!hasAllowedRedirectUris(cached3)) {
17423
18568
  cache.delete(clientId);
18569
+ lastClientTouch.delete(clientId);
17424
18570
  return void 0;
17425
18571
  }
18572
+ touchClientActivity(clientId);
17426
18573
  return cached3;
17427
18574
  }
17428
18575
  if (!supabaseAvailable) return void 0;
@@ -17444,8 +18591,8 @@ function createClientsStore() {
17444
18591
  }
17445
18592
  return void 0;
17446
18593
  }
17447
- cacheClient(cache, clientId, client3);
17448
- void supabase.from("mcp_oauth_clients").update({ last_used_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("client_id", clientId);
18594
+ clearEvictedClientTouches(cacheClient(cache, clientId, client3));
18595
+ touchClientActivity(clientId);
17449
18596
  return client3;
17450
18597
  } catch (err) {
17451
18598
  markUnavailable(err instanceof Error ? err.message : "unknown error");
@@ -17491,7 +18638,8 @@ function createClientsStore() {
17491
18638
  markUnavailable(err instanceof Error ? err.message : "unknown error");
17492
18639
  }
17493
18640
  }
17494
- cacheClient(cache, client3.client_id, client3);
18641
+ lastClientTouch.delete(client3.client_id);
18642
+ clearEvictedClientTouches(cacheClient(cache, client3.client_id, client3));
17495
18643
  return client3;
17496
18644
  }
17497
18645
  };