@socialneuron/mcp-server 1.7.12 → 1.7.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __esm = (fn, res) => function __init() {
5
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
4
+ var __esm = (fn, res, err) => function __init() {
5
+ if (err) throw err[0];
6
+ try {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ } catch (e) {
9
+ throw err = [e], e;
10
+ }
6
11
  };
7
12
  var __export = (target, all) => {
8
13
  for (var name in all)
@@ -14,54 +19,7 @@ var MCP_VERSION;
14
19
  var init_version = __esm({
15
20
  "src/lib/version.ts"() {
16
21
  "use strict";
17
- MCP_VERSION = "1.7.7";
18
- }
19
- });
20
-
21
- // src/lib/posthog.ts
22
- import { createHash } from "node:crypto";
23
- import { PostHog } from "posthog-node";
24
- function hashUserId(userId) {
25
- return createHash("sha256").update(`${POSTHOG_SALT}:${userId}`).digest("hex").substring(0, 32);
26
- }
27
- function initPostHog() {
28
- if (isTelemetryDisabled()) return;
29
- const key = process.env.POSTHOG_KEY || process.env.VITE_POSTHOG_KEY;
30
- const host = process.env.POSTHOG_HOST || process.env.VITE_POSTHOG_HOST || "https://eu.i.posthog.com";
31
- if (!key) return;
32
- client = new PostHog(key, { host, flushAt: 5, flushInterval: 1e4 });
33
- }
34
- async function captureToolEvent(args) {
35
- if (!client) return;
36
- let userId;
37
- try {
38
- userId = await getDefaultUserId();
39
- } catch {
40
- userId = "anonymous_mcp";
41
- }
42
- client.capture({
43
- distinctId: hashUserId(userId),
44
- event: `mcp_tool_${args.status}`,
45
- properties: {
46
- tool_name: args.toolName,
47
- duration_ms: args.durationMs,
48
- ...args.details
49
- }
50
- });
51
- }
52
- async function shutdownPostHog() {
53
- if (client) {
54
- await client.shutdown();
55
- client = null;
56
- }
57
- }
58
- var POSTHOG_SALT, client;
59
- var init_posthog = __esm({
60
- "src/lib/posthog.ts"() {
61
- "use strict";
62
- init_supabase();
63
- POSTHOG_SALT = "socialneuron-mcp-ph-v1";
64
- client = null;
22
+ MCP_VERSION = "1.7.15";
65
23
  }
66
24
  });
67
25
 
@@ -70,6 +28,9 @@ import { AsyncLocalStorage } from "node:async_hooks";
70
28
  function getRequestUserId() {
71
29
  return requestContext.getStore()?.userId ?? null;
72
30
  }
31
+ function getRequestScopes() {
32
+ return requestContext.getStore()?.scopes ?? null;
33
+ }
73
34
  function getRequestToken() {
74
35
  return requestContext.getStore()?.token ?? null;
75
36
  }
@@ -298,83 +259,12 @@ var init_credentials = __esm({
298
259
  }
299
260
  });
300
261
 
301
- // src/lib/validation-cache.ts
302
- var validation_cache_exports = {};
303
- __export(validation_cache_exports, {
304
- clearValidationCache: () => clearValidationCache,
305
- keyFingerprint: () => keyFingerprint,
306
- readValidationCache: () => readValidationCache,
307
- writeValidationCache: () => writeValidationCache
308
- });
309
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, chmodSync } from "node:fs";
310
- import { join as join2 } from "node:path";
311
- import { homedir as homedir2 } from "node:os";
312
- function keyFingerprint(apiKey) {
313
- return apiKey.substring(0, 6) + "..." + apiKey.slice(-4);
314
- }
315
- function readValidationCache(apiKey) {
316
- try {
317
- const raw = readFileSync2(CACHE_FILE, "utf-8");
318
- const cached = JSON.parse(raw);
319
- if (cached.keyFingerprint !== keyFingerprint(apiKey)) {
320
- return null;
321
- }
322
- if (Date.now() - cached.cachedAt > CACHE_TTL_MS) {
323
- return null;
324
- }
325
- if (!cached.result.valid) {
326
- return null;
327
- }
328
- if (cached.result.expiresAt) {
329
- const expiresMs = new Date(cached.result.expiresAt).getTime();
330
- if (expiresMs <= Date.now()) {
331
- return null;
332
- }
333
- }
334
- return cached.result;
335
- } catch {
336
- return null;
337
- }
338
- }
339
- function writeValidationCache(apiKey, result) {
340
- if (!result.valid) return;
341
- try {
342
- mkdirSync2(CONFIG_DIR2, { recursive: true });
343
- const entry = {
344
- keyFingerprint: keyFingerprint(apiKey),
345
- result,
346
- cachedAt: Date.now()
347
- };
348
- writeFileSync2(CACHE_FILE, JSON.stringify(entry, null, 2), { mode: 384 });
349
- try {
350
- chmodSync(CACHE_FILE, 384);
351
- } catch {
352
- }
353
- } catch {
354
- }
355
- }
356
- function clearValidationCache() {
357
- try {
358
- writeFileSync2(CACHE_FILE, "{}", { mode: 384 });
359
- } catch {
360
- }
361
- }
362
- var CONFIG_DIR2, CACHE_FILE, CACHE_TTL_MS;
363
- var init_validation_cache = __esm({
364
- "src/lib/validation-cache.ts"() {
365
- "use strict";
366
- CONFIG_DIR2 = join2(homedir2(), ".config", "socialneuron");
367
- CACHE_FILE = join2(CONFIG_DIR2, "validation-cache.json");
368
- CACHE_TTL_MS = 5 * 60 * 1e3;
369
- }
370
- });
371
-
372
262
  // src/auth/api-keys.ts
373
263
  var api_keys_exports = {};
374
264
  __export(api_keys_exports, {
375
265
  validateApiKey: () => validateApiKey
376
266
  });
