@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/CHANGELOG.md +27 -0
- package/dist/http.js +1710 -562
- package/dist/index.js +1621 -550
- package/dist/sn.js +1615 -544
- package/package.json +6 -3
- package/tools.lock.json +14 -14
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
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
234
|
-
} catch {
|
|
235
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
584
|
-
if (data?.
|
|
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(
|
|
618
|
-
|
|
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
|
|
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.
|
|
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(
|
|
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 ({
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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.
|
|
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(
|
|
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(
|
|
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,
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
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:
|
|
4852
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 ? {
|
|
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(
|
|
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
|
-
|
|
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
|
|
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({
|
|
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)
|
|
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({
|
|
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
|
-
{
|
|
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
|
|
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
|
|
5482
|
-
|
|
5483
|
-
|
|
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 =
|
|
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,
|
|
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 ({
|
|
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
|
|
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
|
-
|
|
6927
|
+
projectId: resolvedProjectId,
|
|
6928
|
+
project_id: resolvedProjectId
|
|
6425
6929
|
});
|
|
6426
6930
|
if (efError) {
|
|
6427
6931
|
return {
|
|
6428
|
-
content: [
|
|
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(
|
|
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
|
|
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
|
-
|
|
7057
|
+
projectId: resolvedProjectId,
|
|
7058
|
+
project_id: resolvedProjectId
|
|
6528
7059
|
});
|
|
6529
7060
|
if (error) {
|
|
6530
7061
|
return {
|
|
6531
|
-
content: [
|
|
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: [
|
|
7074
|
+
content: [
|
|
7075
|
+
{ type: "text", text: "Analytics refresh failed." }
|
|
7076
|
+
],
|
|
6539
7077
|
isError: true
|
|
6540
7078
|
};
|
|
6541
7079
|
}
|
|
6542
|
-
const queued = (result.results ?? []).filter(
|
|
6543
|
-
|
|
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({
|
|
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: [
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 ({
|
|
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
|
-
{
|
|
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: [
|
|
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({
|
|
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
|
-
{
|
|
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 = [
|
|
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: [
|
|
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 = [
|
|
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: [
|
|
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: [
|
|
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 ({
|
|
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: [
|
|
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({
|
|
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(
|
|
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 ({
|
|
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: [
|
|
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: [
|
|
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 ({
|
|
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: [
|
|
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: [
|
|
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 ({
|
|
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: [
|
|
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 ({
|
|
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: [
|
|
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(
|
|
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: [
|
|
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
|
-
|
|
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 {
|
|
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 ? {
|
|
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(
|
|
12222
|
+
blockers.push(
|
|
12223
|
+
`Insufficient credits: ${credits} available, ~${estimatedCost} needed`
|
|
12224
|
+
);
|
|
11394
12225
|
}
|
|
11395
12226
|
if (missingPlatforms.length > 0) {
|
|
11396
|
-
blockers.push(
|
|
12227
|
+
blockers.push(
|
|
12228
|
+
`Missing connected accounts: ${missingPlatforms.join(", ")}`
|
|
12229
|
+
);
|
|
11397
12230
|
}
|
|
11398
12231
|
if (!hasBrand) {
|
|
11399
|
-
warnings.push(
|
|
12232
|
+
warnings.push(
|
|
12233
|
+
"No brand profile found. Content will use generic voice."
|
|
12234
|
+
);
|
|
11400
12235
|
}
|
|
11401
12236
|
if (pendingApprovals > 0) {
|
|
11402
|
-
warnings.push(
|
|
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: {
|
|
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: [
|
|
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(
|
|
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(
|
|
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: [
|
|
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: [
|
|
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(
|
|
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({
|
|
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
|
-
{
|
|
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(
|
|
11653
|
-
|
|
11654
|
-
|
|
11655
|
-
|
|
11656
|
-
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
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 ? {
|
|
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({
|
|
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(
|
|
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 ? {
|
|
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
|
-
{
|
|
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(
|
|
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: [
|
|
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: [
|
|
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(
|
|
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: [
|
|
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
|
-
{
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
-
{
|
|
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: [
|
|
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
|
-
|
|
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
|
|
15725
|
+
function findActiveAccounts(accounts, platform2, projectId) {
|
|
14730
15726
|
const target = platform2.toLowerCase();
|
|
14731
|
-
return accounts.
|
|
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
|
-
})
|
|
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(
|
|
14744
|
-
|
|
14745
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 ({
|
|
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
|
-
|
|
15927
|
+
projectId: resolvedProjectId,
|
|
15928
|
+
project_id: resolvedProjectId
|
|
14881
15929
|
},
|
|
14882
15930
|
{ timeoutMs: 1e4 }
|
|
14883
15931
|
);
|
|
14884
15932
|
if (!error && data?.success) {
|
|
14885
|
-
const
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18641
|
+
lastClientTouch.delete(client3.client_id);
|
|
18642
|
+
clearEvictedClientTouches(cacheClient(cache, client3.client_id, client3));
|
|
17495
18643
|
return client3;
|
|
17496
18644
|
}
|
|
17497
18645
|
};
|