377
- async function validateApiKey(apiKey) {
267
+ async function validateApiKey(apiKey, _attempt = 0) {
378
268
  const supabaseUrl = getSupabaseUrl();
379
269
  try {
380
270
  const anonKey = process.env.SUPABASE_ANON_KEY || process.env.SOCIALNEURON_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY || CLOUD_SUPABASE_ANON_KEY;
@@ -391,21 +281,90 @@ async function validateApiKey(apiKey) {
391
281
  );
392
282
  if (!response.ok) {
393
283
  const text = await response.text();
394
- return { valid: false, error: `Validation failed: ${text}` };
284
+ const retryable = response.status === 429 || response.status >= 500;
285
+ if (retryable && _attempt < VALIDATE_MAX_RETRIES) {
286
+ await sleep(300 * (_attempt + 1));
287
+ return validateApiKey(apiKey, _attempt + 1);
288
+ }
289
+ return {
290
+ valid: false,
291
+ retryable,
292
+ error: `Validation failed (HTTP ${response.status}): ${text}`
293
+ };
395
294
  }
396
- const result = await response.json();
397
- return result;
295
+ return await response.json();
398
296
  } catch (err) {
297
+ if (_attempt < VALIDATE_MAX_RETRIES) {
298
+ await sleep(300 * (_attempt + 1));
299
+ return validateApiKey(apiKey, _attempt + 1);
300
+ }
399
301
  return {
400
302
  valid: false,
303
+ retryable: true,
401
304
  error: err instanceof Error ? err.message : String(err)
402
305
  };
403
306
  }
404
307
  }
308
+ var VALIDATE_MAX_RETRIES, sleep;
405
309
  var init_api_keys = __esm({
406
310
  "src/auth/api-keys.ts"() {
407
311
  "use strict";
408
312
  init_supabase();
313
+ VALIDATE_MAX_RETRIES = 2;
314
+ sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
315
+ }
316
+ });
317
+
318
+ // src/lib/posthog.ts
319
+ var posthog_exports = {};
320
+ __export(posthog_exports, {
321
+ captureToolEvent: () => captureToolEvent,
322
+ initPostHog: () => initPostHog,
323
+ shutdownPostHog: () => shutdownPostHog
324
+ });
325
+ import { createHash } from "node:crypto";
326
+ import { PostHog } from "posthog-node";
327
+ function hashUserId(userId) {
328
+ return createHash("sha256").update(`${POSTHOG_SALT}:${userId}`).digest("hex").substring(0, 32);
329
+ }
330
+ function initPostHog() {
331
+ if (isTelemetryDisabled()) return;
332
+ const key = process.env.POSTHOG_KEY || process.env.VITE_POSTHOG_KEY;
333
+ const host = process.env.POSTHOG_HOST || process.env.VITE_POSTHOG_HOST || "https://eu.i.posthog.com";
334
+ if (!key) return;
335
+ client = new PostHog(key, { host, flushAt: 5, flushInterval: 1e4 });
336
+ }
337
+ async function captureToolEvent(args) {
338
+ if (!client) return;
339
+ let userId;
340
+ try {
341
+ userId = await getDefaultUserId();
342
+ } catch {
343
+ userId = "anonymous_mcp";
344
+ }
345
+ client.capture({
346
+ distinctId: hashUserId(userId),
347
+ event: `mcp_tool_${args.status}`,
348
+ properties: {
349
+ tool_name: args.toolName,
350
+ duration_ms: args.durationMs,
351
+ ...args.details
352
+ }
353
+ });
354
+ }
355
+ async function shutdownPostHog() {
356
+ if (client) {
357
+ await client.shutdown();
358
+ client = null;
359
+ }
360
+ }
361
+ var POSTHOG_SALT, client;
362
+ var init_posthog = __esm({
363
+ "src/lib/posthog.ts"() {
364
+ "use strict";
365
+ init_supabase();
366
+ POSTHOG_SALT = "socialneuron-mcp-ph-v1";
367
+ client = null;
409
368
  }
410
369
  });
411
370
 
@@ -482,7 +441,10 @@ async function getDefaultProjectId() {
482
441
  if (!userId) return null;
483
442
  try {
484
443
  const supabase = getSupabaseClient();
485
- const { data } = await supabase.from("projects").select("id").eq("user_id", userId).order("created_at", { ascending: false }).limit(1).single();
444
+ const { data: memberships } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId);
445
+ const orgIds = (memberships ?? []).map((m) => m.organization_id);
446
+ if (orgIds.length === 0) return null;
447
+ const { data } = await supabase.from("projects").select("id").in("organization_id", orgIds).order("created_at", { ascending: false }).limit(1).maybeSingle();
486
448
  if (data?.id) {
487
449
  projectIdCache.set(userId, data.id);
488
450
  return data.id;
@@ -496,33 +458,27 @@ async function initializeAuth() {
496
458
  const apiKey = await loadApiKey2();
497
459
  if (apiKey) {
498
460
  authenticatedApiKey = apiKey;
499
- const { readValidationCache: readValidationCache2, writeValidationCache: writeValidationCache2 } = await Promise.resolve().then(() => (init_validation_cache(), validation_cache_exports));
500
- const cached = readValidationCache2(apiKey);
501
- let result;
502
- if (cached) {
503
- result = cached;
504
- console.error("[MCP] Using cached validation (skip remote check)");
505
- } else {
506
- const { validateApiKey: validateApiKey2 } = await Promise.resolve().then(() => (init_api_keys(), api_keys_exports));
507
- result = await validateApiKey2(apiKey);
508
- if (result.valid) {
509
- writeValidationCache2(apiKey, result);
510
- }
511
- }
461
+ const _quietAuth = !process.argv.includes("--verbose") && (process.env.SN_CLI_QUIET === "1" || ["setup", "login", "logout", "whoami", "health", "sn", "repl"].includes(
462
+ process.argv[2] ?? ""
463
+ ));
464
+ const { validateApiKey: validateApiKey2 } = await Promise.resolve().then(() => (init_api_keys(), api_keys_exports));
465
+ const result = await validateApiKey2(apiKey);
512
466
  if (result.valid && result.userId) {
513
467
  _authMode = "api-key";
514
468
  authenticatedUserId = result.userId;
515
469
  authenticatedScopes = result.scopes && result.scopes.length > 0 ? result.scopes : ["mcp:read"];
516
470
  authenticatedEmail = result.email || null;
517
471
  authenticatedExpiresAt = result.expiresAt || null;
518
- console.error(
519
- "[MCP] Authenticated via API key (prefix: " + apiKey.substring(0, 6) + "..." + apiKey.slice(-4) + ")"
520
- );
521
- console.error("[MCP] Scopes: " + authenticatedScopes.join(", "));
472
+ if (!_quietAuth) {
473
+ console.error(
474
+ "[MCP] Authenticated via API key (prefix: " + apiKey.substring(0, 6) + "..." + apiKey.slice(-4) + ")"
475
+ );
476
+ console.error("[MCP] Scopes: " + authenticatedScopes.join(", "));
477
+ }
522
478
  if (authenticatedExpiresAt) {
523
479
  const expiresMs = new Date(authenticatedExpiresAt).getTime();
524
480
  const daysLeft = Math.ceil((expiresMs - Date.now()) / (1e3 * 60 * 60 * 24));
525
- console.error("[MCP] Key expires: " + authenticatedExpiresAt);
481
+ if (!_quietAuth) console.error("[MCP] Key expires: " + authenticatedExpiresAt);
526
482
  if (daysLeft <= 7) {
527
483
  console.error(
528
484
  `[MCP] Warning: API key expires in ${daysLeft} day(s). Run: npx @socialneuron/mcp-server login`
@@ -532,8 +488,13 @@ async function initializeAuth() {
532
488
  return;
533
489
  } else {
534
490
  authenticatedApiKey = null;
491
+ if (result.retryable) {
492
+ throw new Error(
493
+ "Temporary issue reaching the auth service \u2014 your session is likely still valid. Wait a moment and retry. If it persists, run `sn login`."
494
+ );
495
+ }
535
496
  throw new Error(
536
- "[MCP] Fatal: API key invalid or expired. Run: npx @socialneuron/mcp-server setup"
497
+ "API key invalid, expired, or revoked. Run `sn login` to reconnect (or `socialneuron-mcp login`)."
537
498
  );
538
499
  }
539
500
  }
@@ -590,14 +551,13 @@ async function logMcpToolInvocation(args) {
590
551
  });
591
552
  } catch {
592
553
  }
593
- captureToolEvent(args).catch(() => {
554
+ Promise.resolve().then(() => (init_posthog(), posthog_exports)).then(({ captureToolEvent: captureToolEvent2 }) => captureToolEvent2(args)).catch(() => {
594
555
  });
595
556
  }
596
557
  var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY, projectIdCache;
597
558
  var init_supabase = __esm({
598
559
  "src/lib/supabase.ts"() {
599
560
  "use strict";
600
- init_posthog();
601
561
  init_request_context();
602
562
  SUPABASE_URL = process.env.SOCIALNEURON_SUPABASE_URL || process.env.SUPABASE_URL || "";
603
563
  SUPABASE_SERVICE_KEY = process.env.SOCIALNEURON_SERVICE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY || "";
@@ -697,10 +657,16 @@ async function callEdgeFunction(functionName, body, options) {
697
657
  } catch {
698
658
  errorMessage = responseText || `HTTP ${response.status}`;
699
659
  }
700
- if (response.status === 401 || response.status === 403) {
660
+ if (response.status === 401) {
661
+ return {
662
+ data: null,
663
+ error: `Authentication failed (HTTP 401). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
664
+ };
665
+ }
666
+ if (response.status === 403) {
701
667
  return {
702
668
  data: null,
703
- error: `Authentication failed (HTTP ${response.status}). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
669
+ error: `Forbidden (HTTP 403): ${errorMessage}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
704
670
  };
705
671
  }
706
672
  if (response.status === 429) {
@@ -1073,13 +1039,15 @@ var init_tool_catalog = __esm({
1073
1039
  name: "capture_screenshot",
1074
1040
  description: "Capture a screenshot of a URL",
1075
1041
  module: "screenshot",
1076
- scope: "mcp:read"
1042
+ scope: "mcp:read",
1043
+ localOnly: true
1077
1044
  },
1078
1045
  {
1079
1046
  name: "capture_app_page",
1080
1047
  description: "Capture a screenshot of an app page",
1081
1048
  module: "screenshot",
1082
- scope: "mcp:read"
1049
+ scope: "mcp:read",
1050
+ localOnly: true
1083
1051
  },
1084
1052
  // remotion
1085
1053
  {
@@ -1267,7 +1235,7 @@ var init_tool_catalog = __esm({
1267
1235
  // niche research
1268
1236
  {
1269
1237
  name: "find_winning_content",
1270
- description: "Find QA-gated high-performing short-form videos in the project's niche. Returns extracted hook patterns, content structures, and pre-compiled replication prompts (backed by niche_winners view, qa_score >= 0.5).",
1238
+ description: "Find QA-gated high-performing short-form videos in the project's niche. Returns extracted hook patterns, content structures, and pre-compiled replication prompts.",
1271
1239
  module: "research",
1272
1240
  scope: "mcp:read"
1273
1241
  },
@@ -1292,6 +1260,18 @@ var init_tool_catalog = __esm({
1292
1260
  module: "discovery",
1293
1261
  scope: "mcp:read"
1294
1262
  },
1263
+ {
1264
+ name: "search",
1265
+ description: "Search public Social Neuron product, integration, developer, and MCP tool knowledge using the ChatGPT-compatible search schema.",
1266
+ module: "discovery",
1267
+ scope: "mcp:read"
1268
+ },
1269
+ {
1270
+ name: "fetch",
1271
+ description: "Fetch one public Social Neuron knowledge document by ID using the ChatGPT-compatible fetch schema.",
1272
+ module: "discovery",
1273
+ scope: "mcp:read"
1274
+ },
1295
1275
  // pipeline
1296
1276
  {
1297
1277
  name: "check_pipeline_readiness",
@@ -1406,58 +1386,93 @@ var init_tool_catalog = __esm({
1406
1386
  // agentic-harness — learning loop write-back
1407
1387
  {
1408
1388
  name: "write_agent_reflection",
1409
- description: "Persist a verbal reflection for an agent loop. Provenance keys are restricted (Anti-Goodhart safety): only content_history_id, outcome_event_id, prm_score_ids, and handoff_ids are accepted.",
1389
+ description: "Persist a verbal reflection for an agent loop. Provenance keys are restricted to an allowlist: only content_history_id, outcome_event_id, prm_score_ids, and handoff_ids are accepted.",
1410
1390
  module: "harness",
1411
- scope: "mcp:write"
1391
+ scope: "mcp:write",
1392
+ internal: true
1412
1393
  },
1413
1394
  {
1414
1395
  name: "record_outcome",
1415
- description: "Record an outcome for a published decision event. Idempotent on (decision_event_id, horizon). Only horizon=24h triggers a content_bandits posterior update.",
1396
+ description: "Record an outcome for a published decision event. Idempotent on (decision_event_id, horizon). Only horizon=24h triggers a learning-loop update.",
1416
1397
  module: "harness",
1417
- scope: "mcp:write"
1398
+ scope: "mcp:write",
1399
+ internal: true
1418
1400
  },
1419
1401
  // agentic-harness — learning loop read-back
1420
1402
  {
1421
1403
  name: "read_agent_reflection",
1422
1404
  description: "Read past agent reflections for a brand. Ordered by created_at DESC, id ASC (deterministic tiebreak). Only active reflections returned (superseded_by IS NULL). Optional generated_by_agent filter.",
1423
1405
  module: "harness",
1424
- scope: "mcp:read"
1406
+ scope: "mcp:read",
1407
+ internal: true
1425
1408
  },
1426
1409
  // hermes — autonomous agent integration (closed-loop content)
1427
1410
  {
1428
1411
  name: "save_draft_to_library",
1429
- description: "Save a draft post to the SN content library with origin=hermes. Used by Hermes to persist drafts before the founder approves them.",
1412
+ description: "Save a draft post to the SN content library for review before publishing. Drafts land in the content library pending approval.",
1430
1413
  module: "hermes",
1431
- scope: "mcp:write"
1414
+ scope: "mcp:write",
1415
+ internal: true
1432
1416
  },
1433
1417
  {
1434
1418
  name: "record_voice_lesson",
1435
- description: "Persist a learned voice lesson to brand_profiles.brand_context.voiceProfile.voice_lessons via atomic RPC. Used by Hermes reflection cron.",
1419
+ description: "Persist a learned voice lesson to the brand voice profile.",
1436
1420
  module: "hermes",
1437
- scope: "mcp:write"
1421
+ scope: "mcp:write",
1422
+ internal: true
1438
1423
  },
1439
1424
  {
1440
1425
  name: "record_observation",
1441
- description: 'Record an agent observation (e.g. "topic X engagement up 23%") for the UnifiedAnalytics > Playbook surface.',
1426
+ description: 'Record an agent observation (e.g. "topic X engagement up 23%") for the analytics playbook.',
1442
1427
  module: "hermes",
1443
- scope: "mcp:write"
1428
+ scope: "mcp:write",
1429
+ internal: true
1444
1430
  },
1445
1431
  {
1446
1432
  name: "record_intel_signal",
1447
- description: "Record a research/trend signal from Hermes watchers (news, HN, competitor, etc.) for Niche Intelligence. Dedupes by URL.",
1433
+ description: "Record a research/trend signal (news, competitor, community sources) for niche intelligence. Dedupes by URL.",
1448
1434
  module: "hermes",
1449
- scope: "mcp:write"
1435
+ scope: "mcp:write",
1436
+ internal: true
1450
1437
  },
1451
1438
  {
1452
1439
  name: "record_campaign_spend",
1453
- description: "Log a campaign cost line (hermes_drafts, carousel_renders, analytics_pulls, paid_amplification, other). Ownership-checked.",
1440
+ description: "Log a campaign cost line item. Ownership-checked.",
1454
1441
  module: "hermes",
1455
- scope: "mcp:write"
1442
+ scope: "mcp:write",
1443
+ internal: true
1456
1444
  },
1457
1445
  {
1458
1446
  name: "get_active_campaigns",
1459
- description: "List currently-running campaigns with thesis, budget, hero format, and current spend. Used by Hermes pitch skill to bias drafts.",
1447
+ description: "List currently-running campaigns with thesis, budget, hero format, and current spend.",
1460
1448
  module: "hermes",
1449
+ scope: "mcp:read",
1450
+ internal: true
1451
+ },
1452
+ // skills (workflow skills — multi-step brand-locked content pipelines)
1453
+ {
1454
+ name: "list_skills",
1455
+ description: "List Social Neuron content workflow skills available to the user. A skill is a brand-locked multi-step pipeline inspired by documented viral patterns (MrBeast 3-second hook, Hormozi pattern interrupt, etc.).",
1456
+ module: "skills",
1457
+ scope: "mcp:read"
1458
+ },
1459
+ {
1460
+ name: "run_skill",
1461
+ description: "Run a Social Neuron workflow skill end-to-end (brand-locked content production). Returns a structured run preview with the step plan, credit cost, and a deep-link to launch in the SN dashboard.",
1462
+ module: "skills",
1463
+ scope: "mcp:write"
1464
+ },
1465
+ // loop observability (growth-loop KPIs + content learning state)
1466
+ {
1467
+ name: "get_loop_pulse",
1468
+ description: "Read dynamic loop-health KPIs for the growth loop over the last 7 days (reflection/decision coverage, visual gate pass rate, learning-update application rate, per-platform uptake, autopilot lag) \u2014 each with an ok/warn/bad status. Use to decide whether the loop is closing or where it is stuck.",
1469
+ module: "loop",
1470
+ scope: "mcp:read"
1471
+ },
1472
+ {
1473
+ name: "get_bandit_state",
1474
+ description: "Read the current content learning state for a project \u2014 top-K arms per (arm_type, platform) with expected performance and uncertainty. Use to reason about which hook family / format / timing slot currently performs best per platform.",
1475
+ module: "loop",
1461
1476
  scope: "mcp:read"
1462
1477
  }
1463
1478
  ];
@@ -1602,7 +1617,7 @@ function classifyError(err) {
1602
1617
  hint: "Run: npx @socialneuron/mcp-server login \u2014 Requires a paid plan (Starter+). See https://socialneuron.com/pricing"
1603
1618
  };
1604
1619
  }
1605
- if (lower.includes("econnrefused") || lower.includes("etimedout") || lower.includes("fetch failed") || lower.includes("aborterror") || lower.includes("network") || lower.includes("dns")) {
1620
+ if (lower.includes("econnrefused") || lower.includes("etimedout") || lower.includes("fetch failed") || lower.includes("aborterror") || lower.includes("network") || lower.includes("dns") || lower.includes("temporary issue")) {
1606
1621
  return {
1607
1622
  message,
1608
1623
  errorType: "NETWORK",
@@ -2631,6 +2646,7 @@ async function handleTools(args, asJson) {
2631
2646
  if (typeof module === "string") {
2632
2647
  tools = getToolsByModule(module);
2633
2648
  }
2649
+ tools = tools.filter((t) => !t.internal);
2634
2650
  if (asJson) {
2635
2651
  emitSnResult({ ok: true, command: "tools", toolCount: tools.length, tools }, true);
2636
2652
  return;
@@ -2661,7 +2677,7 @@ Module: ${moduleName} (${moduleTools.length} tools)`);
2661
2677
  async function handleInfo(args, asJson) {
2662
2678
  const info = {
2663
2679
  version: MCP_VERSION,
2664
- toolCount: TOOL_CATALOG.length,
2680
+ toolCount: TOOL_CATALOG.filter((t) => !t.internal).length,
2665
2681
  modules: getModules()
2666
2682
  };
2667
2683
  try {
@@ -2872,13 +2888,13 @@ __export(presets_exports, {
2872
2888
  handlePreset: () => handlePreset,
2873
2889
  resolvePreset: () => resolvePreset
2874
2890
  });
2875
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync2 } from "node:fs";
2876
- import { join as join3 } from "node:path";
2877
- import { homedir as homedir3 } from "node:os";
2891
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "node:fs";
2892
+ import { join as join2 } from "node:path";
2893
+ import { homedir as homedir2 } from "node:os";
2878
2894
  function loadUserPresets() {
2879
2895
  if (!existsSync2(PRESETS_FILE)) return [];
2880
2896
  try {
2881
- const raw = readFileSync3(PRESETS_FILE, "utf-8");
2897
+ const raw = readFileSync2(PRESETS_FILE, "utf-8");
2882
2898
  const parsed = JSON.parse(raw);
2883
2899
  if (!Array.isArray(parsed)) return [];
2884
2900
  return parsed;
@@ -2888,9 +2904,9 @@ function loadUserPresets() {
2888
2904
  }
2889
2905
  function saveUserPresets(presets) {
2890
2906
  if (!existsSync2(PRESETS_DIR)) {
2891
- mkdirSync3(PRESETS_DIR, { recursive: true });
2907
+ mkdirSync2(PRESETS_DIR, { recursive: true });
2892
2908
  }
2893
- writeFileSync3(PRESETS_FILE, JSON.stringify(presets, null, 2) + "\n", "utf-8");
2909
+ writeFileSync2(PRESETS_FILE, JSON.stringify(presets, null, 2) + "\n", "utf-8");
2894
2910
  }
2895
2911
  function resolvePreset(name) {
2896
2912
  const lower = name.toLowerCase();
@@ -3055,8 +3071,8 @@ var init_presets = __esm({
3055
3071
  { name: "linkedin-post", platform: "LinkedIn", maxLength: 3e3, builtin: true },
3056
3072
  { name: "twitter-post", platform: "Twitter", maxLength: 280, builtin: true }
3057
3073
  ];
3058
- PRESETS_DIR = join3(homedir3(), ".config", "socialneuron");
3059
- PRESETS_FILE = join3(PRESETS_DIR, "presets.json");
3074
+ PRESETS_DIR = join2(homedir2(), ".config", "socialneuron");
3075
+ PRESETS_FILE = join2(PRESETS_DIR, "presets.json");
3060
3076
  USAGE = `Usage: sn preset <sub-command> [options]
3061
3077
 
3062
3078
  Sub-commands:
@@ -3259,9 +3275,9 @@ __export(setup_exports, {
3259
3275
  });
3260
3276
  import { createHash as createHash4, randomBytes, randomUUID as randomUUID4 } from "node:crypto";
3261
3277
  import { createServer } from "node:http";
3262
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
3263
- import { homedir as homedir4, platform as platform2 } from "node:os";
3264
- import { join as join4 } from "node:path";
3278
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
3279
+ import { homedir as homedir3, platform as platform2 } from "node:os";
3280
+ import { join as join3 } from "node:path";
3265
3281
  function base64url(buffer) {
3266
3282
  return buffer.toString("base64url");
3267
3283
  }
@@ -3283,8 +3299,8 @@ function getConfigPaths() {
3283
3299
  const os = platform2();
3284
3300
  if (os === "darwin") {
3285
3301
  paths.push({
3286
- path: join4(
3287
- homedir4(),
3302
+ path: join3(
3303
+ homedir3(),
3288
3304
  "Library",
3289
3305
  "Application Support",
3290
3306
  "Claude",
@@ -3294,15 +3310,15 @@ function getConfigPaths() {
3294
3310
  });
3295
3311
  } else if (os === "linux") {
3296
3312
  paths.push({
3297
- path: join4(homedir4(), ".config", "claude", "claude_desktop_config.json"),
3313
+ path: join3(homedir3(), ".config", "claude", "claude_desktop_config.json"),
3298
3314
  name: "Claude Desktop"
3299
3315
  });
3300
3316
  }
3301
- const claudeCodeGlobal = join4(homedir4(), ".claude", ".mcp.json");
3317
+ const claudeCodeGlobal = join3(homedir3(), ".claude", ".mcp.json");
3302
3318
  if (existsSync3(claudeCodeGlobal)) {
3303
3319
  paths.push({ path: claudeCodeGlobal, name: "Claude Code (global)" });
3304
3320
  }
3305
- const projectConfig = join4(process.cwd(), ".mcp.json");
3321
+ const projectConfig = join3(process.cwd(), ".mcp.json");
3306
3322
  if (existsSync3(projectConfig)) {
3307
3323
  paths.push({ path: projectConfig, name: "Claude Code (project)" });
3308
3324
  }
@@ -3312,12 +3328,12 @@ function configureMcpClient(configPath) {
3312
3328
  try {
3313
3329
  let config = {};
3314
3330
  if (existsSync3(configPath)) {
3315
- const raw = readFileSync4(configPath, "utf-8");
3331
+ const raw = readFileSync3(configPath, "utf-8");
3316
3332
  config = JSON.parse(raw);
3317
3333
  } else {
3318
- const dir = join4(configPath, "..");
3334
+ const dir = join3(configPath, "..");
3319
3335
  if (!existsSync3(dir)) {
3320
- mkdirSync4(dir, { recursive: true });
3336
+ mkdirSync3(dir, { recursive: true });
3321
3337
  }
3322
3338
  }
3323
3339
  if (!config.mcpServers) {
@@ -3327,7 +3343,7 @@ function configureMcpClient(configPath) {
3327
3343
  command: "npx",
3328
3344
  args: ["-y", "@socialneuron/mcp-server"]
3329
3345
  };
3330
- writeFileSync4(configPath, JSON.stringify(config, null, 2) + "\n");
3346
+ writeFileSync3(configPath, JSON.stringify(config, null, 2) + "\n");
3331
3347
  return true;
3332
3348
  } catch {
3333
3349
  return false;
@@ -3504,38 +3520,110 @@ var init_setup = __esm({
3504
3520
  }
3505
3521
  });
3506
3522
 
3507
- // src/cli/commands.ts
3508
- var commands_exports = {};
3509
- __export(commands_exports, {
3510
- runHealthCheck: () => runHealthCheck,
3511
- runLogin: () => runLogin,
3512
- runLogoutCommand: () => runLogoutCommand,
3513
- runWhoami: () => runWhoami
3523
+ // src/lib/validation-cache.ts
3524
+ var validation_cache_exports = {};
3525
+ __export(validation_cache_exports, {
3526
+ clearValidationCache: () => clearValidationCache,
3527
+ keyFingerprint: () => keyFingerprint,
3528
+ readValidationCache: () => readValidationCache,
3529
+ writeValidationCache: () => writeValidationCache
3514
3530
  });
3515
- import { createInterface } from "node:readline";
3516
- function prompt(question) {
3517
- const rl = createInterface({ input: process.stdin, output: process.stderr });
3518
- return new Promise((resolve3) => {
3519
- rl.question(question, (answer) => {
3520
- rl.close();
3521
- resolve3(answer.trim());
3522
- });
3523
- });
3524
- }
3525
- function getDefaultSupabaseUrl2() {
3526
- return process.env.SOCIALNEURON_SUPABASE_URL || process.env.SUPABASE_URL || CLOUD_SUPABASE_URL;
3531
+ import { createHash as createHash5 } from "node:crypto";
3532
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, chmodSync } from "node:fs";
3533
+ import { join as join4 } from "node:path";
3534
+ import { homedir as homedir4 } from "node:os";
3535
+ function keyFingerprint(apiKey) {
3536
+ return `sha256:${createHash5("sha256").update(apiKey, "utf8").digest("hex")}`;
3527
3537
  }
3528
- async function runLogin(method) {
3529
- if (method === "browser") {
3530
- await runSetup();
3531
- return;
3532
- }
3533
- if (method === "paste") {
3534
- await runLoginPaste();
3535
- return;
3536
- }
3537
- if (method === "device") {
3538
- await runLoginDevice();
3538
+ function readValidationCache(apiKey) {
3539
+ try {
3540
+ const raw = readFileSync4(CACHE_FILE, "utf-8");
3541
+ const cached = JSON.parse(raw);
3542
+ if (cached.keyFingerprint !== keyFingerprint(apiKey)) {
3543
+ return null;
3544
+ }
3545
+ if (Date.now() - cached.cachedAt > CACHE_TTL_MS) {
3546
+ return null;
3547
+ }
3548
+ if (!cached.result.valid) {
3549
+ return null;
3550
+ }
3551
+ if (cached.result.expiresAt) {
3552
+ const expiresMs = new Date(cached.result.expiresAt).getTime();
3553
+ if (expiresMs <= Date.now()) {
3554
+ return null;
3555
+ }
3556
+ }
3557
+ return cached.result;
3558
+ } catch {
3559
+ return null;
3560
+ }
3561
+ }
3562
+ function writeValidationCache(apiKey, result) {
3563
+ if (!result.valid) return;
3564
+ try {
3565
+ mkdirSync4(CONFIG_DIR2, { recursive: true });
3566
+ const entry = {
3567
+ keyFingerprint: keyFingerprint(apiKey),
3568
+ result,
3569
+ cachedAt: Date.now()
3570
+ };
3571
+ writeFileSync4(CACHE_FILE, JSON.stringify(entry, null, 2), { mode: 384 });
3572
+ try {
3573
+ chmodSync(CACHE_FILE, 384);
3574
+ } catch {
3575
+ }
3576
+ } catch {
3577
+ }
3578
+ }
3579
+ function clearValidationCache() {
3580
+ try {
3581
+ writeFileSync4(CACHE_FILE, "{}", { mode: 384 });
3582
+ } catch {
3583
+ }
3584
+ }
3585
+ var CONFIG_DIR2, CACHE_FILE, CACHE_TTL_MS;
3586
+ var init_validation_cache = __esm({
3587
+ "src/lib/validation-cache.ts"() {
3588
+ "use strict";
3589
+ CONFIG_DIR2 = join4(homedir4(), ".config", "socialneuron");
3590
+ CACHE_FILE = join4(CONFIG_DIR2, "validation-cache.json");
3591
+ CACHE_TTL_MS = 5 * 60 * 1e3;
3592
+ }
3593
+ });
3594
+
3595
+ // src/cli/commands.ts
3596
+ var commands_exports = {};
3597
+ __export(commands_exports, {
3598
+ runHealthCheck: () => runHealthCheck,
3599
+ runLogin: () => runLogin,
3600
+ runLogoutCommand: () => runLogoutCommand,
3601
+ runWhoami: () => runWhoami
3602
+ });
3603
+ import { createInterface } from "node:readline";
3604
+ function prompt(question) {
3605
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
3606
+ return new Promise((resolve3) => {
3607
+ rl.question(question, (answer) => {
3608
+ rl.close();
3609
+ resolve3(answer.trim());
3610
+ });
3611
+ });
3612
+ }
3613
+ function getDefaultSupabaseUrl2() {
3614
+ return process.env.SOCIALNEURON_SUPABASE_URL || process.env.SUPABASE_URL || CLOUD_SUPABASE_URL;
3615
+ }
3616
+ async function runLogin(method) {
3617
+ if (method === "browser") {
3618
+ await runSetup();
3619
+ return;
3620
+ }
3621
+ if (method === "paste") {
3622
+ await runLoginPaste();
3623
+ return;
3624
+ }
3625
+ if (method === "device") {
3626
+ await runLoginDevice();
3539
3627
  return;
3540
3628
  }
3541
3629
  }
@@ -3626,20 +3714,27 @@ async function runLoginDevice() {
3626
3714
  if (pollData.api_key) {
3627
3715
  try {
3628
3716
  await saveApiKey(pollData.api_key);
3629
- await saveSupabaseUrl(supabaseUrl);
3630
3717
  } catch (saveErr) {
3631
3718
  console.error("");
3632
- console.error(" Warning: Could not save key to keychain.");
3719
+ console.error(" Warning: Could not save API key securely.");
3633
3720
  console.error(` Error: ${saveErr instanceof Error ? saveErr.message : String(saveErr)}`);
3634
3721
  console.error("");
3635
- console.error(" Your API key (save this \u2014 it cannot be retrieved again):");
3636
- console.error(` ${pollData.api_key}`);
3637
- console.error("");
3638
- console.error(" To authenticate manually, run:");
3722
+ console.error(" For security, the API key was not printed to the terminal.");
3723
+ console.error(" Create or copy an API key from your Social Neuron dashboard, then run:");
3639
3724
  console.error(" npx @socialneuron/mcp-server login --paste");
3640
3725
  console.error("");
3641
- console.error(" Or set the environment variable:");
3642
- console.error(` export SOCIALNEURON_API_KEY="${pollData.api_key}"`);
3726
+ return;
3727
+ }
3728
+ try {
3729
+ await saveSupabaseUrl(supabaseUrl);
3730
+ } catch (saveErr) {
3731
+ console.error("");
3732
+ console.error(" Authorized!");
3733
+ console.error(` Key prefix: ${pollData.api_key.substring(0, 12)}...`);
3734
+ console.error("");
3735
+ console.error(" Warning: API key saved, but could not save the Supabase URL.");
3736
+ console.error(` Error: ${saveErr instanceof Error ? saveErr.message : String(saveErr)}`);
3737
+ console.error(" If needed, set SOCIALNEURON_SUPABASE_URL to your Supabase URL.");
3643
3738
  console.error("");
3644
3739
  return;
3645
3740
  }
@@ -4155,6 +4250,8 @@ var TOOL_SCOPES = {
4155
4250
  get_mcp_usage: "mcp:read",
4156
4251
  list_plan_approvals: "mcp:read",
4157
4252
  search_tools: "mcp:read",
4253
+ search: "mcp:read",
4254
+ fetch: "mcp:read",
4158
4255
  // mcp:read (pipeline readiness + status are read-only)
4159
4256
  check_pipeline_readiness: "mcp:read",
4160
4257
  get_pipeline_status: "mcp:read",
@@ -4182,7 +4279,13 @@ var TOOL_SCOPES = {
4182
4279
  record_intel_signal: "mcp:write",
4183
4280
  record_campaign_spend: "mcp:write",
4184
4281
  // mcp:read
4185
- get_active_campaigns: "mcp:read"
4282
+ get_active_campaigns: "mcp:read",
4283
+ // mcp:read / mcp:write (Skills)
4284
+ list_skills: "mcp:read",
4285
+ run_skill: "mcp:write",
4286
+ // mcp:read (Loop observability — growth-loop KPIs + content learning state)
4287
+ get_loop_pulse: "mcp:read",
4288
+ get_bandit_state: "mcp:read"
4186
4289
  };
4187
4290
  function hasScope(userScopes, required) {
4188
4291
  if (userScopes.includes(required)) return true;
@@ -4282,6 +4385,10 @@ var OVERRIDES = {
4282
4385
  generate_performance_digest: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
4283
4386
  detect_anomalies: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
4284
4387
  };
4388
+ function buildToolSecuritySchemes(toolName) {
4389
+ const scope = TOOL_SCOPES[toolName];
4390
+ return scope ? [{ type: "oauth2", scopes: [scope] }] : [];
4391
+ }
4285
4392
  function buildAnnotationsMap() {
4286
4393
  const map = /* @__PURE__ */ new Map();
4287
4394
  for (const [toolName, scope] of Object.entries(TOOL_SCOPES)) {
@@ -4326,17 +4433,37 @@ function applyAnnotations(server2) {
4326
4433
  destructiveHint: ann.destructiveHint,
4327
4434
  idempotentHint: ann.idempotentHint,
4328
4435
  openWorldHint: ann.openWorldHint
4329
- }
4436
+ },
4437
+ securitySchemes: buildToolSecuritySchemes(toolName)
4330
4438
  });
4331
4439
  applied++;
4332
4440
  }
4333
4441
  }
4334
- console.log(`[annotations] Applied annotations to ${applied}/${entries.length} tools`);
4442
+ console.error(`[annotations] Applied annotations to ${applied}/${entries.length} tools`);
4335
4443
  }
4336
4444
 
4337
4445
  // src/lib/register-tools.ts
4338
4446
  init_supabase();
4339
4447
 
4448
+ // src/lib/www-authenticate.ts
4449
+ function buildWwwAuthenticateHeader(opts) {
4450
+ const SANITIZE_CONTROL = /[\x00-\x1f\x7f]/g;
4451
+ const sanitize = (s) => s.replace(/[\\"]/g, "_").replace(SANITIZE_CONTROL, " ");
4452
+ const params = ['realm="socialneuron"'];
4453
+ if (opts.error) {
4454
+ params.push(`error="${sanitize(opts.error)}"`);
4455
+ }
4456
+ if (opts.errorDescription) {
4457
+ params.push(`error_description="${sanitize(opts.errorDescription)}"`);
4458
+ }
4459
+ if (opts.scope) {
4460
+ params.push(`scope="${sanitize(opts.scope)}"`);
4461
+ }
4462
+ const issuer = opts.issuerUrl.replace(/\/+$/, "");
4463
+ params.push(`resource_metadata="${issuer}/.well-known/oauth-protected-resource"`);
4464
+ return `Bearer ${params.join(", ")}`;
4465
+ }
4466
+
4340
4467
  // src/lib/agent-harness/constants.json
4341
4468
  var constants_default = {
4342
4469
  ZERO_WIDTH_CHARS: "[\\u200B\\u200C\\u200D\\u2060\\uFEFF\\u{E0020}-\\u{E007F}]",
@@ -4538,6 +4665,16 @@ function checkRateLimit(category, key) {
4538
4665
 
4539
4666
  // src/tools/ideation.ts
4540
4667
  init_supabase();
4668
+ init_version();
4669
+ function asEnvelope(data) {
4670
+ return {
4671
+ _meta: {
4672
+ version: MCP_VERSION,
4673
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4674
+ },
4675
+ data
4676
+ };
4677
+ }
4541
4678
  function registerIdeationTools(server2) {
4542
4679
  server2.tool(
4543
4680
  "generate_content",
@@ -4699,7 +4836,14 @@ Content Type: ${content_type}`;
4699
4836
  };
4700
4837
  }
4701
4838
  const text = data?.text ?? "(empty response)";
4839
+ const structuredContent = asEnvelope({
4840
+ text,
4841
+ content_type,
4842
+ platform: platform3 ?? null,
4843
+ model: model ?? "gemini-2.5-flash"
4844
+ });
4702
4845
  return {
4846
+ structuredContent,
4703
4847
  content: [{ type: "text", text }]
4704
4848
  };
4705
4849
  }
@@ -4733,7 +4877,10 @@ Content Type: ${content_type}`;
4733
4877
  const { data, error } = await callEdgeFunction(
4734
4878
  "fetch-trends",
4735
4879
  {
4736
- source,
4880
+ // Forward as `trend_source`: the mcp-gateway overwrites top-level `source`
4881
+ // with 'mcp' (attribution) before reaching the EF, which collided with the
4882
+ // routing param. The EF reads `trend_source ?? source`.
4883
+ trend_source: source,
4737
4884
  category: category ?? "general",
4738
4885
  niche,
4739
4886
  url,
@@ -4902,7 +5049,14 @@ ${content}`,
4902
5049
  const header = `Adapted for ${target_platform}${source_platform ? ` (from ${source_platform})` : ""}:
4903
5050
 
4904
5051
  `;
5052
+ const structuredContent = asEnvelope({
5053
+ text,
5054
+ source_platform: source_platform ?? null,
5055
+ target_platform,
5056
+ model: "gemini-2.5-flash"
5057
+ });
4905
5058
  return {
5059
+ structuredContent,
4906
5060
  content: [{ type: "text", text: header + text }]
4907
5061
  };
4908
5062
  }
@@ -4913,8 +5067,10 @@ ${content}`,
4913
5067
  init_edge_function();
4914
5068
  import { z as z2 } from "zod";
4915
5069
  init_supabase();
4916
- init_request_context();
4917
5070
  init_version();
5071
+
5072
+ // src/lib/budget.ts
5073
+ init_request_context();
4918
5074
  var MAX_CREDITS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
4919
5075
  var MAX_ASSETS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0));
4920
5076
  var _globalCreditsUsed = 0;
@@ -4955,7 +5111,35 @@ function getCurrentBudgetStatus() {
4955
5111
  remainingAssets: MAX_ASSETS_PER_RUN > 0 ? Math.max(0, MAX_ASSETS_PER_RUN - assetsGen) : null
4956
5112
  };
4957
5113
  }
4958
- function asEnvelope(data) {
5114
+ function checkCreditBudget(estimatedCost) {
5115
+ if (MAX_CREDITS_PER_RUN <= 0) {
5116
+ return { ok: true };
5117
+ }
5118
+ const used = getCreditsUsed();
5119
+ if (used + estimatedCost > MAX_CREDITS_PER_RUN) {
5120
+ return {
5121
+ ok: false,
5122
+ message: `Credit budget exceeded for this MCP run. Used=${used}, next~=${estimatedCost}, limit=${MAX_CREDITS_PER_RUN}.`
5123
+ };
5124
+ }
5125
+ return { ok: true };
5126
+ }
5127
+ function checkAssetBudget(requestedCount = 1) {
5128
+ if (MAX_ASSETS_PER_RUN <= 0) {
5129
+ return { ok: true };
5130
+ }
5131
+ const generated = getAssetsGenerated();
5132
+ if (generated + requestedCount > MAX_ASSETS_PER_RUN) {
5133
+ return {
5134
+ ok: false,
5135
+ message: `Asset budget exceeded for this MCP run. Generated=${generated}, next=${requestedCount}, limit=${MAX_ASSETS_PER_RUN}.`
5136
+ };
5137
+ }
5138
+ return { ok: true };
5139
+ }
5140
+
5141
+ // src/tools/content.ts
5142
+ function asEnvelope2(data) {
4959
5143
  return {
4960
5144
  _meta: {
4961
5145
  version: MCP_VERSION,
@@ -4982,35 +5166,9 @@ var IMAGE_CREDIT_ESTIMATES = {
4982
5166
  "flux-max": 50,
4983
5167
  "gpt4o-image": 40,
4984
5168
  imagen4: 35,
4985
- "imagen4-fast": 25,
5169
+ "imagen4-fast": 35,
4986
5170
  seedream: 20
4987
5171
  };
4988
- function checkCreditBudget(estimatedCost) {
4989
- if (MAX_CREDITS_PER_RUN <= 0) {
4990
- return { ok: true };
4991
- }
4992
- const used = getCreditsUsed();
4993
- if (used + estimatedCost > MAX_CREDITS_PER_RUN) {
4994
- return {
4995
- ok: false,
4996
- message: `Credit budget exceeded for this MCP run. Used=${used}, next~=${estimatedCost}, limit=${MAX_CREDITS_PER_RUN}.`
4997
- };
4998
- }
4999
- return { ok: true };
5000
- }
5001
- function checkAssetBudget() {
5002
- if (MAX_ASSETS_PER_RUN <= 0) {
5003
- return { ok: true };
5004
- }
5005
- const generated = getAssetsGenerated();
5006
- if (generated + 1 > MAX_ASSETS_PER_RUN) {
5007
- return {
5008
- ok: false,
5009
- message: `Asset budget exceeded for this MCP run. Generated=${generated}, next=1, limit=${MAX_ASSETS_PER_RUN}.`
5010
- };
5011
- }
5012
- return { ok: true };
5013
- }
5014
5172
  function registerContentTools(server2) {
5015
5173
  server2.tool(
5016
5174
  "generate_video",
@@ -5129,7 +5287,7 @@ function registerContentTools(server2) {
5129
5287
  {
5130
5288
  type: "text",
5131
5289
  text: JSON.stringify(
5132
- asEnvelope({
5290
+ asEnvelope2({
5133
5291
  jobId,
5134
5292
  taskId: data.taskId,
5135
5293
  asyncJobId: data.asyncJobId,
@@ -5259,7 +5417,7 @@ function registerContentTools(server2) {
5259
5417
  {
5260
5418
  type: "text",
5261
5419
  text: JSON.stringify(
5262
- asEnvelope({
5420
+ asEnvelope2({
5263
5421
  jobId,
5264
5422
  taskId: data.taskId,
5265
5423
  asyncJobId: data.asyncJobId,
@@ -5362,7 +5520,7 @@ function registerContentTools(server2) {
5362
5520
  {
5363
5521
  type: "text",
5364
5522
  text: JSON.stringify(
5365
- asEnvelope({
5523
+ asEnvelope2({
5366
5524
  jobId: job.id,
5367
5525
  jobType: job.job_type,
5368
5526
  model: job.model,
@@ -5423,7 +5581,7 @@ function registerContentTools(server2) {
5423
5581
  all_urls: allUrls ?? null
5424
5582
  };
5425
5583
  return {
5426
- content: [{ type: "text", text: JSON.stringify(asEnvelope(enriched), null, 2) }]
5584
+ content: [{ type: "text", text: JSON.stringify(asEnvelope2(enriched), null, 2) }]
5427
5585
  };
5428
5586
  }
5429
5587
  return {
@@ -5552,7 +5710,7 @@ Return ONLY valid JSON in this exact format:
5552
5710
  try {
5553
5711
  const parsed = JSON.parse(rawContent);
5554
5712
  return {
5555
- content: [{ type: "text", text: JSON.stringify(asEnvelope(parsed), null, 2) }]
5713
+ content: [{ type: "text", text: JSON.stringify(asEnvelope2(parsed), null, 2) }]
5556
5714
  };
5557
5715
  } catch {
5558
5716
  return {
@@ -5580,20 +5738,8 @@ Return ONLY valid JSON in this exact format:
5580
5738
  "Generate a voiceover audio file for video narration. Returns an R2-hosted audio URL. Use after create_storyboard to add narration to each scene, or standalone for podcast intros and ad reads. Costs ~2 credits per generation.",
5581
5739
  {
5582
5740
  text: z2.string().max(5e3).describe("The script/text to convert to speech."),
5583
- voice: z2.enum([
5584
- "rachel",
5585
- "drew",
5586
- "clyde",
5587
- "paul",
5588
- "domi",
5589
- "dave",
5590
- "fin",
5591
- "sarah",
5592
- "antoni",
5593
- "thomas",
5594
- "charlie"
5595
- ]).optional().describe(
5596
- "Voice selection. rachel=warm female, drew=confident male, paul=authoritative male, sarah=friendly female. Defaults to rachel."
5741
+ voice: z2.enum(["rachel", "domi"]).optional().describe(
5742
+ "Voice selection. rachel=warm female, domi=confident female. Defaults to rachel."
5597
5743
  ),
5598
5744
  speed: z2.number().min(0.5).max(2).optional().describe("Speech speed multiplier. 1.0 is normal. Defaults to 1.0."),
5599
5745
  response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
@@ -5621,11 +5767,15 @@ Return ONLY valid JSON in this exact format:
5621
5767
  isError: true
5622
5768
  };
5623
5769
  }
5770
+ const ELEVENLABS_VOICE_IDS = {
5771
+ rachel: "21m00Tcm4TlvDq8ikWAM",
5772
+ domi: "AZnzlk1XvdvUeBnXmlld"
5773
+ };
5624
5774
  const { data, error } = await callEdgeFunction(
5625
5775
  "elevenlabs-tts",
5626
5776
  {
5627
5777
  text,
5628
- voice: voice ?? "rachel",
5778
+ voiceId: ELEVENLABS_VOICE_IDS[voice ?? "rachel"],
5629
5779
  speed: speed ?? 1
5630
5780
  },
5631
5781
  { timeoutMs: 6e4 }
@@ -5651,7 +5801,7 @@ Return ONLY valid JSON in this exact format:
5651
5801
  {
5652
5802
  type: "text",
5653
5803
  text: JSON.stringify(
5654
- asEnvelope({
5804
+ asEnvelope2({
5655
5805
  audioUrl: data.audioUrl,
5656
5806
  durationSeconds: data.durationSeconds,
5657
5807
  voice: voice ?? "rachel"
@@ -5800,7 +5950,7 @@ Return ONLY valid JSON in this exact format:
5800
5950
  {
5801
5951
  type: "text",
5802
5952
  text: JSON.stringify(
5803
- asEnvelope({
5953
+ asEnvelope2({
5804
5954
  carouselId: data.carousel.id,
5805
5955
  templateId,
5806
5956
  style: resolvedStyle,
@@ -6069,7 +6219,7 @@ var PLATFORM_CASE_MAP = {
6069
6219
  };
6070
6220
  var TIKTOK_AUDIT_APPROVED = false;
6071
6221
  var MCP_NOT_LIVE_FOR_POSTING = /* @__PURE__ */ new Set(["LinkedIn", "Pinterest", "Reddit"]);
6072
- function asEnvelope2(data) {
6222
+ function asEnvelope3(data) {
6073
6223
  return {
6074
6224
  _meta: {
6075
6225
  version: MCP_VERSION,
@@ -6167,7 +6317,24 @@ function registerDistributionTools(server2) {
6167
6317
  linkedin: z3.object({
6168
6318
  article_url: z3.string().optional()
6169
6319
  }).optional(),
6170
- twitter: z3.object({}).passthrough().optional()
6320
+ twitter: z3.object({
6321
+ paid_partnership: z3.boolean().optional().describe(
6322
+ "Set true for sponsored, affiliate, or branded campaign content. Forwarded to X as paid_partnership and persisted on posts.metadata.paid_partnership."
6323
+ ),
6324
+ disclosure: z3.union([
6325
+ z3.string(),
6326
+ z3.object({
6327
+ label: z3.string().optional(),
6328
+ text: z3.string().optional(),
6329
+ source: z3.string().optional(),
6330
+ required: z3.boolean().optional()
6331
+ }).passthrough()
6332
+ ]).optional().describe(
6333
+ "Campaign disclosure metadata for X posts. String values become disclosure.text; object values persist to posts.metadata.disclosure."
6334
+ ),
6335
+ disclosure_text: z3.string().optional(),
6336
+ disclosure_label: z3.string().optional()
6337
+ }).passthrough().optional()
6171
6338
  }).optional().describe(
6172
6339
  'Platform-specific metadata. Example: {"tiktok":{"privacy_status":"PUBLIC_TO_EVERYONE"}, "youtube":{"title":"My Video"}}'
6173
6340
  ),
@@ -6211,11 +6378,11 @@ function registerDistributionTools(server2) {
6211
6378
  visual_gate_result: z3.object({ passed: z3.boolean() }).passthrough().optional().describe(
6212
6379
  "Visual QA gate verdict from the carousel/image generation pipeline. Required when mediaType is CAROUSEL_ALBUM, IMAGE, or VIDEO and VISUAL_GATE_ENFORCE is enabled on the EF. Produce with visual_quality_check tool. Must have passed=true for the gate to allow the publish."
6213
6380
  ),
6214
- origin: z3.literal("hermes").optional().describe(
6215
- "Mark this post as authored by an autonomous agent loop (Hermes-style). When set to 'hermes', the post is recorded with posts.origin='hermes' so the hermes_watch_results weekly review can compare engagement against your human posts. Omit (default) for posts where a human composed or approved the content. Only honoured for MCP service-role callers \u2014 JWT/frontend callers are downgraded to human."
6381
+ origin: z3.enum(["human", "hermes", "user"]).optional().describe(
6382
+ "Originator lineage for the post. 'hermes' marks the post as scheduled by the Hermes autonomous agent \u2014 surfaces the Hermes badge + filter in Distribution. Default 'human'. Use only when calling from an autonomous workflow (cron, Slack bot, etc.)."
6216
6383
  ),
6217
6384
  hermes_run_id: z3.string().optional().describe(
6218
- "Hermes cron run identifier, persisted alongside origin='hermes' for traceability (e.g. cron_<id>_<timestamp>). Ignored unless origin=hermes."
6385
+ "Hermes cron run identifier \u2014 e.g. 'cron_<id>_<timestamp>'. Persists to posts.hermes_run_id when origin='hermes'. Use for traceability between drafts and the cron that produced them."
6219
6386
  )
6220
6387
  },
6221
6388
  async ({
@@ -6498,11 +6665,11 @@ Created with Social Neuron`;
6498
6665
  // Forward visual gate verdict + source so the EF can enforce on media posts.
6499
6666
  ...visual_gate_result ? { visualGateResult: visual_gate_result } : {},
6500
6667
  visualGateSource: "mcp",
6501
- // Forward autonomous-agent provenance for hermes_watch_results accounting.
6502
- // EF gates origin='hermes' on isServiceRoleCall && body.source==='mcp' both true
6503
- // for mcp-gateway-routed calls — so JWT/frontend callers can never claim it.
6668
+ // Originator lineage (Hermes integration, 2026-05-22). EF validates origin
6669
+ // against the posts.origin CHECK constraint and persists hermesRunId when
6670
+ // origin === 'hermes'.
6504
6671
  ...origin ? { origin } : {},
6505
- ...hermes_run_id ? { hermes_run_id } : {}
6672
+ ...hermes_run_id ? { hermesRunId: hermes_run_id } : {}
6506
6673
  },
6507
6674
  { timeoutMs: 3e4 }
6508
6675
  );
@@ -6547,12 +6714,15 @@ Created with Social Neuron`;
6547
6714
  }
6548
6715
  }
6549
6716
  if (format === "json") {
6717
+ const structuredContent = asEnvelope3(data);
6550
6718
  return {
6551
- content: [{ type: "text", text: JSON.stringify(asEnvelope2(data), null, 2) }],
6719
+ structuredContent,
6720
+ content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }],
6552
6721
  isError: !data.success
6553
6722
  };
6554
6723
  }
6555
6724
  return {
6725
+ structuredContent: asEnvelope3(data),
6556
6726
  content: [{ type: "text", text: lines.join("\n") }],
6557
6727
  isError: !data.success
6558
6728
  };
@@ -6585,7 +6755,7 @@ Created with Social Neuron`;
6585
6755
  content: [
6586
6756
  {
6587
6757
  type: "text",
6588
- text: JSON.stringify(asEnvelope2({ accounts: [] }), null, 2)
6758
+ text: JSON.stringify(asEnvelope3({ accounts: [] }), null, 2)
6589
6759
  }
6590
6760
  ]
6591
6761
  };
@@ -6608,7 +6778,7 @@ Created with Social Neuron`;
6608
6778
  if (format === "json") {
6609
6779
  return {
6610
6780
  content: [
6611
- { type: "text", text: JSON.stringify(asEnvelope2({ accounts }), null, 2) }
6781
+ { type: "text", text: JSON.stringify(asEnvelope3({ accounts }), null, 2) }
6612
6782
  ]
6613
6783
  };
6614
6784
  }
@@ -6660,10 +6830,10 @@ Created with Social Neuron`;
6660
6830
  const rows = result.posts ?? [];
6661
6831
  if (rows.length === 0) {
6662
6832
  if (format === "json") {
6833
+ const structuredContent2 = asEnvelope3({ posts: [] });
6663
6834
  return {
6664
- content: [
6665
- { type: "text", text: JSON.stringify(asEnvelope2({ posts: [] }), null, 2) }
6666
- ]
6835
+ structuredContent: structuredContent2,
6836
+ content: [{ type: "text", text: JSON.stringify(structuredContent2, null, 2) }]
6667
6837
  };
6668
6838
  }
6669
6839
  return {
@@ -6676,11 +6846,11 @@ Created with Social Neuron`;
6676
6846
  };
6677
6847
  }
6678
6848
  const posts = rows;
6849
+ const structuredContent = asEnvelope3({ posts });
6679
6850
  if (format === "json") {
6680
6851
  return {
6681
- content: [
6682
- { type: "text", text: JSON.stringify(asEnvelope2({ posts }), null, 2) }
6683
- ]
6852
+ structuredContent,
6853
+ content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }]
6684
6854
  };
6685
6855
  }
6686
6856
  const statusIcon = {
@@ -6705,6 +6875,7 @@ Created with Social Neuron`;
6705
6875
  lines.push(line);
6706
6876
  }
6707
6877
  return {
6878
+ structuredContent,
6708
6879
  content: [{ type: "text", text: lines.join("\n") }]
6709
6880
  };
6710
6881
  }
@@ -6788,7 +6959,7 @@ Created with Social Neuron`;
6788
6959
  {
6789
6960
  type: "text",
6790
6961
  text: JSON.stringify(
6791
- asEnvelope2({
6962
+ asEnvelope3({
6792
6963
  slots,
6793
6964
  total_candidates: candidates.length,
6794
6965
  conflicts_avoided: conflictsAvoided
@@ -7077,7 +7248,7 @@ Created with Social Neuron`;
7077
7248
  {
7078
7249
  type: "text",
7079
7250
  text: JSON.stringify(
7080
- asEnvelope2({
7251
+ asEnvelope3({
7081
7252
  dry_run: true,
7082
7253
  plan_id: effectivePlanId,
7083
7254
  approvals: approvalSummary,
@@ -7265,7 +7436,7 @@ Created with Social Neuron`;
7265
7436
  {
7266
7437
  type: "text",
7267
7438
  text: JSON.stringify(
7268
- asEnvelope2({
7439
+ asEnvelope3({
7269
7440
  plan_id: effectivePlanId,
7270
7441
  approvals: approvalSummary,
7271
7442
  posts: results,
@@ -7540,6 +7711,17 @@ function registerMediaTools(server2) {
7540
7711
  projectId: project_id
7541
7712
  };
7542
7713
  } else {
7714
+ if (process.env.MCP_TRANSPORT !== "stdio") {
7715
+ return {
7716
+ content: [
7717
+ {
7718
+ type: "text",
7719
+ text: `Local file paths are not accepted on the cloud MCP server. Pass the bytes via the \`file_data\` parameter (base64, up to 10MB) or supply a public URL via \`source\`.`
7720
+ }
7721
+ ],
7722
+ isError: true
7723
+ };
7724
+ }
7543
7725
  let fileBuffer;
7544
7726
  try {
7545
7727
  fileBuffer = await readFile(src);
@@ -7582,7 +7764,8 @@ function registerMediaTools(server2) {
7582
7764
  const putResp = await fetch(putUrl, {
7583
7765
  method: "PUT",
7584
7766
  headers: { "Content-Type": ct },
7585
- body: fileBuffer
7767
+ // Uint8Array: Buffer no longer satisfies BodyInit under @types/node 26 fetch types
7768
+ body: new Uint8Array(fileBuffer)
7586
7769
  });
7587
7770
  if (!putResp.ok) {
7588
7771
  return {
@@ -7766,7 +7949,7 @@ init_supabase();
7766
7949
  init_edge_function();
7767
7950
  import { z as z5 } from "zod";
7768
7951
  init_version();
7769
- function asEnvelope3(data) {
7952
+ function asEnvelope4(data) {
7770
7953
  return {
7771
7954
  _meta: {
7772
7955
  version: MCP_VERSION,
@@ -7819,22 +8002,20 @@ function registerAnalyticsTools(server2) {
7819
8002
  const rows = result?.rows ?? [];
7820
8003
  if (rows.length === 0) {
7821
8004
  if (format === "json") {
8005
+ const structuredContent = asEnvelope4({
8006
+ platform: platform3 ?? null,
8007
+ days: lookbackDays,
8008
+ totalViews: 0,
8009
+ totalEngagement: 0,
8010
+ postCount: 0,
8011
+ posts: []
8012
+ });
7822
8013
  return {
8014
+ structuredContent,
7823
8015
  content: [
7824
8016
  {
7825
8017
  type: "text",
7826
- text: JSON.stringify(
7827
- asEnvelope3({
7828
- platform: platform3 ?? null,
7829
- days: lookbackDays,
7830
- totalViews: 0,
7831
- totalEngagement: 0,
7832
- postCount: 0,
7833
- posts: []
7834
- }),
7835
- null,
7836
- 2
7837
- )
8018
+ text: JSON.stringify(structuredContent, null, 2)
7838
8019
  }
7839
8020
  ]
7840
8021
  };
@@ -7924,20 +8105,18 @@ function registerAnalyticsTools(server2) {
7924
8105
  lines.push(` Errors: ${errored}`);
7925
8106
  }
7926
8107
  if (format === "json") {
8108
+ const structuredContent = asEnvelope4({
8109
+ success: true,
8110
+ postsProcessed: result.postsProcessed,
8111
+ queued,
8112
+ errored
8113
+ });
7927
8114
  return {
8115
+ structuredContent,
7928
8116
  content: [
7929
8117
  {
7930
8118
  type: "text",
7931
- text: JSON.stringify(
7932
- asEnvelope3({
7933
- success: true,
7934
- postsProcessed: result.postsProcessed,
7935
- queued,
7936
- errored
7937
- }),
7938
- null,
7939
- 2
7940
- )
8119
+ text: JSON.stringify(structuredContent, null, 2)
7941
8120
  }
7942
8121
  ]
7943
8122
  };
@@ -7947,11 +8126,11 @@ function registerAnalyticsTools(server2) {
7947
8126
  );
7948
8127
  }
7949
8128
  function formatAnalytics(summary, days, format) {
8129
+ const structuredContent = asEnvelope4({ ...summary, days });
7950
8130
  if (format === "json") {
7951
8131
  return {
7952
- content: [
7953
- { type: "text", text: JSON.stringify(asEnvelope3({ ...summary, days }), null, 2) }
7954
- ]
8132
+ structuredContent,
8133
+ content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }]
7955
8134
  };
7956
8135
  }
7957
8136
  const lines = [
@@ -7976,6 +8155,7 @@ function formatAnalytics(summary, days, format) {
7976
8155
  }
7977
8156
  }
7978
8157
  return {
8158
+ structuredContent,
7979
8159
  content: [{ type: "text", text: lines.join("\n") }]
7980
8160
  };
7981
8161
  }
@@ -7985,7 +8165,7 @@ init_edge_function();
7985
8165
  init_supabase();
7986
8166
  import { z as z6 } from "zod";
7987
8167
  init_version();
7988
- function asEnvelope4(data) {
8168
+ function asEnvelope5(data) {
7989
8169
  return {
7990
8170
  _meta: {
7991
8171
  version: MCP_VERSION,
@@ -8040,8 +8220,10 @@ function registerBrandTools(server2) {
8040
8220
  };
8041
8221
  }
8042
8222
  if ((response_format || "text") === "json") {
8223
+ const structuredContent = asEnvelope5(data);
8043
8224
  return {
8044
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(data), null, 2) }]
8225
+ structuredContent,
8226
+ content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }]
8045
8227
  };
8046
8228
  }
8047
8229
  const lines = [
@@ -8082,18 +8264,10 @@ function registerBrandTools(server2) {
8082
8264
  },
8083
8265
  async ({ project_id, response_format }) => {
8084
8266
  const projectId = project_id || await getDefaultProjectId();
8085
- if (!projectId) {
8086
- return {
8087
- content: [
8088
- {
8089
- type: "text",
8090
- text: "No project_id provided and no default project is configured."
8091
- }
8092
- ],
8093
- isError: true
8094
- };
8095
- }
8096
- const { data: result, error: efError } = await callEdgeFunction("mcp-data", { action: "brand-profile", projectId });
8267
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
8268
+ action: "brand-profile",
8269
+ ...projectId ? { projectId } : {}
8270
+ });
8097
8271
  if (efError || result && !result.success) {
8098
8272
  return {
8099
8273
  content: [
@@ -8113,20 +8287,24 @@ function registerBrandTools(server2) {
8113
8287
  ]
8114
8288
  };
8115
8289
  }
8290
+ const structuredContent = asEnvelope5(data);
8116
8291
  if ((response_format || "text") === "json") {
8117
8292
  return {
8118
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(data), null, 2) }]
8293
+ structuredContent,
8294
+ content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }]
8119
8295
  };
8120
8296
  }
8297
+ const brandContext = data.brand_context;
8121
8298
  const lines = [
8122
8299
  `Active Brand Profile`,
8123
- `Project: ${projectId}`,
8124
- `Brand Name: ${data.brand_name || data.brand_context?.name || "N/A"}`,
8300
+ `Project: ${data.project_id || projectId || "default"}`,
8301
+ `Brand Name: ${data.brand_name || brandContext?.name || "N/A"}`,
8125
8302
  `Version: ${data.version ?? "N/A"}`,
8126
8303
  `Updated: ${data.updated_at || "N/A"}`,
8127
8304
  `Extraction Method: ${data.extraction_method || "manual"}`
8128
8305
  ];
8129
8306
  return {
8307
+ structuredContent,
8130
8308
  content: [{ type: "text", text: lines.join("\n") }]
8131
8309
  };
8132
8310
  }
@@ -8201,7 +8379,7 @@ function registerBrandTools(server2) {
8201
8379
  };
8202
8380
  if ((response_format || "text") === "json") {
8203
8381
  return {
8204
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(payload), null, 2) }]
8382
+ content: [{ type: "text", text: JSON.stringify(asEnvelope5(payload), null, 2) }]
8205
8383
  };
8206
8384
  }
8207
8385
  return {
@@ -8291,7 +8469,7 @@ Version: ${payload.version ?? "N/A"}`
8291
8469
  };
8292
8470
  if ((response_format || "text") === "json") {
8293
8471
  return {
8294
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(payload), null, 2) }],
8472
+ content: [{ type: "text", text: JSON.stringify(asEnvelope5(payload), null, 2) }],
8295
8473
  isError: false
8296
8474
  };
8297
8475
  }
@@ -9000,7 +9178,7 @@ var PLATFORM_ENUM = [
9000
9178
  "bluesky"
9001
9179
  ];
9002
9180
  var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
9003
- function asEnvelope5(data) {
9181
+ function asEnvelope6(data) {
9004
9182
  return {
9005
9183
  _meta: {
9006
9184
  version: MCP_VERSION,
@@ -9046,19 +9224,17 @@ function registerInsightsTools(server2) {
9046
9224
  }
9047
9225
  if (rows.length === 0) {
9048
9226
  if (format === "json") {
9227
+ const structuredContent2 = asEnvelope6({
9228
+ insights: [],
9229
+ days: lookbackDays,
9230
+ insightType: insight_type ?? null
9231
+ });
9049
9232
  return {
9233
+ structuredContent: structuredContent2,
9050
9234
  content: [
9051
9235
  {
9052
9236
  type: "text",
9053
- text: JSON.stringify(
9054
- asEnvelope5({
9055
- insights: [],
9056
- days: lookbackDays,
9057
- insightType: insight_type ?? null
9058
- }),
9059
- null,
9060
- 2
9061
- )
9237
+ text: JSON.stringify(structuredContent2, null, 2)
9062
9238
  }
9063
9239
  ]
9064
9240
  };
@@ -9073,20 +9249,18 @@ function registerInsightsTools(server2) {
9073
9249
  };
9074
9250
  }
9075
9251
  const insights = rows;
9252
+ const structuredContent = asEnvelope6({
9253
+ insights,
9254
+ days: lookbackDays,
9255
+ insightType: insight_type ?? null
9256
+ });
9076
9257
  if (format === "json") {
9077
9258
  return {
9259
+ structuredContent,
9078
9260
  content: [
9079
9261
  {
9080
9262
  type: "text",
9081
- text: JSON.stringify(
9082
- asEnvelope5({
9083
- insights,
9084
- days: lookbackDays,
9085
- insightType: insight_type ?? null
9086
- }),
9087
- null,
9088
- 2
9089
- )
9263
+ text: JSON.stringify(structuredContent, null, 2)
9090
9264
  }
9091
9265
  ]
9092
9266
  };
@@ -9111,6 +9285,7 @@ function registerInsightsTools(server2) {
9111
9285
  lines.push(line);
9112
9286
  }
9113
9287
  return {
9288
+ structuredContent,
9114
9289
  content: [{ type: "text", text: lines.join("\n") }]
9115
9290
  };
9116
9291
  }
@@ -9204,7 +9379,7 @@ function registerInsightsTools(server2) {
9204
9379
  {
9205
9380
  type: "text",
9206
9381
  text: JSON.stringify(
9207
- asEnvelope5({
9382
+ asEnvelope6({
9208
9383
  platform: platform3 ?? null,
9209
9384
  days: lookbackDays,
9210
9385
  recordsAnalyzed: rows.length,
@@ -9243,7 +9418,7 @@ function registerInsightsTools(server2) {
9243
9418
  init_edge_function();
9244
9419
  init_version();
9245
9420
  import { z as z10 } from "zod";
9246
- function asEnvelope6(data) {
9421
+ function asEnvelope7(data) {
9247
9422
  return {
9248
9423
  _meta: {
9249
9424
  version: MCP_VERSION,
@@ -9298,7 +9473,7 @@ function registerYouTubeAnalyticsTools(server2) {
9298
9473
  {
9299
9474
  type: "text",
9300
9475
  text: JSON.stringify(
9301
- asEnvelope6({ action, startDate: start_date, endDate: end_date, analytics: a }),
9476
+ asEnvelope7({ action, startDate: start_date, endDate: end_date, analytics: a }),
9302
9477
  null,
9303
9478
  2
9304
9479
  )
@@ -9335,7 +9510,7 @@ function registerYouTubeAnalyticsTools(server2) {
9335
9510
  {
9336
9511
  type: "text",
9337
9512
  text: JSON.stringify(
9338
- asEnvelope6({
9513
+ asEnvelope7({
9339
9514
  action,
9340
9515
  startDate: start_date,
9341
9516
  endDate: end_date,
@@ -9364,7 +9539,7 @@ function registerYouTubeAnalyticsTools(server2) {
9364
9539
  {
9365
9540
  type: "text",
9366
9541
  text: JSON.stringify(
9367
- asEnvelope6({
9542
+ asEnvelope7({
9368
9543
  action,
9369
9544
  videoId: video_id,
9370
9545
  startDate: start_date,
@@ -9404,7 +9579,7 @@ function registerYouTubeAnalyticsTools(server2) {
9404
9579
  {
9405
9580
  type: "text",
9406
9581
  text: JSON.stringify(
9407
- asEnvelope6({
9582
+ asEnvelope7({
9408
9583
  action,
9409
9584
  startDate: start_date,
9410
9585
  endDate: end_date,
@@ -9430,7 +9605,7 @@ function registerYouTubeAnalyticsTools(server2) {
9430
9605
  }
9431
9606
  if (format === "json") {
9432
9607
  return {
9433
- content: [{ type: "text", text: JSON.stringify(asEnvelope6(result), null, 2) }]
9608
+ content: [{ type: "text", text: JSON.stringify(asEnvelope7(result), null, 2) }]
9434
9609
  };
9435
9610
  }
9436
9611
  return {
@@ -9445,7 +9620,7 @@ init_edge_function();
9445
9620
  import { z as z11 } from "zod";
9446
9621
  init_supabase();
9447
9622
  init_version();
9448
- function asEnvelope7(data) {
9623
+ function asEnvelope8(data) {
9449
9624
  return {
9450
9625
  _meta: {
9451
9626
  version: MCP_VERSION,
@@ -9490,7 +9665,7 @@ function registerCommentsTools(server2) {
9490
9665
  {
9491
9666
  type: "text",
9492
9667
  text: JSON.stringify(
9493
- asEnvelope7({ comments, nextPageToken: result.nextPageToken ?? null }),
9668
+ asEnvelope8({ comments, nextPageToken: result.nextPageToken ?? null }),
9494
9669
  null,
9495
9670
  2
9496
9671
  )
@@ -9560,7 +9735,7 @@ function registerCommentsTools(server2) {
9560
9735
  const result = data;
9561
9736
  if (format === "json") {
9562
9737
  return {
9563
- content: [{ type: "text", text: JSON.stringify(asEnvelope7(result), null, 2) }]
9738
+ content: [{ type: "text", text: JSON.stringify(asEnvelope8(result), null, 2) }]
9564
9739
  };
9565
9740
  }
9566
9741
  return {
@@ -9612,7 +9787,7 @@ function registerCommentsTools(server2) {
9612
9787
  const result = data;
9613
9788
  if (format === "json") {
9614
9789
  return {
9615
- content: [{ type: "text", text: JSON.stringify(asEnvelope7(result), null, 2) }]
9790
+ content: [{ type: "text", text: JSON.stringify(asEnvelope8(result), null, 2) }]
9616
9791
  };
9617
9792
  }
9618
9793
  return {
@@ -9667,7 +9842,7 @@ function registerCommentsTools(server2) {
9667
9842
  {
9668
9843
  type: "text",
9669
9844
  text: JSON.stringify(
9670
- asEnvelope7({
9845
+ asEnvelope8({
9671
9846
  success: true,
9672
9847
  commentId: comment_id,
9673
9848
  moderationStatus: moderation_status
@@ -9726,7 +9901,7 @@ function registerCommentsTools(server2) {
9726
9901
  content: [
9727
9902
  {
9728
9903
  type: "text",
9729
- text: JSON.stringify(asEnvelope7({ success: true, commentId: comment_id }), null, 2)
9904
+ text: JSON.stringify(asEnvelope8({ success: true, commentId: comment_id }), null, 2)
9730
9905
  }
9731
9906
  ]
9732
9907
  };
@@ -9743,7 +9918,7 @@ init_supabase();
9743
9918
  init_edge_function();
9744
9919
  init_version();
9745
9920
  import { z as z12 } from "zod";
9746
- function asEnvelope8(data) {
9921
+ function asEnvelope9(data) {
9747
9922
  return {
9748
9923
  _meta: {
9749
9924
  version: MCP_VERSION,
@@ -9795,7 +9970,7 @@ function registerIdeationContextTools(server2) {
9795
9970
  const context = result.context;
9796
9971
  if (format === "json") {
9797
9972
  return {
9798
- content: [{ type: "text", text: JSON.stringify(asEnvelope8(context), null, 2) }]
9973
+ content: [{ type: "text", text: JSON.stringify(asEnvelope9(context), null, 2) }]
9799
9974
  };
9800
9975
  }
9801
9976
  const lines = [
@@ -9817,7 +9992,7 @@ function registerIdeationContextTools(server2) {
9817
9992
  init_edge_function();
9818
9993
  import { z as z13 } from "zod";
9819
9994
  init_version();
9820
- function asEnvelope9(data) {
9995
+ function asEnvelope10(data) {
9821
9996
  return {
9822
9997
  _meta: {
9823
9998
  version: MCP_VERSION,
@@ -9854,7 +10029,7 @@ function registerCreditsTools(server2) {
9854
10029
  };
9855
10030
  if ((response_format || "text") === "json") {
9856
10031
  return {
9857
- content: [{ type: "text", text: JSON.stringify(asEnvelope9(payload), null, 2) }]
10032
+ content: [{ type: "text", text: JSON.stringify(asEnvelope10(payload), null, 2) }]
9858
10033
  };
9859
10034
  }
9860
10035
  return {
@@ -9888,7 +10063,7 @@ Monthly used: ${payload.monthlyUsed}` + (payload.monthlyLimit ? ` / ${payload.mo
9888
10063
  };
9889
10064
  if ((response_format || "text") === "json") {
9890
10065
  return {
9891
- content: [{ type: "text", text: JSON.stringify(asEnvelope9(payload), null, 2) }]
10066
+ content: [{ type: "text", text: JSON.stringify(asEnvelope10(payload), null, 2) }]
9892
10067
  };
9893
10068
  }
9894
10069
  return {
@@ -9914,7 +10089,7 @@ init_supabase();
9914
10089
  init_edge_function();
9915
10090
  init_version();
9916
10091
  import { z as z14 } from "zod";
9917
- function asEnvelope10(data) {
10092
+ function asEnvelope11(data) {
9918
10093
  return {
9919
10094
  _meta: {
9920
10095
  version: MCP_VERSION,
@@ -9965,7 +10140,7 @@ function registerLoopSummaryTools(server2) {
9965
10140
  };
9966
10141
  if ((response_format || "text") === "json") {
9967
10142
  return {
9968
- content: [{ type: "text", text: JSON.stringify(asEnvelope10(payload), null, 2) }]
10143
+ content: [{ type: "text", text: JSON.stringify(asEnvelope11(payload), null, 2) }]
9969
10144
  };
9970
10145
  }
9971
10146
  return {
@@ -9989,7 +10164,7 @@ Next Action: ${payload.recommendedNextAction}`
9989
10164
  init_edge_function();
9990
10165
  init_version();
9991
10166
  import { z as z15 } from "zod";
9992
- function asEnvelope11(data) {
10167
+ function asEnvelope12(data) {
9993
10168
  return {
9994
10169
  _meta: {
9995
10170
  version: MCP_VERSION,
@@ -10022,7 +10197,7 @@ function registerUsageTools(server2) {
10022
10197
  content: [
10023
10198
  {
10024
10199
  type: "text",
10025
- text: JSON.stringify(asEnvelope11({ tools: rows, totalCalls, totalCredits }), null, 2)
10200
+ text: JSON.stringify(asEnvelope12({ tools: rows, totalCalls, totalCredits }), null, 2)
10026
10201
  }
10027
10202
  ]
10028
10203
  };
@@ -10063,7 +10238,7 @@ ${"=".repeat(40)}
10063
10238
  init_edge_function();
10064
10239
  init_version();
10065
10240
  import { z as z16 } from "zod";
10066
- function asEnvelope12(data) {
10241
+ function asEnvelope13(data) {
10067
10242
  return {
10068
10243
  _meta: {
10069
10244
  version: MCP_VERSION,
@@ -10103,7 +10278,7 @@ function registerAutopilotTools(server2) {
10103
10278
  content: [
10104
10279
  {
10105
10280
  type: "text",
10106
- text: JSON.stringify(asEnvelope12(configs), null, 2)
10281
+ text: JSON.stringify(asEnvelope13(configs), null, 2)
10107
10282
  }
10108
10283
  ]
10109
10284
  };
@@ -10244,7 +10419,7 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
10244
10419
  content: [
10245
10420
  {
10246
10421
  type: "text",
10247
- text: JSON.stringify(asEnvelope12(statusData), null, 2)
10422
+ text: JSON.stringify(asEnvelope13(statusData), null, 2)
10248
10423
  }
10249
10424
  ]
10250
10425
  };
@@ -10328,7 +10503,7 @@ ${"=".repeat(40)}
10328
10503
  }
10329
10504
  if (format === "json") {
10330
10505
  return {
10331
- content: [{ type: "text", text: JSON.stringify(asEnvelope12(created), null, 2) }]
10506
+ content: [{ type: "text", text: JSON.stringify(asEnvelope13(created), null, 2) }]
10332
10507
  };
10333
10508
  }
10334
10509
  return {
@@ -10351,7 +10526,7 @@ Active: ${is_active}`
10351
10526
  init_edge_function();
10352
10527
  init_version();
10353
10528
  import { z as z17 } from "zod";
10354
- function asEnvelope13(data) {
10529
+ function asEnvelope14(data) {
10355
10530
  return {
10356
10531
  _meta: {
10357
10532
  version: MCP_VERSION,
@@ -10400,7 +10575,7 @@ function registerRecipeTools(server2) {
10400
10575
  content: [
10401
10576
  {
10402
10577
  type: "text",
10403
- text: JSON.stringify(asEnvelope13(recipes))
10578
+ text: JSON.stringify(asEnvelope14(recipes))
10404
10579
  }
10405
10580
  ]
10406
10581
  };
@@ -10469,7 +10644,7 @@ ${lines.join("\n\n")}`
10469
10644
  content: [
10470
10645
  {
10471
10646
  type: "text",
10472
- text: JSON.stringify(asEnvelope13(recipe))
10647
+ text: JSON.stringify(asEnvelope14(recipe))
10473
10648
  }
10474
10649
  ]
10475
10650
  };
@@ -10506,7 +10681,7 @@ ${lines.join("\n\n")}`
10506
10681
  "Execute a recipe template with the provided inputs. This creates a recipe run that processes each step sequentially. Long-running recipes will return a run_id you can check with get_recipe_run_status.",
10507
10682
  {
10508
10683
  slug: z17.string().describe('Recipe slug (e.g., "weekly-instagram-calendar")'),
10509
- inputs: z17.record(z17.unknown()).describe(
10684
+ inputs: z17.record(z17.string(), z17.unknown()).describe(
10510
10685
  "Input values matching the recipe input schema. Use get_recipe_details to see required inputs."
10511
10686
  ),
10512
10687
  response_format: z17.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
@@ -10529,7 +10704,7 @@ ${lines.join("\n\n")}`
10529
10704
  content: [
10530
10705
  {
10531
10706
  type: "text",
10532
- text: JSON.stringify(asEnvelope13(result))
10707
+ text: JSON.stringify(asEnvelope14(result))
10533
10708
  }
10534
10709
  ]
10535
10710
  };
@@ -10585,7 +10760,7 @@ ${result?.message || "Use get_recipe_run_status to check progress."}`
10585
10760
  content: [
10586
10761
  {
10587
10762
  type: "text",
10588
- text: JSON.stringify(asEnvelope13(run))
10763
+ text: JSON.stringify(asEnvelope14(run))
10589
10764
  }
10590
10765
  ]
10591
10766
  };
@@ -10616,17 +10791,114 @@ ${JSON.stringify(run.outputs, null, 2)}
10616
10791
  }
10617
10792
 
10618
10793
  // src/tools/extraction.ts
10619
- init_edge_function();
10620
10794
  import { z as z18 } from "zod";
10621
10795
  init_version();
10622
- function asEnvelope14(data) {
10623
- return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
10624
- }
10625
- function isYouTubeUrl(url) {
10796
+
10797
+ // src/lib/urlExtraction.ts
10798
+ init_edge_function();
10799
+ function classifyYouTubeUrl(url) {
10626
10800
  if (/youtube\.com\/watch|youtu\.be\//.test(url)) return "video";
10627
10801
  if (/youtube\.com\/@/.test(url)) return "channel";
10628
10802
  return false;
10629
10803
  }
10804
+ function unwrapEf(result, label) {
10805
+ if (result.error) return { error: result.error };
10806
+ const env = result.data;
10807
+ if (!env || env.success === false) {
10808
+ return { error: env?.error ?? `No data returned from ${label}` };
10809
+ }
10810
+ return { payload: env.data ?? void 0 };
10811
+ }
10812
+ function segmentsToTranscript(segments) {
10813
+ if (!Array.isArray(segments)) return "";
10814
+ return segments.map((s) => (s?.text ?? "").trim()).filter(Boolean).join(" ");
10815
+ }
10816
+ async function extractUrlContent(url, opts = {}) {
10817
+ const extractType = opts.extractType ?? "auto";
10818
+ const youtubeType = classifyYouTubeUrl(url);
10819
+ if (youtubeType === "video") {
10820
+ const [txRes, metaRes] = await Promise.all([
10821
+ callEdgeFunction(
10822
+ "scrape-youtube",
10823
+ { action: "transcript", videoUrl: url },
10824
+ { timeoutMs: 3e4 }
10825
+ ),
10826
+ callEdgeFunction(
10827
+ "scrape-youtube",
10828
+ { action: "metadata", videoUrl: url },
10829
+ { timeoutMs: 3e4 }
10830
+ )
10831
+ ]);
10832
+ const tx = unwrapEf(txRes, "scrape-youtube transcript");
10833
+ const meta = unwrapEf(metaRes, "scrape-youtube metadata");
10834
+ if (tx.error && meta.error) {
10835
+ return { error: `Failed to extract YouTube video: ${meta.error ?? tx.error}` };
10836
+ }
10837
+ const m = meta.payload;
10838
+ return {
10839
+ content: {
10840
+ source_type: "youtube_video",
10841
+ url,
10842
+ title: m?.title ?? "",
10843
+ description: m?.description ?? "",
10844
+ transcript: segmentsToTranscript(tx.payload?.segments),
10845
+ video_metadata: m ? {
10846
+ views: typeof m.viewCount === "number" ? m.viewCount : 0,
10847
+ likes: typeof m.likes === "number" ? m.likes : 0,
10848
+ duration: typeof m.duration === "number" ? m.duration : 0,
10849
+ tags: Array.isArray(m.tags) ? m.tags : [],
10850
+ channel_name: m.channelName ?? ""
10851
+ } : void 0
10852
+ }
10853
+ };
10854
+ }
10855
+ if (youtubeType === "channel") {
10856
+ const res2 = await callEdgeFunction(
10857
+ "scrape-youtube",
10858
+ { action: "channel_videos", videoUrl: url },
10859
+ { timeoutMs: 3e4 }
10860
+ );
10861
+ const ch = unwrapEf(res2, "scrape-youtube channel");
10862
+ if (ch.error) return { error: `Failed to extract YouTube channel: ${ch.error}` };
10863
+ const videos = ch.payload?.videos ?? [];
10864
+ return {
10865
+ content: {
10866
+ source_type: "youtube_channel",
10867
+ url,
10868
+ title: url,
10869
+ description: videos.length ? `${videos.length} recent videos:
10870
+ ${videos.map((v) => `- ${v.title ?? "Untitled"}`).join("\n")}` : "No videos found for this channel."
10871
+ }
10872
+ };
10873
+ }
10874
+ const res = await callEdgeFunction(
10875
+ "fetch-url-content",
10876
+ { url, extractType: extractType === "auto" ? "product" : extractType },
10877
+ { timeoutMs: 3e4 }
10878
+ );
10879
+ const result = unwrapEf(res, "fetch-url-content");
10880
+ if (result.error || !result.payload) {
10881
+ return { error: `Failed to extract URL content: ${result.error ?? "No data returned"}` };
10882
+ }
10883
+ const info = result.payload;
10884
+ return {
10885
+ content: {
10886
+ source_type: extractType === "product" ? "product" : "article",
10887
+ url,
10888
+ title: info.name ?? "",
10889
+ description: info.description ?? "",
10890
+ features: info.features,
10891
+ benefits: info.benefits,
10892
+ usp: info.usp,
10893
+ suggested_hooks: info.suggestedHookAngles
10894
+ }
10895
+ };
10896
+ }
10897
+
10898
+ // src/tools/extraction.ts
10899
+ function asEnvelope15(data) {
10900
+ return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
10901
+ }
10630
10902
  function formatExtractedContentAsText(content) {
10631
10903
  const lines = [];
10632
10904
  lines.push(`Source: ${content.source_type} (${content.url})`);
@@ -10671,15 +10943,15 @@ ${content.suggested_hooks.map((h) => ` - ${h}`).join("\n")}`
10671
10943
  function registerExtractionTools(server2) {
10672
10944
  server2.tool(
10673
10945
  "extract_url_content",
10674
- "Extract text content from any URL \u2014 YouTube video transcripts, article text, or product page features/benefits/USP. YouTube URLs auto-route to transcript extraction with optional comments. Use before generate_content to repurpose existing content, or before plan_content_week to base a content plan on a source URL.",
10946
+ "Extract text content from any URL \u2014 YouTube video transcript + metadata (title/views/channel), or article text, or product page features/benefits/USP. YouTube URLs auto-route to transcript+metadata extraction; channel URLs return a recent-video list. Use before generate_content to repurpose existing content, or before plan_content_week to base a content plan on a source URL.",
10675
10947
  {
10676
10948
  url: z18.string().url().describe("URL to extract content from"),
10677
- extract_type: z18.enum(["auto", "transcript", "article", "product"]).default("auto").describe("Type of extraction"),
10678
- include_comments: z18.boolean().default(false).describe("Include top comments (YouTube only)"),
10679
- max_results: z18.number().min(1).max(100).default(10).describe("Max comments to include"),
10949
+ extract_type: z18.enum(["auto", "transcript", "article", "product"]).default("auto").describe(
10950
+ "auto = product-style extraction; transcript = YouTube (auto-detected by URL); article = blog/news main text + key points; product = e-commerce features/benefits/USP."
10951
+ ),
10680
10952
  response_format: z18.enum(["text", "json"]).default("text")
10681
10953
  },
10682
- async ({ url, extract_type, include_comments, max_results, response_format }) => {
10954
+ async ({ url, extract_type, response_format }) => {
10683
10955
  const ssrfCheck = await validateUrlForSSRF(url);
10684
10956
  if (!ssrfCheck.isValid) {
10685
10957
  return {
@@ -10687,106 +10959,20 @@ function registerExtractionTools(server2) {
10687
10959
  isError: true
10688
10960
  };
10689
10961
  }
10690
- const youtubeType = isYouTubeUrl(url);
10691
10962
  try {
10692
- let extracted;
10693
- if (youtubeType === "video") {
10694
- const { data, error } = await callEdgeFunction(
10695
- "scrape-youtube",
10696
- {
10697
- url,
10698
- includeComments: include_comments,
10699
- maxComments: max_results
10700
- },
10701
- { timeoutMs: 3e4 }
10702
- );
10703
- if (error || !data) {
10704
- return {
10705
- content: [
10706
- {
10707
- type: "text",
10708
- text: `Failed to extract YouTube video: ${error ?? "No data returned"}`
10709
- }
10710
- ],
10711
- isError: true
10712
- };
10713
- }
10714
- extracted = {
10715
- source_type: "youtube_video",
10716
- url,
10717
- title: data.title ?? "",
10718
- description: data.description ?? "",
10719
- transcript: data.transcript,
10720
- video_metadata: data.metadata ? {
10721
- views: data.metadata.views ?? 0,
10722
- likes: data.metadata.likes ?? 0,
10723
- duration: data.metadata.duration ?? 0,
10724
- tags: data.metadata.tags ?? [],
10725
- channel_name: data.metadata.channelName ?? ""
10726
- } : void 0
10727
- };
10728
- } else if (youtubeType === "channel") {
10729
- const { data, error } = await callEdgeFunction(
10730
- "scrape-youtube",
10731
- {
10732
- url,
10733
- type: "channel"
10734
- },
10735
- { timeoutMs: 3e4 }
10736
- );
10737
- if (error || !data) {
10738
- return {
10739
- content: [
10740
- {
10741
- type: "text",
10742
- text: `Failed to extract YouTube channel: ${error ?? "No data returned"}`
10743
- }
10744
- ],
10745
- isError: true
10746
- };
10747
- }
10748
- extracted = {
10749
- source_type: "youtube_channel",
10750
- url,
10751
- title: data.title ?? "",
10752
- description: data.description ?? ""
10753
- };
10754
- } else {
10755
- const body = { url };
10756
- if (extract_type !== "auto") body.type = extract_type;
10757
- const { data, error } = await callEdgeFunction(
10758
- "fetch-url-content",
10759
- body,
10760
- { timeoutMs: 3e4 }
10761
- );
10762
- if (error || !data) {
10763
- return {
10764
- content: [
10765
- {
10766
- type: "text",
10767
- text: `Failed to extract URL content: ${error ?? "No data returned"}`
10768
- }
10769
- ],
10770
- isError: true
10771
- };
10772
- }
10773
- const sourceType = extract_type === "product" ? "product" : data.type === "product" ? "product" : "article";
10774
- extracted = {
10775
- source_type: sourceType,
10776
- url,
10777
- title: data.title ?? "",
10778
- description: data.description ?? "",
10779
- transcript: data.content,
10780
- features: data.features,
10781
- benefits: data.benefits,
10782
- usp: data.usp,
10783
- suggested_hooks: data.suggestedHooks
10963
+ const { content: extracted, error } = await extractUrlContent(url, {
10964
+ extractType: extract_type
10965
+ });
10966
+ if (error || !extracted) {
10967
+ return {
10968
+ content: [{ type: "text", text: error ?? "No data returned" }],
10969
+ isError: true
10784
10970
  };
10785
10971
  }
10786
10972
  if (response_format === "json") {
10787
10973
  return {
10788
10974
  content: [
10789
- { type: "text", text: JSON.stringify(asEnvelope14(extracted), null, 2) }
10975
+ { type: "text", text: JSON.stringify(asEnvelope15(extracted), null, 2) }
10790
10976
  ],
10791
10977
  isError: false
10792
10978
  };
@@ -10810,7 +10996,7 @@ function registerExtractionTools(server2) {
10810
10996
  init_quality();
10811
10997
  init_version();
10812
10998
  import { z as z19 } from "zod";
10813
- function asEnvelope15(data) {
10999
+ function asEnvelope16(data) {
10814
11000
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
10815
11001
  }
10816
11002
  function registerQualityTools(server2) {
@@ -10863,7 +11049,7 @@ function registerQualityTools(server2) {
10863
11049
  });
10864
11050
  if (response_format === "json") {
10865
11051
  return {
10866
- content: [{ type: "text", text: JSON.stringify(asEnvelope15(result), null, 2) }],
11052
+ content: [{ type: "text", text: JSON.stringify(asEnvelope16(result), null, 2) }],
10867
11053
  isError: false
10868
11054
  };
10869
11055
  }
@@ -10938,7 +11124,7 @@ function registerQualityTools(server2) {
10938
11124
  content: [
10939
11125
  {
10940
11126
  type: "text",
10941
- text: JSON.stringify(asEnvelope15({ posts: postsWithQuality, summary }), null, 2)
11127
+ text: JSON.stringify(asEnvelope16({ posts: postsWithQuality, summary }), null, 2)
10942
11128
  }
10943
11129
  ],
10944
11130
  isError: false
@@ -11273,7 +11459,7 @@ function buildCorrectiveHint(overflowIssues, spellingIssues) {
11273
11459
 
11274
11460
  // src/tools/visualQuality.ts
11275
11461
  init_version();
11276
- function asEnvelope16(data) {
11462
+ function asEnvelope17(data) {
11277
11463
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
11278
11464
  }
11279
11465
  var VALID_STYLES = ["dark-cinematic", "clean-editorial", "bold-authority"];
@@ -11398,7 +11584,7 @@ function registerVisualQualityTools(server2) {
11398
11584
  {
11399
11585
  type: "text",
11400
11586
  text: JSON.stringify(
11401
- asEnvelope16({ ...result, correctiveHint: hint || null }),
11587
+ asEnvelope17({ ...result, correctiveHint: hint || null }),
11402
11588
  null,
11403
11589
  2
11404
11590
  )
@@ -11468,7 +11654,7 @@ function registerVisualQualityTools(server2) {
11468
11654
  return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
11469
11655
  }
11470
11656
  return {
11471
- content: [{ type: "text", text: JSON.stringify(asEnvelope16(data), null, 2) }],
11657
+ content: [{ type: "text", text: JSON.stringify(asEnvelope17(data), null, 2) }],
11472
11658
  isError: false
11473
11659
  };
11474
11660
  }
@@ -11506,7 +11692,7 @@ function extractJsonArray(text) {
11506
11692
  function toRecord(value) {
11507
11693
  return value && typeof value === "object" ? value : void 0;
11508
11694
  }
11509
- function asEnvelope17(data) {
11695
+ function asEnvelope18(data) {
11510
11696
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
11511
11697
  }
11512
11698
  function tomorrowIsoDate() {
@@ -11519,9 +11705,6 @@ function addDaysToIsoDate(dateStr, days) {
11519
11705
  d.setDate(d.getDate() + days);
11520
11706
  return d.toISOString().split("T")[0];
11521
11707
  }
11522
- function isYouTubeUrl2(url) {
11523
- return /youtube\.com\/watch|youtu\.be\/|youtube\.com\/@/.test(url);
11524
- }
11525
11708
  function formatPlanAsText(plan) {
11526
11709
  const lines = [];
11527
11710
  lines.push(`WEEKLY CONTENT PLAN: "${plan.topic}"`);
@@ -11619,20 +11802,22 @@ function registerPlanningTools(server2) {
11619
11802
  let sourceContext = "";
11620
11803
  if (source_url) {
11621
11804
  try {
11622
- const fnName = isYouTubeUrl2(source_url) ? "scrape-youtube" : "fetch-url-content";
11623
- const { data } = await callEdgeFunction(
11624
- fnName,
11625
- { url: source_url },
11626
- { timeoutMs: 3e4 }
11627
- );
11628
- if (data) {
11629
- const parts = [
11630
- data.title ? String(data.title) : "",
11631
- data.description ? String(data.description) : "",
11632
- data.transcript ? String(data.transcript).slice(0, 2e3) : "",
11633
- data.content ? String(data.content).slice(0, 2e3) : ""
11634
- ].filter(Boolean);
11635
- sourceContext = parts.join("\n\n");
11805
+ const ssrf = await validateUrlForSSRF(source_url);
11806
+ if (ssrf.isValid) {
11807
+ const { content } = await extractUrlContent(source_url);
11808
+ if (content) {
11809
+ const parts = [
11810
+ content.title,
11811
+ content.description,
11812
+ content.transcript ? content.transcript.slice(0, 2e3) : "",
11813
+ content.features?.length ? `Features:
11814
+ ${content.features.join("\n")}` : "",
11815
+ content.benefits?.length ? `Benefits:
11816
+ ${content.benefits.join("\n")}` : "",
11817
+ content.usp ?? ""
11818
+ ].filter(Boolean);
11819
+ sourceContext = parts.join("\n\n");
11820
+ }
11636
11821
  }
11637
11822
  } catch {
11638
11823
  }
@@ -11838,13 +12023,16 @@ ${rawText.slice(0, 1e3)}`
11838
12023
  };
11839
12024
  }
11840
12025
  }
12026
+ const structuredContent = asEnvelope18(plan);
11841
12027
  if (response_format === "json") {
11842
12028
  return {
11843
- content: [{ type: "text", text: JSON.stringify(asEnvelope17(plan), null, 2) }],
12029
+ structuredContent,
12030
+ content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }],
11844
12031
  isError: false
11845
12032
  };
11846
12033
  }
11847
12034
  return {
12035
+ structuredContent,
11848
12036
  content: [{ type: "text", text: formatPlanAsText(plan) }],
11849
12037
  isError: false
11850
12038
  };
@@ -11915,7 +12103,7 @@ ${rawText.slice(0, 1e3)}`
11915
12103
  };
11916
12104
  if (response_format === "json") {
11917
12105
  return {
11918
- content: [{ type: "text", text: JSON.stringify(asEnvelope17(result), null, 2) }],
12106
+ content: [{ type: "text", text: JSON.stringify(asEnvelope18(result), null, 2) }],
11919
12107
  isError: false
11920
12108
  };
11921
12109
  }
@@ -11974,7 +12162,7 @@ ${rawText.slice(0, 1e3)}`
11974
12162
  };
11975
12163
  if (response_format === "json") {
11976
12164
  return {
11977
- content: [{ type: "text", text: JSON.stringify(asEnvelope17(payload), null, 2) }],
12165
+ content: [{ type: "text", text: JSON.stringify(asEnvelope18(payload), null, 2) }],
11978
12166
  isError: false
11979
12167
  };
11980
12168
  }
@@ -12046,7 +12234,7 @@ ${rawText.slice(0, 1e3)}`
12046
12234
  };
12047
12235
  if (response_format === "json") {
12048
12236
  return {
12049
- content: [{ type: "text", text: JSON.stringify(asEnvelope17(payload), null, 2) }],
12237
+ content: [{ type: "text", text: JSON.stringify(asEnvelope18(payload), null, 2) }],
12050
12238
  isError: false
12051
12239
  };
12052
12240
  }
@@ -12099,7 +12287,7 @@ ${rawText.slice(0, 1e3)}`
12099
12287
  };
12100
12288
  if (response_format === "json") {
12101
12289
  return {
12102
- content: [{ type: "text", text: JSON.stringify(asEnvelope17(payload), null, 2) }],
12290
+ content: [{ type: "text", text: JSON.stringify(asEnvelope18(payload), null, 2) }],
12103
12291
  isError: false
12104
12292
  };
12105
12293
  }
@@ -12121,7 +12309,7 @@ init_edge_function();
12121
12309
  init_supabase();
12122
12310
  init_version();
12123
12311
  import { z as z22 } from "zod";
12124
- function asEnvelope18(data) {
12312
+ function asEnvelope19(data) {
12125
12313
  return {
12126
12314
  _meta: {
12127
12315
  version: MCP_VERSION,
@@ -12196,7 +12384,7 @@ function registerPlanApprovalTools(server2) {
12196
12384
  };
12197
12385
  if ((response_format || "text") === "json") {
12198
12386
  return {
12199
- content: [{ type: "text", text: JSON.stringify(asEnvelope18(payload), null, 2) }],
12387
+ content: [{ type: "text", text: JSON.stringify(asEnvelope19(payload), null, 2) }],
12200
12388
  isError: false
12201
12389
  };
12202
12390
  }
@@ -12248,7 +12436,7 @@ function registerPlanApprovalTools(server2) {
12248
12436
  };
12249
12437
  if ((response_format || "text") === "json") {
12250
12438
  return {
12251
- content: [{ type: "text", text: JSON.stringify(asEnvelope18(payload), null, 2) }],
12439
+ content: [{ type: "text", text: JSON.stringify(asEnvelope19(payload), null, 2) }],
12252
12440
  isError: false
12253
12441
  };
12254
12442
  }
@@ -12326,7 +12514,7 @@ function registerPlanApprovalTools(server2) {
12326
12514
  }
12327
12515
  if ((response_format || "text") === "json") {
12328
12516
  return {
12329
- content: [{ type: "text", text: JSON.stringify(asEnvelope18(data), null, 2) }],
12517
+ content: [{ type: "text", text: JSON.stringify(asEnvelope19(data), null, 2) }],
12330
12518
  isError: false
12331
12519
  };
12332
12520
  }
@@ -12346,7 +12534,194 @@ function registerPlanApprovalTools(server2) {
12346
12534
  // src/tools/discovery.ts
12347
12535
  init_tool_catalog();
12348
12536
  import { z as z23 } from "zod";
12537
+ init_supabase();
12538
+ init_request_context();
12539
+ var KNOWLEDGE_BASE_URL = "https://socialneuron.com/for-developers";
12540
+ var KNOWLEDGE_SEARCH_LIMIT = 10;
12541
+ var SearchOutputSchema = {
12542
+ results: z23.array(
12543
+ z23.object({
12544
+ id: z23.string(),
12545
+ title: z23.string(),
12546
+ url: z23.string().url()
12547
+ })
12548
+ )
12549
+ };
12550
+ var FetchOutputSchema = {
12551
+ id: z23.string(),
12552
+ title: z23.string(),
12553
+ text: z23.string(),
12554
+ url: z23.string().url(),
12555
+ metadata: z23.record(z23.string(), z23.string()).optional()
12556
+ };
12557
+ var STATIC_KNOWLEDGE_DOCUMENTS = [
12558
+ {
12559
+ id: "overview",
12560
+ title: "Social Neuron MCP Overview",
12561
+ url: `${KNOWLEDGE_BASE_URL}#mcp`,
12562
+ text: [
12563
+ "Social Neuron exposes an MCP server for creating, scheduling, and optimizing social content.",
12564
+ "Use the hosted streamable HTTP endpoint at https://mcp.socialneuron.com/mcp for ChatGPT, Claude, and other remote MCP clients.",
12565
+ "The npm package provides stdio transport for local tools and Codex-style workflows."
12566
+ ].join("\n"),
12567
+ metadata: { source: "public-developer-docs", category: "overview" }
12568
+ },
12569
+ {
12570
+ id: "integrations",
12571
+ title: "Supported Social Integrations",
12572
+ url: "https://socialneuron.com/integrations",
12573
+ text: [
12574
+ "Social Neuron tracks platform availability on the integrations page.",
12575
+ "YouTube, TikTok, Instagram, LinkedIn, X, and Facebook are live for supported posting workflows.",
12576
+ "Threads and Bluesky are supported surfaces where live availability depends on the current integration status."
12577
+ ].join("\n"),
12578
+ metadata: { source: "public-integrations-page", category: "integrations" }
12579
+ },
12580
+ {
12581
+ id: "chatgpt-connector",
12582
+ title: "ChatGPT Connector Setup",
12583
+ url: `${KNOWLEDGE_BASE_URL}#chatgpt`,
12584
+ text: [
12585
+ "In ChatGPT Developer Mode, create an MCP app using https://mcp.socialneuron.com/mcp as the MCP URL.",
12586
+ "The connector uses OAuth for account linking and tool scopes for read, write, distribution, analytics, comments, and autopilot access.",
12587
+ "Publishing tools should be treated as externally visible actions and require the distribute scope."
12588
+ ].join("\n"),
12589
+ metadata: { source: "public-developer-docs", category: "chatgpt" }
12590
+ },
12591
+ {
12592
+ id: "privacy-security",
12593
+ title: "Connector Security and Data Minimization",
12594
+ url: `${KNOWLEDGE_BASE_URL}#security`,
12595
+ text: [
12596
+ "Social Neuron MCP tools enforce OAuth or API-key scopes before tool execution.",
12597
+ "Read-only discovery tools expose public product and tool metadata, not private account content.",
12598
+ "User-owned content and analytics require authenticated scopes and organization or project membership checks in backend functions."
12599
+ ].join("\n"),
12600
+ metadata: { source: "public-developer-docs", category: "security" }
12601
+ }
12602
+ ];
12603
+ function toolKnowledgeDocument(tool) {
12604
+ const lines = [
12605
+ `Tool: ${tool.name}`,
12606
+ `Description: ${tool.description}`,
12607
+ `Module: ${tool.module}`,
12608
+ `Required scope: ${tool.scope}`
12609
+ ];
12610
+ if (tool.task_intent) lines.push(`Task intent: ${tool.task_intent}`);
12611
+ if (tool.use_when) lines.push(`Use when: ${tool.use_when}`);
12612
+ if (tool.avoid_when) lines.push(`Avoid when: ${tool.avoid_when}`);
12613
+ if (tool.next_tools?.length) lines.push(`Common next tools: ${tool.next_tools.join(", ")}`);
12614
+ return {
12615
+ id: `tool:${tool.name}`,
12616
+ title: `MCP tool: ${tool.name}`,
12617
+ url: `${KNOWLEDGE_BASE_URL}#tool-${tool.name}`,
12618
+ text: lines.join("\n"),
12619
+ metadata: {
12620
+ source: "mcp-tool-catalog",
12621
+ category: "tool",
12622
+ module: tool.module,
12623
+ scope: tool.scope
12624
+ }
12625
+ };
12626
+ }
12627
+ function getKnowledgeDocuments() {
12628
+ return [
12629
+ ...STATIC_KNOWLEDGE_DOCUMENTS,
12630
+ ...TOOL_CATALOG.filter((t) => !t.internal).map(toolKnowledgeDocument)
12631
+ ];
12632
+ }
12633
+ function tokenize(input) {
12634
+ return input.toLowerCase().split(/[^a-z0-9:_-]+/).map((token) => token.trim()).filter(Boolean);
12635
+ }
12636
+ function scoreDocument(queryTokens, doc) {
12637
+ const title = doc.title.toLowerCase();
12638
+ const text = doc.text.toLowerCase();
12639
+ const metadata = JSON.stringify(doc.metadata ?? {}).toLowerCase();
12640
+ return queryTokens.reduce((score, token) => {
12641
+ if (doc.id.toLowerCase() === token) return score + 12;
12642
+ if (doc.id.toLowerCase().includes(token)) score += 8;
12643
+ if (title.includes(token)) score += 5;
12644
+ if (text.includes(token)) score += 2;
12645
+ if (metadata.includes(token)) score += 1;
12646
+ return score;
12647
+ }, 0);
12648
+ }
12649
+ function searchKnowledge(query) {
12650
+ const docs = getKnowledgeDocuments();
12651
+ const tokens = tokenize(query);
12652
+ if (tokens.length === 0) {
12653
+ return docs.slice(0, KNOWLEDGE_SEARCH_LIMIT);
12654
+ }
12655
+ return docs.map((doc) => ({ doc, score: scoreDocument(tokens, doc) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score || a.doc.title.localeCompare(b.doc.title)).slice(0, KNOWLEDGE_SEARCH_LIMIT).map(({ doc }) => doc);
12656
+ }
12349
12657
  function registerDiscoveryTools(server2) {
12658
+ server2.registerTool(
12659
+ "search",
12660
+ {
12661
+ title: "Search Social Neuron Knowledge",
12662
+ description: "Search public Social Neuron product, integration, developer, and MCP tool knowledge. Uses the standard ChatGPT MCP search schema and never returns private account content.",
12663
+ inputSchema: {
12664
+ query: z23.string().describe("Search query.")
12665
+ },
12666
+ outputSchema: SearchOutputSchema,
12667
+ annotations: {
12668
+ readOnlyHint: true,
12669
+ destructiveHint: false,
12670
+ idempotentHint: true,
12671
+ openWorldHint: false
12672
+ }
12673
+ },
12674
+ async ({ query }) => {
12675
+ const structuredContent = {
12676
+ results: searchKnowledge(query).map((doc) => ({
12677
+ id: doc.id,
12678
+ title: doc.title,
12679
+ url: doc.url
12680
+ }))
12681
+ };
12682
+ return {
12683
+ structuredContent,
12684
+ content: [{ type: "text", text: JSON.stringify(structuredContent) }]
12685
+ };
12686
+ }
12687
+ );
12688
+ server2.registerTool(
12689
+ "fetch",
12690
+ {
12691
+ title: "Fetch Social Neuron Knowledge",
12692
+ description: "Fetch a public Social Neuron knowledge document by ID. Use IDs returned by the search tool.",
12693
+ inputSchema: {
12694
+ id: z23.string().describe("Document ID returned by search.")
12695
+ },
12696
+ outputSchema: FetchOutputSchema,
12697
+ annotations: {
12698
+ readOnlyHint: true,
12699
+ destructiveHint: false,
12700
+ idempotentHint: true,
12701
+ openWorldHint: false
12702
+ }
12703
+ },
12704
+ async ({ id }) => {
12705
+ const doc = getKnowledgeDocuments().find((candidate) => candidate.id === id);
12706
+ if (!doc) {
12707
+ return {
12708
+ content: [{ type: "text", text: `Document not found: ${id}` }],
12709
+ isError: true
12710
+ };
12711
+ }
12712
+ const structuredContent = {
12713
+ id: doc.id,
12714
+ title: doc.title,
12715
+ text: doc.text,
12716
+ url: doc.url,
12717
+ ...doc.metadata ? { metadata: doc.metadata } : {}
12718
+ };
12719
+ return {
12720
+ structuredContent,
12721
+ content: [{ type: "text", text: JSON.stringify(structuredContent) }]
12722
+ };
12723
+ }
12724
+ );
12350
12725
  server2.tool(
12351
12726
  "search_tools",
12352
12727
  'Search available tools by name, description, module, or scope. Use "name" detail (~50 tokens) for quick lookup, "summary" (~500 tokens) for descriptions, "full" for complete input schemas. Start here if unsure which tool to call \u2014 filter by module (e.g. "planning", "content", "analytics") to narrow results.',
@@ -12356,13 +12731,20 @@ function registerDiscoveryTools(server2) {
12356
12731
  scope: z23.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
12357
12732
  detail: z23.enum(["name", "summary", "full"]).default("summary").describe(
12358
12733
  'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
12734
+ ),
12735
+ available_only: z23.boolean().default(false).describe(
12736
+ "When true, only return tools allowed by the current API key/OAuth scopes. Use this after a permission_denied error."
12359
12737
  )
12360
12738
  },
12361
- async ({ query, module, scope, detail }) => {
12739
+ async ({ query, module, scope, detail, available_only }) => {
12740
+ const currentScopes = getRequestScopes() ?? getAuthenticatedScopes();
12741
+ const hasKnownScopes = currentScopes.length > 0;
12742
+ const isAvailable = (tool) => !hasKnownScopes || hasScope(currentScopes, tool.scope);
12362
12743
  let results = [...TOOL_CATALOG];
12363
12744
  if (query) {
12364
12745
  results = searchTools(query);
12365
12746
  }
12747
+ results = results.filter((t) => !t.internal);
12366
12748
  if (module) {
12367
12749
  const moduleTools = getToolsByModule(module);
12368
12750
  results = results.filter((t) => moduleTools.some((mt) => mt.name === t.name));
@@ -12371,24 +12753,48 @@ function registerDiscoveryTools(server2) {
12371
12753
  const scopeTools = getToolsByScope(scope);
12372
12754
  results = results.filter((t) => scopeTools.some((st) => st.name === t.name));
12373
12755
  }
12756
+ if (available_only) {
12757
+ results = results.filter(isAvailable);
12758
+ }
12759
+ const unavailableCount = hasKnownScopes ? results.filter((t) => !isAvailable(t)).length : 0;
12374
12760
  let output;
12375
12761
  switch (detail) {
12376
12762
  case "name":
12377
12763
  output = results.map((t) => t.name);
12378
12764
  break;
12379
12765
  case "summary":
12380
- output = results.map((t) => ({ name: t.name, description: t.description }));
12766
+ output = results.map((t) => ({
12767
+ name: t.name,
12768
+ description: t.description,
12769
+ ...hasKnownScopes || scope ? { required_scope: t.scope } : {},
12770
+ ...hasKnownScopes ? { available: isAvailable(t) } : {},
12771
+ ...t.task_intent ? { task_intent: t.task_intent } : {},
12772
+ ...t.use_when ? { use_when: t.use_when } : {},
12773
+ ...t.avoid_when ? { avoid_when: t.avoid_when } : {}
12774
+ }));
12381
12775
  break;
12382
12776
  case "full":
12383
12777
  default:
12384
- output = results;
12778
+ output = results.map((tool) => ({
12779
+ ...tool,
12780
+ required_scope: tool.scope,
12781
+ ...hasKnownScopes ? { available: isAvailable(tool) } : {}
12782
+ }));
12385
12783
  break;
12386
12784
  }
12387
12785
  return {
12388
12786
  content: [
12389
12787
  {
12390
12788
  type: "text",
12391
- text: JSON.stringify({ toolCount: results.length, tools: output }, null, 2)
12789
+ text: JSON.stringify(
12790
+ {
12791
+ toolCount: results.length,
12792
+ ...hasKnownScopes ? { scopes: { available: currentScopes, unavailable_matches: unavailableCount } } : {},
12793
+ tools: output
12794
+ },
12795
+ null,
12796
+ detail === "full" ? 2 : 0
12797
+ )
12392
12798
  }
12393
12799
  ]
12394
12800
  };
@@ -12403,7 +12809,7 @@ import { randomUUID as randomUUID3 } from "node:crypto";
12403
12809
  init_supabase();
12404
12810
  init_quality();
12405
12811
  init_version();
12406
- function asEnvelope19(data) {
12812
+ function asEnvelope20(data) {
12407
12813
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
12408
12814
  }
12409
12815
  var PLATFORM_ENUM2 = z24.enum([
@@ -12418,6 +12824,7 @@ var PLATFORM_ENUM2 = z24.enum([
12418
12824
  ]);
12419
12825
  var BASE_PLAN_CREDITS = 15;
12420
12826
  var SOURCE_EXTRACTION_CREDITS = 5;
12827
+ var SCHEDULE_POST_CREDITS = 1;
12421
12828
  function registerPipelineTools(server2) {
12422
12829
  server2.tool(
12423
12830
  "check_pipeline_readiness",
@@ -12447,6 +12854,7 @@ function registerPipelineTools(server2) {
12447
12854
  throw new Error(readinessError ?? "No response from mcp-data");
12448
12855
  }
12449
12856
  const credits = readiness.credits;
12857
+ const isUnlimited = readiness.is_unlimited === true;
12450
12858
  const connectedPlatforms = readiness.connected_platforms;
12451
12859
  const missingPlatforms = readiness.missing_platforms;
12452
12860
  const hasBrand = readiness.has_brand;
@@ -12455,7 +12863,7 @@ function registerPipelineTools(server2) {
12455
12863
  const insightsFresh = readiness.insights_fresh;
12456
12864
  const blockers = [];
12457
12865
  const warnings = [];
12458
- if (credits < estimatedCost) {
12866
+ if (!isUnlimited && credits < estimatedCost) {
12459
12867
  blockers.push(`Insufficient credits: ${credits} available, ~${estimatedCost} needed`);
12460
12868
  }
12461
12869
  if (missingPlatforms.length > 0) {
@@ -12494,7 +12902,7 @@ function registerPipelineTools(server2) {
12494
12902
  };
12495
12903
  if (format === "json") {
12496
12904
  return {
12497
- content: [{ type: "text", text: JSON.stringify(asEnvelope19(result), null, 2) }]
12905
+ content: [{ type: "text", text: JSON.stringify(asEnvelope20(result), null, 2) }]
12498
12906
  };
12499
12907
  }
12500
12908
  const lines = [];
@@ -12533,7 +12941,7 @@ function registerPipelineTools(server2) {
12533
12941
  );
12534
12942
  server2.tool(
12535
12943
  "run_content_pipeline",
12536
- "Run the full content pipeline: research trends \u2192 generate plan \u2192 quality check \u2192 auto-approve \u2192 schedule posts. Chains all stages in one call for maximum efficiency. Set dry_run=true to preview the plan without publishing. Check check_pipeline_readiness first to verify credits, OAuth, and brand profile are ready.",
12944
+ "Run the full content pipeline: research trends \u2192 generate plan \u2192 quality check \u2192 auto-approve \u2192 schedule posts. Chains all stages in one call for maximum efficiency. Set dry_run=true to preview the plan without publishing. To schedule posts, set schedule_confirmed=true after the user explicitly approves publishing. Check check_pipeline_readiness first to verify credits, OAuth, and brand profile are ready.",
12537
12945
  {
12538
12946
  project_id: z24.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
12539
12947
  topic: z24.string().optional().describe("Content topic (required if no source_url)"),
@@ -12549,6 +12957,9 @@ function registerPipelineTools(server2) {
12549
12957
  ),
12550
12958
  max_credits: z24.number().optional().describe("Credit budget cap"),
12551
12959
  dry_run: z24.boolean().default(false).describe("If true, skip scheduling and return plan only"),
12960
+ schedule_confirmed: z24.boolean().default(false).describe(
12961
+ "Required to schedule posts. Set true only after explicit user confirmation to publish/schedule."
12962
+ ),
12552
12963
  skip_stages: z24.array(z24.enum(["research", "quality", "schedule"])).optional().describe("Stages to skip"),
12553
12964
  response_format: z24.enum(["text", "json"]).default("json")
12554
12965
  },
@@ -12563,6 +12974,7 @@ function registerPipelineTools(server2) {
12563
12974
  auto_approve_threshold,
12564
12975
  max_credits,
12565
12976
  dry_run,
12977
+ schedule_confirmed,
12566
12978
  skip_stages,
12567
12979
  response_format
12568
12980
  }) => {
@@ -12578,6 +12990,29 @@ function registerPipelineTools(server2) {
12578
12990
  };
12579
12991
  }
12580
12992
  const skipSet = new Set(skip_stages ?? []);
12993
+ const schedulingRequested = !dry_run && !skipSet.has("schedule");
12994
+ if (schedulingRequested && !schedule_confirmed) {
12995
+ return {
12996
+ content: [
12997
+ {
12998
+ type: "text",
12999
+ text: 'Scheduling requires explicit confirmation. Re-run with schedule_confirmed=true after the user approves publishing, or set dry_run=true / skip_stages=["schedule"].'
13000
+ }
13001
+ ],
13002
+ isError: true
13003
+ };
13004
+ }
13005
+ if (schedulingRequested && skipSet.has("quality")) {
13006
+ return {
13007
+ content: [
13008
+ {
13009
+ type: "text",
13010
+ text: "Scheduling cannot run when the quality stage is skipped."
13011
+ }
13012
+ ],
13013
+ isError: true
13014
+ };
13015
+ }
12581
13016
  try {
12582
13017
  const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
12583
13018
  const estimatedCost = BASE_PLAN_CREDITS + (source_url ? SOURCE_EXTRACTION_CREDITS : 0);
@@ -12591,8 +13026,9 @@ function registerPipelineTools(server2) {
12591
13026
  { timeoutMs: 1e4 }
12592
13027
  );
12593
13028
  const availableCredits = budgetData?.credits ?? 0;
13029
+ const isUnlimited = budgetData?.is_unlimited === true;
12594
13030
  const creditLimit = max_credits ?? availableCredits;
12595
- if (availableCredits < estimatedCost) {
13031
+ if (!isUnlimited && availableCredits < estimatedCost) {
12596
13032
  return {
12597
13033
  content: [
12598
13034
  {
@@ -12619,6 +13055,7 @@ function registerPipelineTools(server2) {
12619
13055
  approval_mode,
12620
13056
  auto_approve_threshold,
12621
13057
  dry_run,
13058
+ schedule_confirmed,
12622
13059
  skip_stages: skip_stages ?? []
12623
13060
  },
12624
13061
  current_stage: "planning",
@@ -12684,7 +13121,9 @@ function registerPipelineTools(server2) {
12684
13121
  stagesCompleted.push("planning");
12685
13122
  const rawText = String(planData.text ?? planData.content ?? "");
12686
13123
  const postsArray = extractJsonArray(rawText);
12687
- const posts = (postsArray ?? []).map((p) => ({
13124
+ const requestedPlatformSet = new Set(platforms);
13125
+ const maxPosts = platforms.length * days * posts_per_day;
13126
+ const parsedPosts = (postsArray ?? []).map((p) => ({
12688
13127
  id: String(p.id ?? randomUUID3().slice(0, 8)),
12689
13128
  day: Number(p.day ?? 1),
12690
13129
  date: String(p.date ?? ""),
@@ -12698,6 +13137,23 @@ function registerPipelineTools(server2) {
12698
13137
  visual_direction: p.visual_direction ? String(p.visual_direction) : void 0,
12699
13138
  media_type: p.media_type ? String(p.media_type) : void 0
12700
13139
  }));
13140
+ const platformFilteredPosts = parsedPosts.filter(
13141
+ (post) => requestedPlatformSet.has(post.platform)
13142
+ );
13143
+ const posts = platformFilteredPosts.slice(0, maxPosts);
13144
+ if (parsedPosts.length > maxPosts) {
13145
+ errors.push({
13146
+ stage: "planning",
13147
+ message: `AI returned ${parsedPosts.length} posts; truncated to ${maxPosts}.`
13148
+ });
13149
+ }
13150
+ const invalidPlatformCount = parsedPosts.length - platformFilteredPosts.length;
13151
+ if (invalidPlatformCount > 0) {
13152
+ errors.push({
13153
+ stage: "planning",
13154
+ message: `Dropped ${invalidPlatformCount} post(s) with unrequested or invalid platform.`
13155
+ });
13156
+ }
12701
13157
  let postsApproved = 0;
12702
13158
  let postsFlagged = 0;
12703
13159
  if (!skipSet.has("quality")) {
@@ -12818,21 +13274,27 @@ function registerPipelineTools(server2) {
12818
13274
  let postsScheduled = 0;
12819
13275
  if (!dry_run && !skipSet.has("schedule") && postsApproved > 0) {
12820
13276
  const approvedPosts = posts.filter((p) => p.status === "approved");
13277
+ const scheduleBase = /* @__PURE__ */ new Date();
13278
+ scheduleBase.setDate(scheduleBase.getDate() + 1);
13279
+ const scheduleBaseMs = scheduleBase.getTime();
12821
13280
  for (const post of approvedPosts) {
12822
13281
  if (creditsUsed >= creditLimit) {
12823
13282
  errors.push({ stage: "schedule", message: "Credit limit reached" });
12824
13283
  break;
12825
13284
  }
13285
+ const scheduledAt = post.schedule_at ?? new Date(scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5).toISOString();
12826
13286
  try {
12827
13287
  const { error: schedError } = await callEdgeFunction(
12828
13288
  "schedule-post",
12829
13289
  {
12830
- platform: post.platform,
13290
+ platforms: [post.platform],
12831
13291
  caption: post.caption,
12832
13292
  title: post.title,
12833
13293
  hashtags: post.hashtags,
12834
- media_url: post.media_url,
12835
- scheduled_at: post.schedule_at,
13294
+ mediaUrl: post.media_url,
13295
+ scheduledAt,
13296
+ planId,
13297
+ idempotencyKey: `pipeline-${planId}-${post.id}`,
12836
13298
  ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
12837
13299
  },
12838
13300
  { timeoutMs: 15e3 }
@@ -12844,6 +13306,7 @@ function registerPipelineTools(server2) {
12844
13306
  });
12845
13307
  } else {
12846
13308
  postsScheduled++;
13309
+ creditsUsed += SCHEDULE_POST_CREDITS;
12847
13310
  }
12848
13311
  } catch (schedErr) {
12849
13312
  errors.push({
@@ -12898,7 +13361,7 @@ function registerPipelineTools(server2) {
12898
13361
  if (response_format === "json") {
12899
13362
  return {
12900
13363
  content: [
12901
- { type: "text", text: JSON.stringify(asEnvelope19(resultPayload), null, 2) }
13364
+ { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
12902
13365
  ]
12903
13366
  };
12904
13367
  }
@@ -12979,7 +13442,7 @@ function registerPipelineTools(server2) {
12979
13442
  }
12980
13443
  if (format === "json") {
12981
13444
  return {
12982
- content: [{ type: "text", text: JSON.stringify(asEnvelope19(data), null, 2) }]
13445
+ content: [{ type: "text", text: JSON.stringify(asEnvelope20(data), null, 2) }]
12983
13446
  };
12984
13447
  }
12985
13448
  const lines = [];
@@ -13113,7 +13576,7 @@ function registerPipelineTools(server2) {
13113
13576
  if (response_format === "json") {
13114
13577
  return {
13115
13578
  content: [
13116
- { type: "text", text: JSON.stringify(asEnvelope19(resultPayload), null, 2) }
13579
+ { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
13117
13580
  ]
13118
13581
  };
13119
13582
  }
@@ -13163,7 +13626,7 @@ function buildPlanPrompt(topic, platforms, days, postsPerDay, sourceUrl) {
13163
13626
  init_edge_function();
13164
13627
  import { z as z25 } from "zod";
13165
13628
  init_version();
13166
- function asEnvelope20(data) {
13629
+ function asEnvelope21(data) {
13167
13630
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
13168
13631
  }
13169
13632
  function registerSuggestTools(server2) {
@@ -13271,7 +13734,7 @@ function registerSuggestTools(server2) {
13271
13734
  if (format === "json") {
13272
13735
  return {
13273
13736
  content: [
13274
- { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
13737
+ { type: "text", text: JSON.stringify(asEnvelope21(resultPayload), null, 2) }
13275
13738
  ]
13276
13739
  };
13277
13740
  }
@@ -13408,7 +13871,7 @@ function detectAnomalies(currentData, previousData, sensitivity = "medium", aver
13408
13871
 
13409
13872
  // src/tools/digest.ts
13410
13873
  init_version();
13411
- function asEnvelope21(data) {
13874
+ function asEnvelope22(data) {
13412
13875
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
13413
13876
  }
13414
13877
  var PLATFORM_ENUM3 = z26.enum([
@@ -13577,7 +14040,7 @@ function registerDigestTools(server2) {
13577
14040
  };
13578
14041
  if (format === "json") {
13579
14042
  return {
13580
- content: [{ type: "text", text: JSON.stringify(asEnvelope21(digest), null, 2) }]
14043
+ content: [{ type: "text", text: JSON.stringify(asEnvelope22(digest), null, 2) }]
13581
14044
  };
13582
14045
  }
13583
14046
  const lines = [];
@@ -13676,7 +14139,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
13676
14139
  if (format === "json") {
13677
14140
  return {
13678
14141
  content: [
13679
- { type: "text", text: JSON.stringify(asEnvelope21(resultPayload), null, 2) }
14142
+ { type: "text", text: JSON.stringify(asEnvelope22(resultPayload), null, 2) }
13680
14143
  ]
13681
14144
  };
13682
14145
  }
@@ -13725,18 +14188,129 @@ var WEIGHTS = {
13725
14188
  structuralPatterns: 0.05
13726
14189
  };
13727
14190
  function norm(content) {
13728
- return content.toLowerCase().replace(/[^a-z0-9\s]/g, " ");
14191
+ return content.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
13729
14192
  }
13730
14193
  function findMatches(content, terms) {
13731
14194
  const n = norm(content);
13732
- return terms.filter((t) => n.includes(t.toLowerCase()));
14195
+ return terms.filter((t) => {
14196
+ const nt = norm(t);
14197
+ return nt.length > 0 && n.includes(nt);
14198
+ });
13733
14199
  }
13734
14200
  function findMissing(content, terms) {
13735
14201
  const n = norm(content);
13736
- return terms.filter((t) => !n.includes(t.toLowerCase()));
14202
+ return terms.filter((t) => {
14203
+ const nt = norm(t);
14204
+ return nt.length > 0 && !n.includes(nt);
14205
+ });
14206
+ }
14207
+ function asStringArray(v) {
14208
+ if (Array.isArray(v)) return v.map((x) => String(x ?? "").trim()).filter(Boolean);
14209
+ if (typeof v === "string") {
14210
+ return v.split(/[,;\n]/).map((s) => s.trim()).filter(Boolean);
14211
+ }
14212
+ return [];
14213
+ }
14214
+ function extractAtomicTerms(entries) {
14215
+ const out = [];
14216
+ for (const raw of entries) {
14217
+ const e = (raw || "").trim();
14218
+ if (!e) continue;
14219
+ const quoted = e.match(/['"“”‘’]([^'"“”‘’]{2,40})['"“”‘’]/g);
14220
+ if (quoted) {
14221
+ for (const q of quoted) out.push(q.replace(/['"“”‘’]/g, "").trim());
14222
+ continue;
14223
+ }
14224
+ const words = e.split(/\s+/);
14225
+ const isGuidancePhrase = words.length > 2 && /\b(tone|voice|style|approach|language)\b/i.test(e);
14226
+ if (!e.includes("(") && words.length <= 3 && !isGuidancePhrase) out.push(e);
14227
+ }
14228
+ return Array.from(new Set(out.map((t) => t.trim()).filter(Boolean)));
14229
+ }
14230
+ function hasField(obj, key) {
14231
+ return Object.prototype.hasOwnProperty.call(obj, key) && obj[key] != null;
14232
+ }
14233
+ function unionTerms(existing, incoming) {
14234
+ const out = [];
14235
+ const seen = /* @__PURE__ */ new Set();
14236
+ for (const t of [...existing, ...incoming]) {
14237
+ if (t && !seen.has(t)) {
14238
+ seen.add(t);
14239
+ out.push(t);
14240
+ }
14241
+ }
14242
+ return out;
14243
+ }
14244
+ function mergeVoiceVocabulary(resolved, raw) {
14245
+ const voice = raw.voice;
14246
+ if (!voice || typeof voice !== "object") return resolved;
14247
+ const vp = asStringArray(voice.preferredTerms);
14248
+ const vb = asStringArray(voice.bannedTerms);
14249
+ if (!vp.length && !vb.length) return resolved;
14250
+ const existing = resolved.vocabularyRules ?? {};
14251
+ return {
14252
+ ...resolved,
14253
+ vocabularyRules: {
14254
+ preferredTerms: unionTerms(asStringArray(existing.preferredTerms), vp),
14255
+ bannedTerms: unionTerms(asStringArray(existing.bannedTerms), vb)
14256
+ }
14257
+ };
14258
+ }
14259
+ function resolveBrandProfile(raw) {
14260
+ if (!raw || typeof raw !== "object") return {};
14261
+ const r = raw;
14262
+ const name = typeof r.name === "string" ? r.name : void 0;
14263
+ const voiceProfile = r.voiceProfile;
14264
+ const vocabularyRules = r.vocabularyRules;
14265
+ if (voiceProfile && vocabularyRules) {
14266
+ return mergeVoiceVocabulary(raw, r);
14267
+ }
14268
+ if (voiceProfile) {
14269
+ return mergeVoiceVocabulary(
14270
+ {
14271
+ name,
14272
+ voiceProfile,
14273
+ vocabularyRules: {
14274
+ bannedTerms: extractAtomicTerms(asStringArray(voiceProfile.avoidPatterns)),
14275
+ preferredTerms: asStringArray(voiceProfile.languagePatterns)
14276
+ },
14277
+ targetAudience: r.targetAudience,
14278
+ writingStyleRules: r.writingStyleRules
14279
+ },
14280
+ r
14281
+ );
14282
+ }
14283
+ const isFlat = hasField(r, "voiceTone") || hasField(r, "voiceTags") || hasField(r, "discouragedTerms") || hasField(r, "preferredTerms");
14284
+ if (isFlat) {
14285
+ const ta = r.targetAudience;
14286
+ const psy = ta?.psychographics;
14287
+ const painPoints = ta ? asStringArray(psy?.painPoints ?? ta.painPoints) : [];
14288
+ const interests = ta ? asStringArray(psy?.interests ?? ta.interests) : [];
14289
+ return mergeVoiceVocabulary(
14290
+ {
14291
+ name,
14292
+ voiceProfile: {
14293
+ tone: [...asStringArray(r.voiceTags), ...asStringArray(r.voiceTone)],
14294
+ style: asStringArray(r.voiceStyle),
14295
+ languagePatterns: [],
14296
+ avoidPatterns: []
14297
+ },
14298
+ vocabularyRules: {
14299
+ bannedTerms: asStringArray(r.discouragedTerms),
14300
+ preferredTerms: asStringArray(r.preferredTerms)
14301
+ },
14302
+ targetAudience: painPoints.length || interests.length ? { psychographics: { painPoints, interests } } : void 0
14303
+ },
14304
+ r
14305
+ );
14306
+ }
14307
+ const stub = { name };
14308
+ if (vocabularyRules)
14309
+ stub.vocabularyRules = vocabularyRules;
14310
+ return mergeVoiceVocabulary(stub, r);
13737
14311
  }
13738
14312
  var FABRICATION_PATTERNS = [
13739
- { regex: /\b\d+[,.]?\d*\s*(%|percent)/gi, label: "unverified percentage" },
14313
+ { regex: /\b\d+(?:[,.]\d+)?\s*(?:%|percent\b)/gi, label: "unverified percentage" },
13740
14314
  { regex: /\b(award[- ]?winning|best[- ]selling|#\s*1)\b/gi, label: "unverified ranking" },
13741
14315
  {
13742
14316
  regex: /\b(guaranteed|proven to|studies show|scientifically proven)\b/gi,
@@ -13952,24 +14526,25 @@ function computeBrandConsistency(content, profile, threshold = 60) {
13952
14526
  fabricationWarnings: []
13953
14527
  };
13954
14528
  }
14529
+ const resolved = resolveBrandProfile(profile);
13955
14530
  const dimensions = {
13956
- toneAlignment: scoreTone(content, profile),
13957
- vocabularyAdherence: scoreVocab(content, profile),
13958
- avoidCompliance: scoreAvoid(content, profile),
13959
- audienceRelevance: scoreAudience(content, profile),
13960
- brandMentions: scoreBrand(content, profile),
13961
- structuralPatterns: scoreStructure(content, profile)
14531
+ toneAlignment: scoreTone(content, resolved),
14532
+ vocabularyAdherence: scoreVocab(content, resolved),
14533
+ avoidCompliance: scoreAvoid(content, resolved),
14534
+ audienceRelevance: scoreAudience(content, resolved),
14535
+ brandMentions: scoreBrand(content, resolved),
14536
+ structuralPatterns: scoreStructure(content, resolved)
13962
14537
  };
13963
14538
  const overall = Math.round(
13964
14539
  Object.values(dimensions).reduce((sum, d) => sum + d.score * d.weight, 0)
13965
14540
  );
13966
14541
  const preferred = [
13967
- ...profile.voiceProfile?.languagePatterns || [],
13968
- ...profile.vocabularyRules?.preferredTerms || []
14542
+ ...resolved.voiceProfile?.languagePatterns || [],
14543
+ ...resolved.vocabularyRules?.preferredTerms || []
13969
14544
  ];
13970
14545
  const banned = [
13971
- ...profile.voiceProfile?.avoidPatterns || [],
13972
- ...profile.vocabularyRules?.bannedTerms || []
14546
+ ...resolved.voiceProfile?.avoidPatterns || [],
14547
+ ...resolved.vocabularyRules?.bannedTerms || []
13973
14548
  ];
13974
14549
  const fabrications = detectFabricationPatterns(content);
13975
14550
  return {
@@ -14000,11 +14575,11 @@ function hexToLab(hex) {
14000
14575
  const lb = srgbToLinear(b);
14001
14576
  const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
14002
14577
  const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
14003
- const z36 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
14578
+ const z39 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
14004
14579
  const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
14005
14580
  const fx = f(x / 0.95047);
14006
14581
  const fy = f(y / 1);
14007
- const fz = f(z36 / 1.08883);
14582
+ const fz = f(z39 / 1.08883);
14008
14583
  return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
14009
14584
  }
14010
14585
  function deltaE2000(lab1, lab2) {
@@ -14156,7 +14731,7 @@ function exportFigma(palette, typography) {
14156
14731
  }
14157
14732
 
14158
14733
  // src/tools/brandRuntime.ts
14159
- function asEnvelope22(data) {
14734
+ function asEnvelope23(data) {
14160
14735
  return {
14161
14736
  _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
14162
14737
  data
@@ -14237,8 +14812,9 @@ function registerBrandRuntimeTools(server2) {
14237
14812
  pagesScraped: meta.pagesScraped || 0
14238
14813
  }
14239
14814
  };
14240
- const envelope = asEnvelope22(runtime);
14815
+ const envelope = asEnvelope23(runtime);
14241
14816
  return {
14817
+ structuredContent: envelope,
14242
14818
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
14243
14819
  };
14244
14820
  }
@@ -14379,7 +14955,7 @@ function registerBrandRuntimeTools(server2) {
14379
14955
  }
14380
14956
  const profile = row.profile_data;
14381
14957
  const checkResult = computeBrandConsistency(content, profile);
14382
- const envelope = asEnvelope22(checkResult);
14958
+ const envelope = asEnvelope23(checkResult);
14383
14959
  return {
14384
14960
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
14385
14961
  };
@@ -14413,7 +14989,7 @@ function registerBrandRuntimeTools(server2) {
14413
14989
  content_colors,
14414
14990
  threshold ?? 10
14415
14991
  );
14416
- const envelope = asEnvelope22(auditResult);
14992
+ const envelope = asEnvelope23(auditResult);
14417
14993
  return {
14418
14994
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
14419
14995
  };
@@ -14448,7 +15024,7 @@ function registerBrandRuntimeTools(server2) {
14448
15024
  row.profile_data.typography,
14449
15025
  format
14450
15026
  );
14451
- const envelope = asEnvelope22({ format, tokens: output });
15027
+ const envelope = asEnvelope23({ format, tokens: output });
14452
15028
  return {
14453
15029
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
14454
15030
  };
@@ -14460,58 +15036,7 @@ function registerBrandRuntimeTools(server2) {
14460
15036
  init_edge_function();
14461
15037
  import { z as z28 } from "zod";
14462
15038
  init_supabase();
14463
- init_request_context();
14464
15039
  init_version();
14465
- var MAX_CREDITS_PER_RUN2 = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
14466
- var MAX_ASSETS_PER_RUN2 = Math.max(0, Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0));
14467
- var _globalCreditsUsed2 = 0;
14468
- var _globalAssetsGenerated2 = 0;
14469
- function getCreditsUsed2() {
14470
- const ctx = requestContext.getStore();
14471
- return ctx ? ctx.creditsUsed : _globalCreditsUsed2;
14472
- }
14473
- function addCreditsUsed2(amount) {
14474
- const ctx = requestContext.getStore();
14475
- if (ctx) {
14476
- ctx.creditsUsed += amount;
14477
- } else {
14478
- _globalCreditsUsed2 += amount;
14479
- }
14480
- }
14481
- function getAssetsGenerated2() {
14482
- const ctx = requestContext.getStore();
14483
- return ctx ? ctx.assetsGenerated : _globalAssetsGenerated2;
14484
- }
14485
- function addAssetsGenerated2(count) {
14486
- const ctx = requestContext.getStore();
14487
- if (ctx) {
14488
- ctx.assetsGenerated += count;
14489
- } else {
14490
- _globalAssetsGenerated2 += count;
14491
- }
14492
- }
14493
- function checkCreditBudget2(estimatedCost) {
14494
- if (MAX_CREDITS_PER_RUN2 <= 0) return { ok: true };
14495
- const used = getCreditsUsed2();
14496
- if (used + estimatedCost > MAX_CREDITS_PER_RUN2) {
14497
- return {
14498
- ok: false,
14499
- message: `Credit budget exceeded: ${used} used + ${estimatedCost} estimated > ${MAX_CREDITS_PER_RUN2} limit. Use a smaller slide count or cheaper image model.`
14500
- };
14501
- }
14502
- return { ok: true };
14503
- }
14504
- function checkAssetBudget2() {
14505
- if (MAX_ASSETS_PER_RUN2 <= 0) return { ok: true };
14506
- const gen = getAssetsGenerated2();
14507
- if (gen >= MAX_ASSETS_PER_RUN2) {
14508
- return {
14509
- ok: false,
14510
- message: `Asset limit reached: ${gen}/${MAX_ASSETS_PER_RUN2} assets generated this run.`
14511
- };
14512
- }
14513
- return { ok: true };
14514
- }
14515
15040
  var IMAGE_CREDIT_ESTIMATES2 = {
14516
15041
  midjourney: 20,
14517
15042
  "nano-banana": 15,
@@ -14520,7 +15045,7 @@ var IMAGE_CREDIT_ESTIMATES2 = {
14520
15045
  "flux-max": 50,
14521
15046
  "gpt4o-image": 40,
14522
15047
  imagen4: 35,
14523
- "imagen4-fast": 25,
15048
+ "imagen4-fast": 35,
14524
15049
  seedream: 20
14525
15050
  };
14526
15051
  async function fetchBrandVisualContext(projectId) {
@@ -14644,14 +15169,14 @@ function registerCarouselTools(server2) {
14644
15169
  const carouselTextCost = 10 + slideCount * 2;
14645
15170
  const perImageCost = IMAGE_CREDIT_ESTIMATES2[image_model] ?? 30;
14646
15171
  const totalEstimatedCost = carouselTextCost + slideCount * perImageCost;
14647
- const budgetCheck = checkCreditBudget2(totalEstimatedCost);
15172
+ const budgetCheck = checkCreditBudget(totalEstimatedCost);
14648
15173
  if (!budgetCheck.ok) {
14649
15174
  return {
14650
15175
  content: [{ type: "text", text: budgetCheck.message }],
14651
15176
  isError: true
14652
15177
  };
14653
15178
  }
14654
- const assetBudget = checkAssetBudget2();
15179
+ const assetBudget = checkAssetBudget(slideCount);
14655
15180
  if (!assetBudget.ok) {
14656
15181
  return {
14657
15182
  content: [{ type: "text", text: assetBudget.message }],
@@ -14698,8 +15223,15 @@ function registerCarouselTools(server2) {
14698
15223
  };
14699
15224
  }
14700
15225
  const carousel = carouselData.carousel;
15226
+ const imageAssetBudget = checkAssetBudget(carousel.slides.length);
15227
+ if (!imageAssetBudget.ok) {
15228
+ return {
15229
+ content: [{ type: "text", text: imageAssetBudget.message }],
15230
+ isError: true
15231
+ };
15232
+ }
14701
15233
  const textCredits = carousel.credits?.used ?? carouselTextCost;
14702
- addCreditsUsed2(textCredits);
15234
+ addCreditsUsed(textCredits);
14703
15235
  const imageJobs = await Promise.all(
14704
15236
  carousel.slides.map(async (slide) => {
14705
15237
  const promptParts = [];
@@ -14729,8 +15261,8 @@ function registerCarouselTools(server2) {
14729
15261
  }
14730
15262
  const jobId = data.asyncJobId ?? data.taskId ?? null;
14731
15263
  if (jobId) {
14732
- addCreditsUsed2(perImageCost);
14733
- addAssetsGenerated2(1);
15264
+ addCreditsUsed(perImageCost);
15265
+ addAssetsGenerated(1);
14734
15266
  }
14735
15267
  return {
14736
15268
  slideNumber: slide.slideNumber,
@@ -14838,19 +15370,19 @@ function registerCarouselTools(server2) {
14838
15370
  init_edge_function();
14839
15371
  import { z as z29 } from "zod";
14840
15372
  init_version();
14841
- function asEnvelope23(data) {
15373
+ function asEnvelope24(data) {
14842
15374
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
14843
15375
  }
14844
15376
  function registerNicheResearchTools(server2) {
14845
15377
  server2.tool(
14846
15378
  "find_winning_content",
14847
- "Find QA-gated high-performing short-form videos in the project's niche. Returns extracted hook patterns, content structures, and pre-compiled replication prompts you can use to generate new content on a different topic. Backed by niche_winners view (qa_score >= 0.5 + replication_prompt populated).",
15379
+ "Find QA-gated high-performing short-form videos in the project's niche. Returns extracted hook patterns, content structures, and pre-compiled replication prompts you can use to generate new content on a different topic. Only QA-gated winners with a populated replication prompt are returned.",
14848
15380
  {
14849
15381
  project_id: z29.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
14850
15382
  platform: z29.enum(["tiktok", "instagram", "youtube", "reddit", "twitter"]).optional().describe("Filter to one platform. Omit for all platforms."),
14851
15383
  days: z29.number().int().min(1).max(365).default(30).describe("Window: only return winners scanned within this many days."),
14852
15384
  limit: z29.number().int().min(1).max(50).default(10).describe("Number of winners to return (1-50)."),
14853
- min_qa_score: z29.number().min(0).max(1).default(0.5).describe("Minimum QA score (0..1). Default 0.5 matches the niche_winners view floor."),
15385
+ min_qa_score: z29.number().min(0).max(1).default(0.5).describe("Minimum QA score (0..1). Default 0.5."),
14854
15386
  response_format: z29.enum(["text", "json"]).optional()
14855
15387
  },
14856
15388
  async ({ project_id, platform: platform3, days, limit, min_qa_score, response_format }) => {
@@ -14872,7 +15404,7 @@ function registerNicheResearchTools(server2) {
14872
15404
  {
14873
15405
  type: "text",
14874
15406
  text: JSON.stringify(
14875
- asEnvelope23({
15407
+ asEnvelope24({
14876
15408
  winners,
14877
15409
  count: winners.length,
14878
15410
  filters: result?.filters ?? {}
@@ -15171,6 +15703,16 @@ import { z as z31 } from "zod";
15171
15703
  import fs from "node:fs/promises";
15172
15704
  import path from "node:path";
15173
15705
  var CALENDAR_URI = "ui://content-calendar/mcp-app.html";
15706
+ var RecentPostOutputSchema = z31.object({
15707
+ id: z31.string(),
15708
+ platform: z31.string(),
15709
+ status: z31.string(),
15710
+ title: z31.string().nullable(),
15711
+ external_post_id: z31.string().nullable(),
15712
+ published_at: z31.string().nullable(),
15713
+ scheduled_at: z31.string().nullable(),
15714
+ created_at: z31.string()
15715
+ });
15174
15716
  function startOfCurrentWeekMonday() {
15175
15717
  const now = /* @__PURE__ */ new Date();
15176
15718
  const day = now.getDay();
@@ -15190,6 +15732,11 @@ function registerContentCalendarApp(server2) {
15190
15732
  "ISO date for the week start (YYYY-MM-DD); defaults to the current week's Monday."
15191
15733
  )
15192
15734
  },
15735
+ outputSchema: {
15736
+ start_date: z31.string(),
15737
+ posts: z31.array(RecentPostOutputSchema),
15738
+ scopes: z31.array(z31.string())
15739
+ },
15193
15740
  _meta: {
15194
15741
  ui: {
15195
15742
  resourceUri: CALENDAR_URI,
@@ -15228,11 +15775,17 @@ function registerContentCalendarApp(server2) {
15228
15775
  if (!ts) return false;
15229
15776
  return ts.split("T")[0] >= fromDate;
15230
15777
  });
15778
+ const structuredContent = {
15779
+ start_date: fromDate,
15780
+ posts,
15781
+ scopes: userScopes
15782
+ };
15231
15783
  return {
15784
+ structuredContent,
15232
15785
  content: [
15233
15786
  {
15234
15787
  type: "text",
15235
- text: JSON.stringify({ posts, scopes: userScopes })
15788
+ text: `Loaded ${posts.length} calendar post${posts.length === 1 ? "" : "s"} from ${fromDate}.`
15236
15789
  }
15237
15790
  ]
15238
15791
  };
@@ -15491,7 +16044,7 @@ var writeReflectionSchema = {
15491
16044
  prm_score_ids: z33.array(z33.string()).optional().describe("PRM score IDs that informed this reflection."),
15492
16045
  handoff_ids: z33.array(z33.string()).optional().describe("Handoff event IDs that triggered this reflection.")
15493
16046
  }).strict().describe(
15494
- "Source evidence for this reflection. Only these four keys are accepted (Anti-Goodhart guard \u2014 unknown keys are rejected to prevent spurious provenance claims)."
16047
+ "Source evidence for this reflection. Only these four keys are accepted \u2014 unknown keys are rejected to prevent spurious provenance claims."
15495
16048
  ),
15496
16049
  brand_id: z33.string().describe("Brand profile UUID this reflection belongs to."),
15497
16050
  pipeline_id: z33.string().optional().describe("Optional: pipeline run UUID."),
@@ -15509,7 +16062,7 @@ var readReflectionSchema = {
15509
16062
  var recordOutcomeSchema = {
15510
16063
  decision_event_id: z33.string().describe("UUID of the decision_events row to record an outcome for."),
15511
16064
  horizon: z33.enum(["1h", "6h", "24h"]).describe(
15512
- "Observation horizon. Only horizon=24h with a non-null reward triggers a content_bandits posterior update; 1h/6h are stored but inert for learning."
16065
+ "Observation horizon. Only horizon=24h with a non-null reward triggers a learning-loop update; 1h/6h are stored but inert for learning."
15513
16066
  ),
15514
16067
  reward: z33.number().min(0).max(1).describe("Normalised reward signal in [0, 1]. Higher = better outcome."),
15515
16068
  outcome_metrics: z33.record(z33.string(), z33.number()).optional().describe(
@@ -15519,7 +16072,7 @@ var recordOutcomeSchema = {
15519
16072
  function registerHarnessTools(server2, _ctx) {
15520
16073
  server2.tool(
15521
16074
  "write_agent_reflection",
15522
- "Persist a verbal reflection for an agent loop. Provenance keys are restricted (Anti-Goodhart safety): only content_history_id, outcome_event_id, prm_score_ids, and handoff_ids are accepted \u2014 unknown keys are rejected at the input layer. Requires mcp:write scope. Returns the created reflection UUID on success.",
16075
+ "Persist a verbal reflection for an agent loop. Provenance keys are restricted to an allowlist: only content_history_id, outcome_event_id, prm_score_ids, and handoff_ids are accepted \u2014 unknown keys are rejected at the input layer. Requires mcp:write scope. Returns the created reflection UUID on success.",
15523
16076
  writeReflectionSchema,
15524
16077
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
15525
16078
  async (args) => {
@@ -15547,7 +16100,7 @@ function registerHarnessTools(server2, _ctx) {
15547
16100
  );
15548
16101
  server2.tool(
15549
16102
  "record_outcome",
15550
- "Record an outcome for a published decision event. Idempotent on (decision_event_id, horizon) \u2014 safe to call multiple times. Returns idempotent:true when the row already existed (UPDATE), idempotent:false on fresh INSERT. Note: only horizon=24h with reward != null triggers a content_bandits posterior update; 1h/6h outcomes are stored but are inert for the learning loop. Requires mcp:write scope.",
16103
+ "Record an outcome for a published decision event. Idempotent on (decision_event_id, horizon) \u2014 safe to call multiple times. Returns idempotent:true when the row already existed (UPDATE), idempotent:false on fresh INSERT. Note: only horizon=24h with reward != null triggers a learning-loop update; 1h/6h outcomes are stored but are inert for the learning loop. Requires mcp:write scope.",
15551
16104
  recordOutcomeSchema,
15552
16105
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
15553
16106
  async (args) => {
@@ -15611,7 +16164,7 @@ function registerHarnessTools(server2, _ctx) {
15611
16164
  init_edge_function();
15612
16165
  init_version();
15613
16166
  import { z as z34 } from "zod";
15614
- function asEnvelope24(data) {
16167
+ function asEnvelope25(data) {
15615
16168
  return {
15616
16169
  _meta: {
15617
16170
  version: MCP_VERSION,
@@ -15633,13 +16186,13 @@ var PLATFORM = z34.enum([
15633
16186
  function registerHermesTools(server2) {
15634
16187
  server2.tool(
15635
16188
  "save_draft_to_library",
15636
- "Save a draft post to the SN content library. Use when an autonomous agent (Hermes) wants to persist a draft before the founder approves it. Lands in ContentLibrary with status='draft', origin='hermes'. The draft can then be approved/edited in the SN UI.",
16189
+ "Save a draft post to the SN content library. Use when an autonomous agent wants to persist a draft for review before publishing. Lands in the content library with status='draft'. The draft can then be approved/edited in the SN UI.",
15637
16190
  {
15638
16191
  platform: PLATFORM.describe("Target platform for the draft."),
15639
16192
  copy: z34.string().min(1).max(8e3).describe("The draft post body."),
15640
16193
  project_id: z34.string().optional().describe("SN project UUID. Optional but recommended."),
15641
16194
  media_url: z34.string().url().optional().describe("Optional cover/media URL (R2 signed URL)."),
15642
- hermes_run_id: z34.string().optional().describe("Hermes cron run id, for traceability."),
16195
+ hermes_run_id: z34.string().optional().describe("Agent run id, for traceability."),
15643
16196
  source_intel_ids: z34.array(z34.string()).optional().describe("Optional list of intel_signals.id rows that informed this draft."),
15644
16197
  response_format: z34.enum(["text", "json"]).optional()
15645
16198
  },
@@ -15662,7 +16215,7 @@ function registerHermesTools(server2) {
15662
16215
  const payload = { content_id: data?.content_id ?? null };
15663
16216
  if (format === "json") {
15664
16217
  return {
15665
- content: [{ type: "text", text: JSON.stringify(asEnvelope24(payload), null, 2) }]
16218
+ content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
15666
16219
  };
15667
16220
  }
15668
16221
  return {
@@ -15706,7 +16259,7 @@ function registerHermesTools(server2) {
15706
16259
  };
15707
16260
  if (response_format === "json") {
15708
16261
  return {
15709
- content: [{ type: "text", text: JSON.stringify(asEnvelope24(payload), null, 2) }]
16262
+ content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
15710
16263
  };
15711
16264
  }
15712
16265
  return {
@@ -15721,11 +16274,11 @@ function registerHermesTools(server2) {
15721
16274
  );
15722
16275
  server2.tool(
15723
16276
  "record_observation",
15724
- 'Record an agent observation ("Hermes noticed X this week"). Surfaces in UnifiedAnalytics > Playbook tab. Use for weekly reflection digests and mid-campaign pulse summaries.',
16277
+ 'Record an agent observation (e.g. "topic X engagement up 23% this week"). Surfaces in the analytics Playbook. Use for weekly reflection digests and mid-campaign pulse summaries.',
15725
16278
  {
15726
- summary: z34.string().min(1).max(2e3).describe("One-paragraph summary, founder-facing."),
16279
+ summary: z34.string().min(1).max(2e3).describe("One-paragraph summary, shown to the account owner."),
15727
16280
  deltas: z34.record(z34.string(), z34.union([z34.number(), z34.string(), z34.boolean()])).optional().describe("Optional structured key/value payload (e.g. {topic_x_er_pct: 23})."),
15728
- run_id: z34.string().optional().describe("Hermes cron run id for traceability."),
16281
+ run_id: z34.string().optional().describe("Agent run id for traceability."),
15729
16282
  response_format: z34.enum(["text", "json"]).optional()
15730
16283
  },
15731
16284
  async ({ summary, deltas, run_id, response_format }) => {
@@ -15736,7 +16289,7 @@ function registerHermesTools(server2) {
15736
16289
  const payload = { observation_id: data?.observation_id ?? null };
15737
16290
  if (response_format === "json") {
15738
16291
  return {
15739
- content: [{ type: "text", text: JSON.stringify(asEnvelope24(payload), null, 2) }]
16292
+ content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
15740
16293
  };
15741
16294
  }
15742
16295
  return {
@@ -15780,7 +16333,7 @@ function registerHermesTools(server2) {
15780
16333
  };
15781
16334
  if (response_format === "json") {
15782
16335
  return {
15783
- content: [{ type: "text", text: JSON.stringify(asEnvelope24(payload), null, 2) }]
16336
+ content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
15784
16337
  };
15785
16338
  }
15786
16339
  return {
@@ -15795,7 +16348,7 @@ function registerHermesTools(server2) {
15795
16348
  );
15796
16349
  server2.tool(
15797
16350
  "record_campaign_spend",
15798
- "Log a campaign cost line. Use when Hermes incurs spend that should be attributed to a campaign \u2014 drafts, renders, paid amp. Categories: hermes_drafts / carousel_renders / analytics_pulls / paid_amplification / other. Read aggregate via get_active_campaigns.",
16351
+ "Log a campaign cost line. Use when an autonomous agent incurs spend that should be attributed to a campaign \u2014 drafts, renders, paid amp. Read aggregate via get_active_campaigns.",
15799
16352
  {
15800
16353
  campaign_id: z34.string().min(1).max(200).describe("Campaign identifier (slug)."),
15801
16354
  category: z34.enum([
@@ -15824,7 +16377,7 @@ function registerHermesTools(server2) {
15824
16377
  };
15825
16378
  if (response_format === "json") {
15826
16379
  return {
15827
- content: [{ type: "text", text: JSON.stringify(asEnvelope24(payload), null, 2) }]
16380
+ content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
15828
16381
  };
15829
16382
  }
15830
16383
  return {
@@ -15839,7 +16392,7 @@ function registerHermesTools(server2) {
15839
16392
  );
15840
16393
  server2.tool(
15841
16394
  "get_active_campaigns",
15842
- "List currently-running campaigns with thesis, budget, hero format, and current spend. Used by Hermes pitch skill to bias drafts toward active campaign themes.",
16395
+ "List currently-running campaigns with thesis, budget, hero format, and current spend. Use to bias drafts toward active campaign themes.",
15843
16396
  {
15844
16397
  response_format: z34.enum(["text", "json"]).optional()
15845
16398
  },
@@ -15854,7 +16407,7 @@ function registerHermesTools(server2) {
15854
16407
  content: [
15855
16408
  {
15856
16409
  type: "text",
15857
- text: JSON.stringify(asEnvelope24({ campaigns }), null, 2)
16410
+ text: JSON.stringify(asEnvelope25({ campaigns }), null, 2)
15858
16411
  }
15859
16412
  ]
15860
16413
  };
@@ -15885,6 +16438,343 @@ ${"=".repeat(40)}
15885
16438
  );
15886
16439
  }
15887
16440
 
16441
+ // src/tools/skills.ts
16442
+ init_version();
16443
+ import { z as z35 } from "zod";
16444
+
16445
+ // src/lib/skills-manifest.ts
16446
+ var SKILLS_MANIFEST = [
16447
+ {
16448
+ id: "skill-brand-locked-viral-hook-reel",
16449
+ name: "Brand-locked viral hook reel",
16450
+ studio: "video",
16451
+ category: "hook",
16452
+ shortDescription: "Pulls your brand voice + the hook pattern that actually worked last cycle, then renders a 12s reel with stacked camera moves and brand-cloned voice.",
16453
+ hookFormula: "Pattern interrupt in first 0.5s + escalating stakes every 2s + brand-locked voice + cloned-from-winning-past-post structure.",
16454
+ estimatedCredits: 580,
16455
+ estimatedSeconds: 158,
16456
+ featured: true,
16457
+ stepCount: 9,
16458
+ inspiredBy: ["MrBeast", "Alex Hormozi"]
16459
+ }
16460
+ ];
16461
+ function getSkill(id) {
16462
+ return SKILLS_MANIFEST.find((s) => s.id === id);
16463
+ }
16464
+ function listSkills(opts) {
16465
+ let entries = SKILLS_MANIFEST;
16466
+ if (opts?.studio) entries = entries.filter((s) => s.studio === opts.studio);
16467
+ if (opts?.featuredOnly) entries = entries.filter((s) => s.featured);
16468
+ return entries;
16469
+ }
16470
+
16471
+ // src/tools/skills.ts
16472
+ function asEnvelope26(data) {
16473
+ return {
16474
+ _meta: {
16475
+ version: MCP_VERSION,
16476
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
16477
+ },
16478
+ data
16479
+ };
16480
+ }
16481
+ var STUDIO_VALUES = ["video", "avatar", "carousel", "voice", "caption", "edit"];
16482
+ function renderSkillSummary(skill) {
16483
+ const lines = [];
16484
+ lines.push(`${skill.name} (${skill.id})`);
16485
+ lines.push(` Studio: ${skill.studio} \xB7 Category: ${skill.category}`);
16486
+ if (skill.featured) lines.push(" \u2B50 Featured");
16487
+ lines.push(` ${skill.shortDescription}`);
16488
+ lines.push(` Hook: ${skill.hookFormula}`);
16489
+ lines.push(
16490
+ ` Cost: ~${skill.estimatedCredits} credits \xB7 ~${skill.estimatedSeconds}s \xB7 ${skill.stepCount} steps`
16491
+ );
16492
+ lines.push(` Inspired by: ${skill.inspiredBy.join(", ")}`);
16493
+ return lines.join("\n");
16494
+ }
16495
+ function registerSkillsTools(server2) {
16496
+ server2.tool(
16497
+ "list_skills",
16498
+ 'List Social Neuron content workflow skills available to the authenticated user. A skill is a brand-locked multi-step pipeline (research \u2192 hook \u2192 script \u2192 visuals \u2192 voice \u2192 captions \u2192 assembly \u2192 quality gate \u2192 schedule) inspired by documented viral patterns (MrBeast 3-second hook, Hormozi pattern interrupt, etc.). Use this tool when the user asks "what can SN do", "what skills are available", "show me viral templates", or before calling run_skill so you can pick the right one.',
16499
+ {
16500
+ studio: z35.enum(STUDIO_VALUES).optional().describe("Filter to one studio (video, avatar, carousel, voice, caption, edit)."),
16501
+ featured_only: z35.boolean().optional().describe("Return only featured (recommended) skills. Defaults to false."),
16502
+ response_format: z35.enum(["text", "json"]).optional().describe("Response format. Defaults to text \u2014 human-readable summary.")
16503
+ },
16504
+ async ({ studio, featured_only, response_format }) => {
16505
+ const skills = listSkills({ studio, featuredOnly: featured_only });
16506
+ const format = response_format ?? "text";
16507
+ if (format === "json") {
16508
+ return {
16509
+ content: [
16510
+ {
16511
+ type: "text",
16512
+ text: JSON.stringify(asEnvelope26({ count: skills.length, skills }), null, 2)
16513
+ }
16514
+ ]
16515
+ };
16516
+ }
16517
+ if (skills.length === 0) {
16518
+ return {
16519
+ content: [
16520
+ {
16521
+ type: "text",
16522
+ text: "No skills match the filter." + (studio ? ` Studio "${studio}" has no skills yet.` : "") + "\n\nAvailable studios with skills: " + Array.from(new Set(SKILLS_MANIFEST.map((s) => s.studio))).join(", ")
16523
+ }
16524
+ ]
16525
+ };
16526
+ }
16527
+ const blocks = skills.map(renderSkillSummary).join("\n\n");
16528
+ const header = `${skills.length} skill${skills.length === 1 ? "" : "s"} available
16529
+ ${"=".repeat(40)}`;
16530
+ return {
16531
+ content: [{ type: "text", text: `${header}
16532
+
16533
+ ${blocks}` }]
16534
+ };
16535
+ }
16536
+ );
16537
+ server2.tool(
16538
+ "run_skill",
16539
+ "Run a Social Neuron workflow skill end-to-end (brand-locked content production). Returns a structured run preview with the exact step plan, credit cost, and a deep-link to launch the run in the SN dashboard. A future release executes in-process so you can stream step-by-step progress back to the user. Call list_skills first to discover available skill ids.",
16540
+ {
16541
+ skill_id: z35.string().describe(
16542
+ 'The id of the skill to run (e.g. "skill-brand-locked-viral-hook-reel"). Use list_skills to discover available ids.'
16543
+ ),
16544
+ topic: z35.string().min(1).describe('What the content is about (e.g. "why we built Social Neuron").'),
16545
+ audience: z35.string().optional().describe(
16546
+ 'Who the content is for (e.g. "first-time founders"). Defaults to brand persona.'
16547
+ ),
16548
+ hook: z35.string().optional().describe(
16549
+ "Optional explicit hook. If omitted, the skill drafts and scores 3-5 candidates."
16550
+ ),
16551
+ cta: z35.string().optional().describe("Optional call-to-action override. Defaults to brand standard CTA."),
16552
+ response_format: z35.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
16553
+ },
16554
+ async ({ skill_id, topic, audience, hook, cta, response_format }) => {
16555
+ const skill = getSkill(skill_id);
16556
+ const format = response_format ?? "text";
16557
+ if (!skill) {
16558
+ const knownIds = SKILLS_MANIFEST.map((s) => s.id).join(", ");
16559
+ return {
16560
+ content: [
16561
+ {
16562
+ type: "text",
16563
+ text: `Unknown skill_id "${skill_id}". Available skills: ${knownIds}. Call list_skills for descriptions.`
16564
+ }
16565
+ ],
16566
+ isError: true
16567
+ };
16568
+ }
16569
+ const runUrl = "https://socialneuron.com/dashboard/creation?skill=" + encodeURIComponent(skill.id);
16570
+ const inputs = { topic, audience, hook, cta };
16571
+ const previewedAt = (/* @__PURE__ */ new Date()).toISOString();
16572
+ if (format === "json") {
16573
+ return {
16574
+ content: [
16575
+ {
16576
+ type: "text",
16577
+ text: JSON.stringify(
16578
+ asEnvelope26({
16579
+ status: "preview",
16580
+ skill,
16581
+ inputs,
16582
+ runUrl,
16583
+ previewedAt,
16584
+ note: "This release returns a preview. A future release will execute in-process."
16585
+ }),
16586
+ null,
16587
+ 2
16588
+ )
16589
+ }
16590
+ ]
16591
+ };
16592
+ }
16593
+ const lines = [
16594
+ `Ready to run: ${skill.name}`,
16595
+ "=".repeat(40),
16596
+ "",
16597
+ skill.shortDescription,
16598
+ "",
16599
+ `Hook formula: ${skill.hookFormula}`,
16600
+ `Inspired by: ${skill.inspiredBy.join(", ")}`,
16601
+ "",
16602
+ "Inputs",
16603
+ ` topic : ${topic}`,
16604
+ ` audience : ${audience ?? "(brand persona)"}`,
16605
+ ` hook : ${hook ?? "(auto-drafted from 3-5 candidates, scored)"}`,
16606
+ ` cta : ${cta ?? "(brand default)"}`,
16607
+ "",
16608
+ "Plan",
16609
+ ` ${skill.stepCount} steps \xB7 ~${skill.estimatedCredits} credits \xB7 ~${skill.estimatedSeconds}s wall time`,
16610
+ "",
16611
+ `Launch in dashboard: ${runUrl}`,
16612
+ "",
16613
+ "\u26A0\uFE0F Preview only. End-to-end MCP execution arrives in a future release."
16614
+ ];
16615
+ return {
16616
+ content: [{ type: "text", text: lines.join("\n") }]
16617
+ };
16618
+ }
16619
+ );
16620
+ }
16621
+
16622
+ // src/tools/loopPulse.ts
16623
+ init_edge_function();
16624
+ init_version();
16625
+ import { z as z36 } from "zod";
16626
+ function asEnvelope27(data) {
16627
+ return {
16628
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
16629
+ data
16630
+ };
16631
+ }
16632
+ function registerLoopPulseTools(server2) {
16633
+ server2.tool(
16634
+ "get_loop_pulse",
16635
+ 'Read the dynamic loop-health KPIs for the Social Neuron growth loop over the last 7 days. Returns reflection coverage, decision coverage, visual gate pass rate, learning-update application rate, per-platform learning uptake, autopilot lag, and pattern aggregation counts \u2014 each with a status ("ok" / "warn" / "bad") and a why-line explaining what the metric measures. Use this to decide whether the loop is closing or where it is stuck before recommending next moves.',
16636
+ {
16637
+ response_format: z36.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
16638
+ },
16639
+ async ({ response_format }) => {
16640
+ const format = response_format ?? "text";
16641
+ const { data, error } = await callEdgeFunction("mc-loop-pulse", {});
16642
+ if (error || !data) {
16643
+ return {
16644
+ content: [
16645
+ {
16646
+ type: "text",
16647
+ text: `Failed to read loop pulse: ${error || "no data"}`
16648
+ }
16649
+ ],
16650
+ isError: true
16651
+ };
16652
+ }
16653
+ if (format === "json") {
16654
+ return {
16655
+ content: [{ type: "text", text: JSON.stringify(asEnvelope27(data), null, 2) }]
16656
+ };
16657
+ }
16658
+ const rank = { bad: 0, warn: 1, unknown: 2, ok: 3 };
16659
+ const sorted = [...data.kpis].sort((a, b) => rank[a.status] - rank[b.status]);
16660
+ const lines = [
16661
+ `LOOP PULSE \u2014 overall: ${data.overall.toUpperCase()}`,
16662
+ "",
16663
+ ...sorted.map((k) => {
16664
+ const unit = k.unit ?? "";
16665
+ const val = k.value == null ? "\u2014" : `${k.value}${unit}`;
16666
+ return `[${k.status.toUpperCase().padEnd(4)}] ${k.label}: ${val}
16667
+ ${k.why}`;
16668
+ }),
16669
+ "",
16670
+ `Generated ${data.generated_at}`
16671
+ ];
16672
+ return {
16673
+ content: [{ type: "text", text: lines.join("\n") }]
16674
+ };
16675
+ }
16676
+ );
16677
+ }
16678
+
16679
+ // src/tools/banditState.ts
16680
+ init_edge_function();
16681
+ init_supabase();
16682
+ init_version();
16683
+ import { z as z37 } from "zod";
16684
+ function asEnvelope28(data) {
16685
+ return {
16686
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
16687
+ data
16688
+ };
16689
+ }
16690
+ function registerBanditStateTools(server2) {
16691
+ server2.tool(
16692
+ "get_bandit_state",
16693
+ "Read the current content learning state for a project. Returns top-K arms per (arm_type, platform) with expected performance and uncertainty. Use this to reason about which hook family / format / timing slot currently performs best on each platform before recommending next moves. Arm types: hook_family, length_bucket (xs/s/m/l/xl by platform), posting_time_bucket (morning/midday/evening/late), content_format (video/carousel/image/caption/text/avatar/storyboard).",
16694
+ {
16695
+ project_id: z37.string().uuid().optional().describe("Project UUID. Defaults to the authenticated user default project."),
16696
+ platform: z37.enum([
16697
+ "instagram",
16698
+ "tiktok",
16699
+ "youtube",
16700
+ "linkedin",
16701
+ "twitter",
16702
+ "facebook",
16703
+ "threads",
16704
+ "bluesky"
16705
+ ]).optional().describe(
16706
+ "Lowercase platform name. Omit to return both platform-scoped and legacy platform-agnostic arms."
16707
+ ),
16708
+ arm_type: z37.enum([
16709
+ "hook_family",
16710
+ "hook_type",
16711
+ "format",
16712
+ "timing_slot",
16713
+ "topic_cluster",
16714
+ "caption_style",
16715
+ "platform",
16716
+ "story_type",
16717
+ "emoji_type",
16718
+ "length_bucket",
16719
+ "posting_time_bucket",
16720
+ "content_format"
16721
+ ]).optional().describe("Arm dimension. Omit to return all types grouped."),
16722
+ top_k: z37.number().min(1).max(50).optional().describe("Max arms per (arm_type) group. Default 5."),
16723
+ response_format: z37.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
16724
+ },
16725
+ async ({ project_id, platform: platform3, arm_type, top_k, response_format }) => {
16726
+ const format = response_format ?? "text";
16727
+ const projectId = project_id ?? await getDefaultProjectId();
16728
+ if (!projectId) {
16729
+ return {
16730
+ content: [
16731
+ { type: "text", text: "No project_id provided and no default project available." }
16732
+ ],
16733
+ isError: true
16734
+ };
16735
+ }
16736
+ const { data, error } = await callEdgeFunction("mc-bandit-state", {
16737
+ project_id: projectId,
16738
+ platform: platform3,
16739
+ arm_type,
16740
+ top_k
16741
+ });
16742
+ if (error || !data) {
16743
+ return {
16744
+ content: [{ type: "text", text: `Failed to read bandit state: ${error || "no data"}` }],
16745
+ isError: true
16746
+ };
16747
+ }
16748
+ if (format === "json") {
16749
+ return { content: [{ type: "text", text: JSON.stringify(asEnvelope28(data), null, 2) }] };
16750
+ }
16751
+ const lines = [
16752
+ `BANDIT STATE \u2014 project ${data.project_id.slice(0, 8)}${data.platform_filter ? ` \xB7 ${data.platform_filter}` : ""}`,
16753
+ `${data.total_arms} arms across ${data.groups.length} arm types`,
16754
+ ""
16755
+ ];
16756
+ for (const group of data.groups) {
16757
+ lines.push(`[${group.arm_type}] ${group.summary}`);
16758
+ const renderArm = (a) => {
16759
+ const conf = a.posterior_stdev < 0.05 ? "high" : a.posterior_stdev < 0.15 ? "med" : "low";
16760
+ return ` ${a.arm_name.padEnd(18)} mean=${a.posterior_mean.toFixed(3)} \xB1${a.posterior_stdev.toFixed(3)} (${conf} conf \xB7 ${a.total_pulls} pulls)`;
16761
+ };
16762
+ if (group.platform_scoped.length > 0) {
16763
+ lines.push(` Per-platform top arms:`);
16764
+ group.platform_scoped.forEach((a) => lines.push(renderArm(a)));
16765
+ }
16766
+ if (group.platform_agnostic.length > 0) {
16767
+ lines.push(` Platform-agnostic (legacy) top arms:`);
16768
+ group.platform_agnostic.forEach((a) => lines.push(renderArm(a)));
16769
+ }
16770
+ lines.push("");
16771
+ }
16772
+ lines.push(`Generated ${data.generated_at}`);
16773
+ return { content: [{ type: "text", text: lines.join("\n") }] };
16774
+ }
16775
+ );
16776
+ }
16777
+
15888
16778
  // src/lib/register-tools.ts
15889
16779
  function wrapToolWithScanner(toolName, handler) {
15890
16780
  return async function scannerWrappedHandler(...handlerArgs) {
@@ -15937,7 +16827,8 @@ function wrapToolWithScanner(toolName, handler) {
15937
16827
  }
15938
16828
  function applyScopeEnforcement(server2, scopeResolver) {
15939
16829
  const originalTool = server2.tool.bind(server2);
15940
- server2.tool = function wrappedTool(...args) {
16830
+ const originalRegisterTool = server2.registerTool?.bind(server2);
16831
+ const wrapRegistration = (...args) => {
15941
16832
  const name = args[0];
15942
16833
  const requiredScope = TOOL_SCOPES[name];
15943
16834
  const handlerIndex = args.findIndex(
@@ -15948,27 +16839,11 @@ function applyScopeEnforcement(server2, scopeResolver) {
15948
16839
  const scannerWrappedHandler = wrapToolWithScanner(name, originalHandler);
15949
16840
  args[handlerIndex] = async function scopeEnforcedHandler(...handlerArgs) {
15950
16841
  if (!requiredScope) {
15951
- return {
15952
- content: [
15953
- {
15954
- type: "text",
15955
- text: `Permission denied: '${name}' has no scope defined. Contact support.`
15956
- }
15957
- ],
15958
- isError: true
15959
- };
16842
+ return scopeDeniedResult(name, void 0, scopeResolver());
15960
16843
  }
15961
16844
  const userScopes = scopeResolver();
15962
16845
  if (!hasScope(userScopes, requiredScope)) {
15963
- return {
15964
- content: [
15965
- {
15966
- type: "text",
15967
- text: `Permission denied: '${name}' requires scope '${requiredScope}'. Generate a new key with the required scope at https://socialneuron.com/settings/developer`
15968
- }
15969
- ],
15970
- isError: true
15971
- };
16846
+ return scopeDeniedResult(name, requiredScope, userScopes);
15972
16847
  }
15973
16848
  const startedAt = Date.now();
15974
16849
  try {
@@ -15995,9 +16870,59 @@ function applyScopeEnforcement(server2, scopeResolver) {
15995
16870
  }
15996
16871
  };
15997
16872
  }
15998
- return originalTool(...args);
16873
+ return args;
16874
+ };
16875
+ server2.tool = function wrappedTool(...args) {
16876
+ return originalTool(...wrapRegistration(...args));
16877
+ };
16878
+ if (originalRegisterTool) {
16879
+ server2.registerTool = function wrappedRegisterTool(...args) {
16880
+ return originalRegisterTool(...wrapRegistration(...args));
16881
+ };
16882
+ }
16883
+ }
16884
+ function scopeDeniedResult(name, requiredScope, userScopes) {
16885
+ const error = requiredScope ? {
16886
+ error: "permission_denied",
16887
+ tool: name,
16888
+ required_scope: requiredScope,
16889
+ available_scopes: userScopes,
16890
+ recover_with: [
16891
+ "Call search_tools with available_only=true to find tools this key can use.",
16892
+ "Use a read-only alternative if one is available for the task.",
16893
+ "Regenerate the API key with the required scope or upgrade the plan tier."
16894
+ ],
16895
+ developer_url: "https://socialneuron.com/settings/developer"
16896
+ } : {
16897
+ error: "tool_scope_missing",
16898
+ tool: name,
16899
+ available_scopes: userScopes,
16900
+ recover_with: ["Contact support; this tool is not mapped to a required scope."]
16901
+ };
16902
+ const challenge = requiredScope ? buildWwwAuthenticateHeader({
16903
+ issuerUrl: getChallengeIssuerUrl(),
16904
+ error: "insufficient_scope",
16905
+ errorDescription: `Tool ${name} requires scope ${requiredScope}.`,
16906
+ scope: requiredScope
16907
+ }) : void 0;
16908
+ return {
16909
+ content: [{ type: "text", text: JSON.stringify(error, null, 2) }],
16910
+ ...challenge ? { _meta: { "mcp/www_authenticate": [challenge] } } : {},
16911
+ isError: true
15999
16912
  };
16000
16913
  }
16914
+ function getChallengeIssuerUrl() {
16915
+ if (process.env.OAUTH_ISSUER_URL) return process.env.OAUTH_ISSUER_URL;
16916
+ const mcpServerUrl = process.env.MCP_SERVER_URL;
16917
+ if (mcpServerUrl) {
16918
+ try {
16919
+ const parsed = new URL(mcpServerUrl);
16920
+ return `${parsed.protocol}//${parsed.host}`;
16921
+ } catch {
16922
+ }
16923
+ }
16924
+ return "https://mcp.socialneuron.com";
16925
+ }
16001
16926
  var RESPONSE_CHAR_LIMIT = 1e5;
16002
16927
  function truncateResponse(result) {
16003
16928
  if (!result?.content || !Array.isArray(result.content)) return result;
@@ -16067,6 +16992,9 @@ function registerAllTools(server2, options) {
16067
16992
  registerConnectionTools(server2);
16068
16993
  registerHarnessTools(server2, void 0);
16069
16994
  registerHermesTools(server2);
16995
+ registerSkillsTools(server2);
16996
+ registerLoopPulseTools(server2);
16997
+ registerBanditStateTools(server2);
16070
16998
  if (!options?.skipApps) {
16071
16999
  registerContentCalendarApp(server2);
16072
17000
  }
@@ -16074,17 +17002,17 @@ function registerAllTools(server2, options) {
16074
17002
  }
16075
17003
 
16076
17004
  // src/prompts.ts
16077
- import { z as z35 } from "zod";
17005
+ import { z as z38 } from "zod";
16078
17006
  function registerPrompts(server2) {
16079
17007
  server2.prompt(
16080
17008
  "create_weekly_content_plan",
16081
17009
  "Generate a full week of social media content (7 days, multiple platforms). Returns a structured plan with topics, formats, and posting times.",
16082
17010
  {
16083
- niche: z35.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
16084
- platforms: z35.string().optional().describe(
17011
+ niche: z38.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
17012
+ platforms: z38.string().optional().describe(
16085
17013
  'Comma-separated platforms to target (default: "YouTube, Instagram, TikTok, LinkedIn")'
16086
17014
  ),
16087
- tone: z35.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
17015
+ tone: z38.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
16088
17016
  },
16089
17017
  ({ niche, platforms, tone }) => {
16090
17018
  const targetPlatforms = platforms || "YouTube, Instagram, TikTok, LinkedIn";
@@ -16126,8 +17054,8 @@ After building the plan, use \`save_content_plan\` to save it.`
16126
17054
  "analyze_top_content",
16127
17055
  "Analyze your best-performing posts to identify patterns and replicate success. Returns insights on hooks, formats, timing, and topics that resonate.",
16128
17056
  {
16129
- timeframe: z35.string().optional().describe('Analysis period (default: "30 days"). E.g., "7 days", "90 days"'),
16130
- platform: z35.string().optional().describe('Filter to a specific platform (e.g., "youtube", "instagram")')
17057
+ timeframe: z38.string().optional().describe('Analysis period (default: "30 days"). E.g., "7 days", "90 days"'),
17058
+ platform: z38.string().optional().describe('Filter to a specific platform (e.g., "youtube", "instagram")')
16131
17059
  },
16132
17060
  ({ timeframe, platform: platform3 }) => {
16133
17061
  const period = timeframe || "30 days";
@@ -16164,10 +17092,10 @@ Format as a clear, actionable performance report.`
16164
17092
  "repurpose_content",
16165
17093
  "Take one piece of content and transform it into 8-10 pieces across multiple platforms and formats.",
16166
17094
  {
16167
- source: z35.string().describe(
17095
+ source: z38.string().describe(
16168
17096
  "The source content to repurpose \u2014 a URL, transcript, or the content text itself"
16169
17097
  ),
16170
- target_platforms: z35.string().optional().describe(
17098
+ target_platforms: z38.string().optional().describe(
16171
17099
  'Comma-separated target platforms (default: "Twitter, LinkedIn, Instagram, TikTok, YouTube")'
16172
17100
  )
16173
17101
  },
@@ -16209,9 +17137,9 @@ For each piece, include the platform, format, character count, and suggested pos
16209
17137
  "setup_brand_voice",
16210
17138
  "Define or refine your brand voice profile so all generated content stays on-brand. Walks through tone, audience, values, and style.",
16211
17139
  {
16212
- brand_name: z35.string().describe("Your brand or business name"),
16213
- industry: z35.string().optional().describe('Your industry or niche (e.g., "B2B SaaS", "fitness coaching")'),
16214
- website: z35.string().optional().describe("Your website URL for context")
17140
+ brand_name: z38.string().describe("Your brand or business name"),
17141
+ industry: z38.string().optional().describe('Your industry or niche (e.g., "B2B SaaS", "fitness coaching")'),
17142
+ website: z38.string().optional().describe("Your website URL for context")
16215
17143
  },
16216
17144
  ({ brand_name, industry, website }) => {
16217
17145
  const industryContext = industry ? ` in the ${industry} space` : "";
@@ -16454,13 +17382,13 @@ function registerResources(server2) {
16454
17382
  pro: {
16455
17383
  price: "$49/mo",
16456
17384
  credits: 1500,
16457
- mcp_access: "Read + Analytics",
17385
+ mcp_access: "Read + Analytics + Write + Distribute",
16458
17386
  features: [
16459
17387
  "All Starter features",
16460
17388
  "Closed-loop optimization",
16461
- "5 autopilot runs / month",
17389
+ "Full agent engine via MCP (generate, gate, publish)",
16462
17390
  "All 10 platforms",
16463
- "MCP read + analytics"
17391
+ "MCP read + analytics + write + distribute"
16464
17392
  ]
16465
17393
  },
16466
17394
  team: {
@@ -16565,6 +17493,7 @@ function registerResources(server2) {
16565
17493
  init_posthog();
16566
17494
  init_supabase();
16567
17495
  init_sn();
17496
+ process.env.MCP_TRANSPORT ??= "stdio";
16568
17497
  function flushAndExit(code) {
16569
17498
  const done = { out: false, err: false };
16570
17499
  const tryExit = () => {
@@ -16726,7 +17655,7 @@ if (command === "sn") {
16726
17655
  process.exit(snSubcommand ? 0 : 1);
16727
17656
  }
16728
17657
  await runSnCli(process.argv.slice(3));
16729
- await new Promise((resolve3) => process.stdout.write("", resolve3));
17658
+ await new Promise((resolve3) => process.stdout.write("", () => resolve3()));
16730
17659
  process.exit(0);
16731
17660
  }
16732
17661
  if (command && !["setup", "login", "logout", "whoami", "health", "sn", "repl"].includes(command)) {