@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/http.js CHANGED
@@ -1,60 +1,18 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
- var __esm = (fn, res) => function __init() {
4
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
3
+ var __esm = (fn, res, err) => function __init() {
4
+ if (err) throw err[0];
5
+ try {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ } catch (e) {
8
+ throw err = [e], e;
9
+ }
5
10
  };
6
11
  var __export = (target, all) => {
7
12
  for (var name in all)
8
13
  __defProp(target, name, { get: all[name], enumerable: true });
9
14
  };
10
15
 
11
- // src/lib/posthog.ts
12
- import { createHash } from "node:crypto";
13
- import { PostHog } from "posthog-node";
14
- function hashUserId(userId) {
15
- return createHash("sha256").update(`${POSTHOG_SALT}:${userId}`).digest("hex").substring(0, 32);
16
- }
17
- function initPostHog() {
18
- if (isTelemetryDisabled()) return;
19
- const key = process.env.POSTHOG_KEY || process.env.VITE_POSTHOG_KEY;
20
- const host = process.env.POSTHOG_HOST || process.env.VITE_POSTHOG_HOST || "https://eu.i.posthog.com";
21
- if (!key) return;
22
- client = new PostHog(key, { host, flushAt: 5, flushInterval: 1e4 });
23
- }
24
- async function captureToolEvent(args) {
25
- if (!client) return;
26
- let userId;
27
- try {
28
- userId = await getDefaultUserId();
29
- } catch {
30
- userId = "anonymous_mcp";
31
- }
32
- client.capture({
33
- distinctId: hashUserId(userId),
34
- event: `mcp_tool_${args.status}`,
35
- properties: {
36
- tool_name: args.toolName,
37
- duration_ms: args.durationMs,
38
- ...args.details
39
- }
40
- });
41
- }
42
- async function shutdownPostHog() {
43
- if (client) {
44
- await client.shutdown();
45
- client = null;
46
- }
47
- }
48
- var POSTHOG_SALT, client;
49
- var init_posthog = __esm({
50
- "src/lib/posthog.ts"() {
51
- "use strict";
52
- init_supabase();
53
- POSTHOG_SALT = "socialneuron-mcp-ph-v1";
54
- client = null;
55
- }
56
- });
57
-
58
16
  // src/lib/request-context.ts
59
17
  import { AsyncLocalStorage } from "node:async_hooks";
60
18
  function getRequestUserId() {
@@ -291,83 +249,12 @@ var init_credentials = __esm({
291
249
  }
292
250
  });
293
251
 
294
- // src/lib/validation-cache.ts
295
- var validation_cache_exports = {};
296
- __export(validation_cache_exports, {
297
- clearValidationCache: () => clearValidationCache,
298
- keyFingerprint: () => keyFingerprint,
299
- readValidationCache: () => readValidationCache,
300
- writeValidationCache: () => writeValidationCache
301
- });
302
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, chmodSync } from "node:fs";
303
- import { join as join2 } from "node:path";
304
- import { homedir as homedir2 } from "node:os";
305
- function keyFingerprint(apiKey) {
306
- return apiKey.substring(0, 6) + "..." + apiKey.slice(-4);
307
- }
308
- function readValidationCache(apiKey) {
309
- try {
310
- const raw = readFileSync2(CACHE_FILE, "utf-8");
311
- const cached = JSON.parse(raw);
312
- if (cached.keyFingerprint !== keyFingerprint(apiKey)) {
313
- return null;
314
- }
315
- if (Date.now() - cached.cachedAt > CACHE_TTL_MS) {
316
- return null;
317
- }
318
- if (!cached.result.valid) {
319
- return null;
320
- }
321
- if (cached.result.expiresAt) {
322
- const expiresMs = new Date(cached.result.expiresAt).getTime();
323
- if (expiresMs <= Date.now()) {
324
- return null;
325
- }
326
- }
327
- return cached.result;
328
- } catch {
329
- return null;
330
- }
331
- }
332
- function writeValidationCache(apiKey, result) {
333
- if (!result.valid) return;
334
- try {
335
- mkdirSync2(CONFIG_DIR2, { recursive: true });
336
- const entry = {
337
- keyFingerprint: keyFingerprint(apiKey),
338
- result,
339
- cachedAt: Date.now()
340
- };
341
- writeFileSync2(CACHE_FILE, JSON.stringify(entry, null, 2), { mode: 384 });
342
- try {
343
- chmodSync(CACHE_FILE, 384);
344
- } catch {
345
- }
346
- } catch {
347
- }
348
- }
349
- function clearValidationCache() {
350
- try {
351
- writeFileSync2(CACHE_FILE, "{}", { mode: 384 });
352
- } catch {
353
- }
354
- }
355
- var CONFIG_DIR2, CACHE_FILE, CACHE_TTL_MS;
356
- var init_validation_cache = __esm({
357
- "src/lib/validation-cache.ts"() {
358
- "use strict";
359
- CONFIG_DIR2 = join2(homedir2(), ".config", "socialneuron");
360
- CACHE_FILE = join2(CONFIG_DIR2, "validation-cache.json");
361
- CACHE_TTL_MS = 5 * 60 * 1e3;
362
- }
363
- });
364
-
365
252
  // src/auth/api-keys.ts
366
253
  var api_keys_exports = {};
367
254
  __export(api_keys_exports, {
368
255
  validateApiKey: () => validateApiKey
369
256
  });
370
- async function validateApiKey(apiKey) {
257
+ async function validateApiKey(apiKey, _attempt = 0) {
371
258
  const supabaseUrl = getSupabaseUrl();
372
259
  try {
373
260
  const anonKey = process.env.SUPABASE_ANON_KEY || process.env.SOCIALNEURON_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY || CLOUD_SUPABASE_ANON_KEY;
@@ -384,21 +271,90 @@ async function validateApiKey(apiKey) {
384
271
  );
385
272
  if (!response.ok) {
386
273
  const text = await response.text();
387
- return { valid: false, error: `Validation failed: ${text}` };
274
+ const retryable = response.status === 429 || response.status >= 500;
275
+ if (retryable && _attempt < VALIDATE_MAX_RETRIES) {
276
+ await sleep(300 * (_attempt + 1));
277
+ return validateApiKey(apiKey, _attempt + 1);
278
+ }
279
+ return {
280
+ valid: false,
281
+ retryable,
282
+ error: `Validation failed (HTTP ${response.status}): ${text}`
283
+ };
388
284
  }
389
- const result = await response.json();
390
- return result;
285
+ return await response.json();
391
286
  } catch (err) {
287
+ if (_attempt < VALIDATE_MAX_RETRIES) {
288
+ await sleep(300 * (_attempt + 1));
289
+ return validateApiKey(apiKey, _attempt + 1);
290
+ }
392
291
  return {
393
292
  valid: false,
293
+ retryable: true,
394
294
  error: err instanceof Error ? err.message : String(err)
395
295
  };
396
296
  }
397
297
  }
298
+ var VALIDATE_MAX_RETRIES, sleep;
398
299
  var init_api_keys = __esm({
399
300
  "src/auth/api-keys.ts"() {
400
301
  "use strict";
401
302
  init_supabase();
303
+ VALIDATE_MAX_RETRIES = 2;
304
+ sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
305
+ }
306
+ });
307
+
308
+ // src/lib/posthog.ts
309
+ var posthog_exports = {};
310
+ __export(posthog_exports, {
311
+ captureToolEvent: () => captureToolEvent,
312
+ initPostHog: () => initPostHog,
313
+ shutdownPostHog: () => shutdownPostHog
314
+ });
315
+ import { createHash } from "node:crypto";
316
+ import { PostHog } from "posthog-node";
317
+ function hashUserId(userId) {
318
+ return createHash("sha256").update(`${POSTHOG_SALT}:${userId}`).digest("hex").substring(0, 32);
319
+ }
320
+ function initPostHog() {
321
+ if (isTelemetryDisabled()) return;
322
+ const key = process.env.POSTHOG_KEY || process.env.VITE_POSTHOG_KEY;
323
+ const host = process.env.POSTHOG_HOST || process.env.VITE_POSTHOG_HOST || "https://eu.i.posthog.com";
324
+ if (!key) return;
325
+ client = new PostHog(key, { host, flushAt: 5, flushInterval: 1e4 });
326
+ }
327
+ async function captureToolEvent(args) {
328
+ if (!client) return;
329
+ let userId;
330
+ try {
331
+ userId = await getDefaultUserId();
332
+ } catch {
333
+ userId = "anonymous_mcp";
334
+ }
335
+ client.capture({
336
+ distinctId: hashUserId(userId),
337
+ event: `mcp_tool_${args.status}`,
338
+ properties: {
339
+ tool_name: args.toolName,
340
+ duration_ms: args.durationMs,
341
+ ...args.details
342
+ }
343
+ });
344
+ }
345
+ async function shutdownPostHog() {
346
+ if (client) {
347
+ await client.shutdown();
348
+ client = null;
349
+ }
350
+ }
351
+ var POSTHOG_SALT, client;
352
+ var init_posthog = __esm({
353
+ "src/lib/posthog.ts"() {
354
+ "use strict";
355
+ init_supabase();
356
+ POSTHOG_SALT = "socialneuron-mcp-ph-v1";
357
+ client = null;
402
358
  }
403
359
  });
404
360
 
@@ -464,8 +420,8 @@ async function getDefaultUserId() {
464
420
  async function getDefaultProjectId() {
465
421
  const userId = await getDefaultUserId().catch(() => null);
466
422
  if (userId) {
467
- const cached = projectIdCache.get(userId);
468
- if (cached) return cached;
423
+ const cached2 = projectIdCache.get(userId);
424
+ if (cached2) return cached2;
469
425
  }
470
426
  const envProjectId = process.env.SOCIALNEURON_PROJECT_ID;
471
427
  if (envProjectId) {
@@ -475,7 +431,10 @@ async function getDefaultProjectId() {
475
431
  if (!userId) return null;
476
432
  try {
477
433
  const supabase = getSupabaseClient();
478
- const { data } = await supabase.from("projects").select("id").eq("user_id", userId).order("created_at", { ascending: false }).limit(1).single();
434
+ const { data: memberships } = await supabase.from("organization_members").select("organization_id").eq("user_id", userId);
435
+ const orgIds = (memberships ?? []).map((m) => m.organization_id);
436
+ if (orgIds.length === 0) return null;
437
+ const { data } = await supabase.from("projects").select("id").in("organization_id", orgIds).order("created_at", { ascending: false }).limit(1).maybeSingle();
479
438
  if (data?.id) {
480
439
  projectIdCache.set(userId, data.id);
481
440
  return data.id;
@@ -489,33 +448,27 @@ async function initializeAuth() {
489
448
  const apiKey = await loadApiKey2();
490
449
  if (apiKey) {
491
450
  authenticatedApiKey = apiKey;
492
- const { readValidationCache: readValidationCache2, writeValidationCache: writeValidationCache2 } = await Promise.resolve().then(() => (init_validation_cache(), validation_cache_exports));
493
- const cached = readValidationCache2(apiKey);
494
- let result;
495
- if (cached) {
496
- result = cached;
497
- console.error("[MCP] Using cached validation (skip remote check)");
498
- } else {
499
- const { validateApiKey: validateApiKey2 } = await Promise.resolve().then(() => (init_api_keys(), api_keys_exports));
500
- result = await validateApiKey2(apiKey);
501
- if (result.valid) {
502
- writeValidationCache2(apiKey, result);
503
- }
504
- }
451
+ const _quietAuth = !process.argv.includes("--verbose") && (process.env.SN_CLI_QUIET === "1" || ["setup", "login", "logout", "whoami", "health", "sn", "repl"].includes(
452
+ process.argv[2] ?? ""
453
+ ));
454
+ const { validateApiKey: validateApiKey2 } = await Promise.resolve().then(() => (init_api_keys(), api_keys_exports));
455
+ const result = await validateApiKey2(apiKey);
505
456
  if (result.valid && result.userId) {
506
457
  _authMode = "api-key";
507
458
  authenticatedUserId = result.userId;
508
459
  authenticatedScopes = result.scopes && result.scopes.length > 0 ? result.scopes : ["mcp:read"];
509
460
  authenticatedEmail = result.email || null;
510
461
  authenticatedExpiresAt = result.expiresAt || null;
511
- console.error(
512
- "[MCP] Authenticated via API key (prefix: " + apiKey.substring(0, 6) + "..." + apiKey.slice(-4) + ")"
513
- );
514
- console.error("[MCP] Scopes: " + authenticatedScopes.join(", "));
462
+ if (!_quietAuth) {
463
+ console.error(
464
+ "[MCP] Authenticated via API key (prefix: " + apiKey.substring(0, 6) + "..." + apiKey.slice(-4) + ")"
465
+ );
466
+ console.error("[MCP] Scopes: " + authenticatedScopes.join(", "));
467
+ }
515
468
  if (authenticatedExpiresAt) {
516
469
  const expiresMs = new Date(authenticatedExpiresAt).getTime();
517
470
  const daysLeft = Math.ceil((expiresMs - Date.now()) / (1e3 * 60 * 60 * 24));
518
- console.error("[MCP] Key expires: " + authenticatedExpiresAt);
471
+ if (!_quietAuth) console.error("[MCP] Key expires: " + authenticatedExpiresAt);
519
472
  if (daysLeft <= 7) {
520
473
  console.error(
521
474
  `[MCP] Warning: API key expires in ${daysLeft} day(s). Run: npx @socialneuron/mcp-server login`
@@ -525,8 +478,13 @@ async function initializeAuth() {
525
478
  return;
526
479
  } else {
527
480
  authenticatedApiKey = null;
481
+ if (result.retryable) {
482
+ throw new Error(
483
+ "Temporary issue reaching the auth service \u2014 your session is likely still valid. Wait a moment and retry. If it persists, run `sn login`."
484
+ );
485
+ }
528
486
  throw new Error(
529
- "[MCP] Fatal: API key invalid or expired. Run: npx @socialneuron/mcp-server setup"
487
+ "API key invalid, expired, or revoked. Run `sn login` to reconnect (or `socialneuron-mcp login`)."
530
488
  );
531
489
  }
532
490
  }
@@ -583,14 +541,13 @@ async function logMcpToolInvocation(args) {
583
541
  });
584
542
  } catch {
585
543
  }
586
- captureToolEvent(args).catch(() => {
544
+ Promise.resolve().then(() => (init_posthog(), posthog_exports)).then(({ captureToolEvent: captureToolEvent2 }) => captureToolEvent2(args)).catch(() => {
587
545
  });
588
546
  }
589
547
  var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedEmail, authenticatedExpiresAt, authenticatedApiKey, MCP_RUN_ID, CLOUD_SUPABASE_URL, CLOUD_SUPABASE_ANON_KEY, projectIdCache;
590
548
  var init_supabase = __esm({
591
549
  "src/lib/supabase.ts"() {
592
550
  "use strict";
593
- init_posthog();
594
551
  init_request_context();
595
552
  SUPABASE_URL = process.env.SOCIALNEURON_SUPABASE_URL || process.env.SUPABASE_URL || "";
596
553
  SUPABASE_SERVICE_KEY = process.env.SOCIALNEURON_SERVICE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY || "";
@@ -611,7 +568,7 @@ var init_supabase = __esm({
611
568
  // src/http.ts
612
569
  import express from "express";
613
570
  import { randomUUID as randomUUID4 } from "node:crypto";
614
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
571
+ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
615
572
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
616
573
  import {
617
574
  mcpAuthRouter,
@@ -720,6 +677,8 @@ var TOOL_SCOPES = {
720
677
  get_mcp_usage: "mcp:read",
721
678
  list_plan_approvals: "mcp:read",
722
679
  search_tools: "mcp:read",
680
+ search: "mcp:read",
681
+ fetch: "mcp:read",
723
682
  // mcp:read (pipeline readiness + status are read-only)
724
683
  check_pipeline_readiness: "mcp:read",
725
684
  get_pipeline_status: "mcp:read",
@@ -747,7 +706,13 @@ var TOOL_SCOPES = {
747
706
  record_intel_signal: "mcp:write",
748
707
  record_campaign_spend: "mcp:write",
749
708
  // mcp:read
750
- get_active_campaigns: "mcp:read"
709
+ get_active_campaigns: "mcp:read",
710
+ // mcp:read / mcp:write (Skills)
711
+ list_skills: "mcp:read",
712
+ run_skill: "mcp:write",
713
+ // mcp:read (Loop observability — growth-loop KPIs + content learning state)
714
+ get_loop_pulse: "mcp:read",
715
+ get_bandit_state: "mcp:read"
751
716
  };
752
717
  function hasScope(userScopes, required) {
753
718
  if (userScopes.includes(required)) return true;
@@ -847,6 +812,10 @@ var OVERRIDES = {
847
812
  generate_performance_digest: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
848
813
  detect_anomalies: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
849
814
  };
815
+ function buildToolSecuritySchemes(toolName) {
816
+ const scope = TOOL_SCOPES[toolName];
817
+ return scope ? [{ type: "oauth2", scopes: [scope] }] : [];
818
+ }
850
819
  function buildAnnotationsMap() {
851
820
  const map = /* @__PURE__ */ new Map();
852
821
  for (const [toolName, scope] of Object.entries(TOOL_SCOPES)) {
@@ -891,17 +860,37 @@ function applyAnnotations(server) {
891
860
  destructiveHint: ann.destructiveHint,
892
861
  idempotentHint: ann.idempotentHint,
893
862
  openWorldHint: ann.openWorldHint
894
- }
863
+ },
864
+ securitySchemes: buildToolSecuritySchemes(toolName)
895
865
  });
896
866
  applied++;
897
867
  }
898
868
  }
899
- console.log(`[annotations] Applied annotations to ${applied}/${entries.length} tools`);
869
+ console.error(`[annotations] Applied annotations to ${applied}/${entries.length} tools`);
900
870
  }
901
871
 
902
872
  // src/lib/register-tools.ts
903
873
  init_supabase();
904
874
 
875
+ // src/lib/www-authenticate.ts
876
+ function buildWwwAuthenticateHeader(opts) {
877
+ const SANITIZE_CONTROL = /[\x00-\x1f\x7f]/g;
878
+ const sanitize = (s) => s.replace(/[\\"]/g, "_").replace(SANITIZE_CONTROL, " ");
879
+ const params = ['realm="socialneuron"'];
880
+ if (opts.error) {
881
+ params.push(`error="${sanitize(opts.error)}"`);
882
+ }
883
+ if (opts.errorDescription) {
884
+ params.push(`error_description="${sanitize(opts.errorDescription)}"`);
885
+ }
886
+ if (opts.scope) {
887
+ params.push(`scope="${sanitize(opts.scope)}"`);
888
+ }
889
+ const issuer = opts.issuerUrl.replace(/\/+$/, "");
890
+ params.push(`resource_metadata="${issuer}/.well-known/oauth-protected-resource"`);
891
+ return `Bearer ${params.join(", ")}`;
892
+ }
893
+
905
894
  // src/lib/agent-harness/constants.json
906
895
  var constants_default = {
907
896
  ZERO_WIDTH_CHARS: "[\\u200B\\u200C\\u200D\\u2060\\uFEFF\\u{E0020}-\\u{E007F}]",
@@ -1112,10 +1101,16 @@ async function callEdgeFunction(functionName, body, options) {
1112
1101
  } catch {
1113
1102
  errorMessage = responseText || `HTTP ${response.status}`;
1114
1103
  }
1115
- if (response.status === 401 || response.status === 403) {
1104
+ if (response.status === 401) {
1105
+ return {
1106
+ data: null,
1107
+ error: `Authentication failed (HTTP 401). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
1108
+ };
1109
+ }
1110
+ if (response.status === 403) {
1116
1111
  return {
1117
1112
  data: null,
1118
- error: `Authentication failed (HTTP ${response.status}). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
1113
+ error: `Forbidden (HTTP 403): ${errorMessage}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
1119
1114
  };
1120
1115
  }
1121
1116
  if (response.status === 429) {
@@ -1216,6 +1211,20 @@ function checkRateLimit(category, key) {
1216
1211
 
1217
1212
  // src/tools/ideation.ts
1218
1213
  init_supabase();
1214
+
1215
+ // src/lib/version.ts
1216
+ var MCP_VERSION = "1.7.15";
1217
+
1218
+ // src/tools/ideation.ts
1219
+ function asEnvelope(data) {
1220
+ return {
1221
+ _meta: {
1222
+ version: MCP_VERSION,
1223
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1224
+ },
1225
+ data
1226
+ };
1227
+ }
1219
1228
  function registerIdeationTools(server) {
1220
1229
  server.tool(
1221
1230
  "generate_content",
@@ -1377,7 +1386,14 @@ Content Type: ${content_type}`;
1377
1386
  };
1378
1387
  }
1379
1388
  const text = data?.text ?? "(empty response)";
1389
+ const structuredContent = asEnvelope({
1390
+ text,
1391
+ content_type,
1392
+ platform: platform2 ?? null,
1393
+ model: model ?? "gemini-2.5-flash"
1394
+ });
1380
1395
  return {
1396
+ structuredContent,
1381
1397
  content: [{ type: "text", text }]
1382
1398
  };
1383
1399
  }
@@ -1411,7 +1427,10 @@ Content Type: ${content_type}`;
1411
1427
  const { data, error } = await callEdgeFunction(
1412
1428
  "fetch-trends",
1413
1429
  {
1414
- source,
1430
+ // Forward as `trend_source`: the mcp-gateway overwrites top-level `source`
1431
+ // with 'mcp' (attribution) before reaching the EF, which collided with the
1432
+ // routing param. The EF reads `trend_source ?? source`.
1433
+ trend_source: source,
1415
1434
  category: category ?? "general",
1416
1435
  niche,
1417
1436
  url,
@@ -1580,7 +1599,14 @@ ${content}`,
1580
1599
  const header = `Adapted for ${target_platform}${source_platform ? ` (from ${source_platform})` : ""}:
1581
1600
 
1582
1601
  `;
1602
+ const structuredContent = asEnvelope({
1603
+ text,
1604
+ source_platform: source_platform ?? null,
1605
+ target_platform,
1606
+ model: "gemini-2.5-flash"
1607
+ });
1583
1608
  return {
1609
+ structuredContent,
1584
1610
  content: [{ type: "text", text: header + text }]
1585
1611
  };
1586
1612
  }
@@ -1590,12 +1616,9 @@ ${content}`,
1590
1616
  // src/tools/content.ts
1591
1617
  import { z as z2 } from "zod";
1592
1618
  init_supabase();
1593
- init_request_context();
1594
-
1595
- // src/lib/version.ts
1596
- var MCP_VERSION = "1.7.7";
1597
1619
 
1598
- // src/tools/content.ts
1620
+ // src/lib/budget.ts
1621
+ init_request_context();
1599
1622
  var MAX_CREDITS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
1600
1623
  var MAX_ASSETS_PER_RUN = Math.max(0, Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0));
1601
1624
  var _globalCreditsUsed = 0;
@@ -1636,7 +1659,35 @@ function getCurrentBudgetStatus() {
1636
1659
  remainingAssets: MAX_ASSETS_PER_RUN > 0 ? Math.max(0, MAX_ASSETS_PER_RUN - assetsGen) : null
1637
1660
  };
1638
1661
  }
1639
- function asEnvelope(data) {
1662
+ function checkCreditBudget(estimatedCost) {
1663
+ if (MAX_CREDITS_PER_RUN <= 0) {
1664
+ return { ok: true };
1665
+ }
1666
+ const used = getCreditsUsed();
1667
+ if (used + estimatedCost > MAX_CREDITS_PER_RUN) {
1668
+ return {
1669
+ ok: false,
1670
+ message: `Credit budget exceeded for this MCP run. Used=${used}, next~=${estimatedCost}, limit=${MAX_CREDITS_PER_RUN}.`
1671
+ };
1672
+ }
1673
+ return { ok: true };
1674
+ }
1675
+ function checkAssetBudget(requestedCount = 1) {
1676
+ if (MAX_ASSETS_PER_RUN <= 0) {
1677
+ return { ok: true };
1678
+ }
1679
+ const generated = getAssetsGenerated();
1680
+ if (generated + requestedCount > MAX_ASSETS_PER_RUN) {
1681
+ return {
1682
+ ok: false,
1683
+ message: `Asset budget exceeded for this MCP run. Generated=${generated}, next=${requestedCount}, limit=${MAX_ASSETS_PER_RUN}.`
1684
+ };
1685
+ }
1686
+ return { ok: true };
1687
+ }
1688
+
1689
+ // src/tools/content.ts
1690
+ function asEnvelope2(data) {
1640
1691
  return {
1641
1692
  _meta: {
1642
1693
  version: MCP_VERSION,
@@ -1663,35 +1714,9 @@ var IMAGE_CREDIT_ESTIMATES = {
1663
1714
  "flux-max": 50,
1664
1715
  "gpt4o-image": 40,
1665
1716
  imagen4: 35,
1666
- "imagen4-fast": 25,
1717
+ "imagen4-fast": 35,
1667
1718
  seedream: 20
1668
1719
  };
1669
- function checkCreditBudget(estimatedCost) {
1670
- if (MAX_CREDITS_PER_RUN <= 0) {
1671
- return { ok: true };
1672
- }
1673
- const used = getCreditsUsed();
1674
- if (used + estimatedCost > MAX_CREDITS_PER_RUN) {
1675
- return {
1676
- ok: false,
1677
- message: `Credit budget exceeded for this MCP run. Used=${used}, next~=${estimatedCost}, limit=${MAX_CREDITS_PER_RUN}.`
1678
- };
1679
- }
1680
- return { ok: true };
1681
- }
1682
- function checkAssetBudget() {
1683
- if (MAX_ASSETS_PER_RUN <= 0) {
1684
- return { ok: true };
1685
- }
1686
- const generated = getAssetsGenerated();
1687
- if (generated + 1 > MAX_ASSETS_PER_RUN) {
1688
- return {
1689
- ok: false,
1690
- message: `Asset budget exceeded for this MCP run. Generated=${generated}, next=1, limit=${MAX_ASSETS_PER_RUN}.`
1691
- };
1692
- }
1693
- return { ok: true };
1694
- }
1695
1720
  function registerContentTools(server) {
1696
1721
  server.tool(
1697
1722
  "generate_video",
@@ -1810,7 +1835,7 @@ function registerContentTools(server) {
1810
1835
  {
1811
1836
  type: "text",
1812
1837
  text: JSON.stringify(
1813
- asEnvelope({
1838
+ asEnvelope2({
1814
1839
  jobId,
1815
1840
  taskId: data.taskId,
1816
1841
  asyncJobId: data.asyncJobId,
@@ -1940,7 +1965,7 @@ function registerContentTools(server) {
1940
1965
  {
1941
1966
  type: "text",
1942
1967
  text: JSON.stringify(
1943
- asEnvelope({
1968
+ asEnvelope2({
1944
1969
  jobId,
1945
1970
  taskId: data.taskId,
1946
1971
  asyncJobId: data.asyncJobId,
@@ -2043,7 +2068,7 @@ function registerContentTools(server) {
2043
2068
  {
2044
2069
  type: "text",
2045
2070
  text: JSON.stringify(
2046
- asEnvelope({
2071
+ asEnvelope2({
2047
2072
  jobId: job.id,
2048
2073
  jobType: job.job_type,
2049
2074
  model: job.model,
@@ -2104,7 +2129,7 @@ function registerContentTools(server) {
2104
2129
  all_urls: allUrls ?? null
2105
2130
  };
2106
2131
  return {
2107
- content: [{ type: "text", text: JSON.stringify(asEnvelope(enriched), null, 2) }]
2132
+ content: [{ type: "text", text: JSON.stringify(asEnvelope2(enriched), null, 2) }]
2108
2133
  };
2109
2134
  }
2110
2135
  return {
@@ -2233,7 +2258,7 @@ Return ONLY valid JSON in this exact format:
2233
2258
  try {
2234
2259
  const parsed = JSON.parse(rawContent);
2235
2260
  return {
2236
- content: [{ type: "text", text: JSON.stringify(asEnvelope(parsed), null, 2) }]
2261
+ content: [{ type: "text", text: JSON.stringify(asEnvelope2(parsed), null, 2) }]
2237
2262
  };
2238
2263
  } catch {
2239
2264
  return {
@@ -2261,20 +2286,8 @@ Return ONLY valid JSON in this exact format:
2261
2286
  "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.",
2262
2287
  {
2263
2288
  text: z2.string().max(5e3).describe("The script/text to convert to speech."),
2264
- voice: z2.enum([
2265
- "rachel",
2266
- "drew",
2267
- "clyde",
2268
- "paul",
2269
- "domi",
2270
- "dave",
2271
- "fin",
2272
- "sarah",
2273
- "antoni",
2274
- "thomas",
2275
- "charlie"
2276
- ]).optional().describe(
2277
- "Voice selection. rachel=warm female, drew=confident male, paul=authoritative male, sarah=friendly female. Defaults to rachel."
2289
+ voice: z2.enum(["rachel", "domi"]).optional().describe(
2290
+ "Voice selection. rachel=warm female, domi=confident female. Defaults to rachel."
2278
2291
  ),
2279
2292
  speed: z2.number().min(0.5).max(2).optional().describe("Speech speed multiplier. 1.0 is normal. Defaults to 1.0."),
2280
2293
  response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
@@ -2302,11 +2315,15 @@ Return ONLY valid JSON in this exact format:
2302
2315
  isError: true
2303
2316
  };
2304
2317
  }
2318
+ const ELEVENLABS_VOICE_IDS = {
2319
+ rachel: "21m00Tcm4TlvDq8ikWAM",
2320
+ domi: "AZnzlk1XvdvUeBnXmlld"
2321
+ };
2305
2322
  const { data, error } = await callEdgeFunction(
2306
2323
  "elevenlabs-tts",
2307
2324
  {
2308
2325
  text,
2309
- voice: voice ?? "rachel",
2326
+ voiceId: ELEVENLABS_VOICE_IDS[voice ?? "rachel"],
2310
2327
  speed: speed ?? 1
2311
2328
  },
2312
2329
  { timeoutMs: 6e4 }
@@ -2332,7 +2349,7 @@ Return ONLY valid JSON in this exact format:
2332
2349
  {
2333
2350
  type: "text",
2334
2351
  text: JSON.stringify(
2335
- asEnvelope({
2352
+ asEnvelope2({
2336
2353
  audioUrl: data.audioUrl,
2337
2354
  durationSeconds: data.durationSeconds,
2338
2355
  voice: voice ?? "rachel"
@@ -2481,7 +2498,7 @@ Return ONLY valid JSON in this exact format:
2481
2498
  {
2482
2499
  type: "text",
2483
2500
  text: JSON.stringify(
2484
- asEnvelope({
2501
+ asEnvelope2({
2485
2502
  carouselId: data.carousel.id,
2486
2503
  templateId,
2487
2504
  style: resolvedStyle,
@@ -2871,7 +2888,7 @@ var PLATFORM_CASE_MAP = {
2871
2888
  };
2872
2889
  var TIKTOK_AUDIT_APPROVED = false;
2873
2890
  var MCP_NOT_LIVE_FOR_POSTING = /* @__PURE__ */ new Set(["LinkedIn", "Pinterest", "Reddit"]);
2874
- function asEnvelope2(data) {
2891
+ function asEnvelope3(data) {
2875
2892
  return {
2876
2893
  _meta: {
2877
2894
  version: MCP_VERSION,
@@ -2969,7 +2986,24 @@ function registerDistributionTools(server) {
2969
2986
  linkedin: z3.object({
2970
2987
  article_url: z3.string().optional()
2971
2988
  }).optional(),
2972
- twitter: z3.object({}).passthrough().optional()
2989
+ twitter: z3.object({
2990
+ paid_partnership: z3.boolean().optional().describe(
2991
+ "Set true for sponsored, affiliate, or branded campaign content. Forwarded to X as paid_partnership and persisted on posts.metadata.paid_partnership."
2992
+ ),
2993
+ disclosure: z3.union([
2994
+ z3.string(),
2995
+ z3.object({
2996
+ label: z3.string().optional(),
2997
+ text: z3.string().optional(),
2998
+ source: z3.string().optional(),
2999
+ required: z3.boolean().optional()
3000
+ }).passthrough()
3001
+ ]).optional().describe(
3002
+ "Campaign disclosure metadata for X posts. String values become disclosure.text; object values persist to posts.metadata.disclosure."
3003
+ ),
3004
+ disclosure_text: z3.string().optional(),
3005
+ disclosure_label: z3.string().optional()
3006
+ }).passthrough().optional()
2973
3007
  }).optional().describe(
2974
3008
  'Platform-specific metadata. Example: {"tiktok":{"privacy_status":"PUBLIC_TO_EVERYONE"}, "youtube":{"title":"My Video"}}'
2975
3009
  ),
@@ -3013,11 +3047,11 @@ function registerDistributionTools(server) {
3013
3047
  visual_gate_result: z3.object({ passed: z3.boolean() }).passthrough().optional().describe(
3014
3048
  "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."
3015
3049
  ),
3016
- origin: z3.literal("hermes").optional().describe(
3017
- "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."
3050
+ origin: z3.enum(["human", "hermes", "user"]).optional().describe(
3051
+ "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.)."
3018
3052
  ),
3019
3053
  hermes_run_id: z3.string().optional().describe(
3020
- "Hermes cron run identifier, persisted alongside origin='hermes' for traceability (e.g. cron_<id>_<timestamp>). Ignored unless origin=hermes."
3054
+ "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."
3021
3055
  )
3022
3056
  },
3023
3057
  async ({
@@ -3300,11 +3334,11 @@ Created with Social Neuron`;
3300
3334
  // Forward visual gate verdict + source so the EF can enforce on media posts.
3301
3335
  ...visual_gate_result ? { visualGateResult: visual_gate_result } : {},
3302
3336
  visualGateSource: "mcp",
3303
- // Forward autonomous-agent provenance for hermes_watch_results accounting.
3304
- // EF gates origin='hermes' on isServiceRoleCall && body.source==='mcp' both true
3305
- // for mcp-gateway-routed calls — so JWT/frontend callers can never claim it.
3337
+ // Originator lineage (Hermes integration, 2026-05-22). EF validates origin
3338
+ // against the posts.origin CHECK constraint and persists hermesRunId when
3339
+ // origin === 'hermes'.
3306
3340
  ...origin ? { origin } : {},
3307
- ...hermes_run_id ? { hermes_run_id } : {}
3341
+ ...hermes_run_id ? { hermesRunId: hermes_run_id } : {}
3308
3342
  },
3309
3343
  { timeoutMs: 3e4 }
3310
3344
  );
@@ -3349,12 +3383,15 @@ Created with Social Neuron`;
3349
3383
  }
3350
3384
  }
3351
3385
  if (format === "json") {
3386
+ const structuredContent = asEnvelope3(data);
3352
3387
  return {
3353
- content: [{ type: "text", text: JSON.stringify(asEnvelope2(data), null, 2) }],
3388
+ structuredContent,
3389
+ content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }],
3354
3390
  isError: !data.success
3355
3391
  };
3356
3392
  }
3357
3393
  return {
3394
+ structuredContent: asEnvelope3(data),
3358
3395
  content: [{ type: "text", text: lines.join("\n") }],
3359
3396
  isError: !data.success
3360
3397
  };
@@ -3387,7 +3424,7 @@ Created with Social Neuron`;
3387
3424
  content: [
3388
3425
  {
3389
3426
  type: "text",
3390
- text: JSON.stringify(asEnvelope2({ accounts: [] }), null, 2)
3427
+ text: JSON.stringify(asEnvelope3({ accounts: [] }), null, 2)
3391
3428
  }
3392
3429
  ]
3393
3430
  };
@@ -3410,7 +3447,7 @@ Created with Social Neuron`;
3410
3447
  if (format === "json") {
3411
3448
  return {
3412
3449
  content: [
3413
- { type: "text", text: JSON.stringify(asEnvelope2({ accounts }), null, 2) }
3450
+ { type: "text", text: JSON.stringify(asEnvelope3({ accounts }), null, 2) }
3414
3451
  ]
3415
3452
  };
3416
3453
  }
@@ -3462,10 +3499,10 @@ Created with Social Neuron`;
3462
3499
  const rows = result.posts ?? [];
3463
3500
  if (rows.length === 0) {
3464
3501
  if (format === "json") {
3502
+ const structuredContent2 = asEnvelope3({ posts: [] });
3465
3503
  return {
3466
- content: [
3467
- { type: "text", text: JSON.stringify(asEnvelope2({ posts: [] }), null, 2) }
3468
- ]
3504
+ structuredContent: structuredContent2,
3505
+ content: [{ type: "text", text: JSON.stringify(structuredContent2, null, 2) }]
3469
3506
  };
3470
3507
  }
3471
3508
  return {
@@ -3478,11 +3515,11 @@ Created with Social Neuron`;
3478
3515
  };
3479
3516
  }
3480
3517
  const posts = rows;
3518
+ const structuredContent = asEnvelope3({ posts });
3481
3519
  if (format === "json") {
3482
3520
  return {
3483
- content: [
3484
- { type: "text", text: JSON.stringify(asEnvelope2({ posts }), null, 2) }
3485
- ]
3521
+ structuredContent,
3522
+ content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }]
3486
3523
  };
3487
3524
  }
3488
3525
  const statusIcon = {
@@ -3507,6 +3544,7 @@ Created with Social Neuron`;
3507
3544
  lines.push(line);
3508
3545
  }
3509
3546
  return {
3547
+ structuredContent,
3510
3548
  content: [{ type: "text", text: lines.join("\n") }]
3511
3549
  };
3512
3550
  }
@@ -3590,7 +3628,7 @@ Created with Social Neuron`;
3590
3628
  {
3591
3629
  type: "text",
3592
3630
  text: JSON.stringify(
3593
- asEnvelope2({
3631
+ asEnvelope3({
3594
3632
  slots,
3595
3633
  total_candidates: candidates.length,
3596
3634
  conflicts_avoided: conflictsAvoided
@@ -3879,7 +3917,7 @@ Created with Social Neuron`;
3879
3917
  {
3880
3918
  type: "text",
3881
3919
  text: JSON.stringify(
3882
- asEnvelope2({
3920
+ asEnvelope3({
3883
3921
  dry_run: true,
3884
3922
  plan_id: effectivePlanId,
3885
3923
  approvals: approvalSummary,
@@ -4067,7 +4105,7 @@ Created with Social Neuron`;
4067
4105
  {
4068
4106
  type: "text",
4069
4107
  text: JSON.stringify(
4070
- asEnvelope2({
4108
+ asEnvelope3({
4071
4109
  plan_id: effectivePlanId,
4072
4110
  approvals: approvalSummary,
4073
4111
  posts: results,
@@ -4341,6 +4379,17 @@ function registerMediaTools(server) {
4341
4379
  projectId: project_id
4342
4380
  };
4343
4381
  } else {
4382
+ if (process.env.MCP_TRANSPORT !== "stdio") {
4383
+ return {
4384
+ content: [
4385
+ {
4386
+ type: "text",
4387
+ 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\`.`
4388
+ }
4389
+ ],
4390
+ isError: true
4391
+ };
4392
+ }
4344
4393
  let fileBuffer;
4345
4394
  try {
4346
4395
  fileBuffer = await readFile(src);
@@ -4383,7 +4432,8 @@ function registerMediaTools(server) {
4383
4432
  const putResp = await fetch(putUrl, {
4384
4433
  method: "PUT",
4385
4434
  headers: { "Content-Type": ct },
4386
- body: fileBuffer
4435
+ // Uint8Array: Buffer no longer satisfies BodyInit under @types/node 26 fetch types
4436
+ body: new Uint8Array(fileBuffer)
4387
4437
  });
4388
4438
  if (!putResp.ok) {
4389
4439
  return {
@@ -4565,7 +4615,7 @@ function registerMediaTools(server) {
4565
4615
  // src/tools/analytics.ts
4566
4616
  init_supabase();
4567
4617
  import { z as z5 } from "zod";
4568
- function asEnvelope3(data) {
4618
+ function asEnvelope4(data) {
4569
4619
  return {
4570
4620
  _meta: {
4571
4621
  version: MCP_VERSION,
@@ -4618,22 +4668,20 @@ function registerAnalyticsTools(server) {
4618
4668
  const rows = result?.rows ?? [];
4619
4669
  if (rows.length === 0) {
4620
4670
  if (format === "json") {
4671
+ const structuredContent = asEnvelope4({
4672
+ platform: platform2 ?? null,
4673
+ days: lookbackDays,
4674
+ totalViews: 0,
4675
+ totalEngagement: 0,
4676
+ postCount: 0,
4677
+ posts: []
4678
+ });
4621
4679
  return {
4680
+ structuredContent,
4622
4681
  content: [
4623
4682
  {
4624
4683
  type: "text",
4625
- text: JSON.stringify(
4626
- asEnvelope3({
4627
- platform: platform2 ?? null,
4628
- days: lookbackDays,
4629
- totalViews: 0,
4630
- totalEngagement: 0,
4631
- postCount: 0,
4632
- posts: []
4633
- }),
4634
- null,
4635
- 2
4636
- )
4684
+ text: JSON.stringify(structuredContent, null, 2)
4637
4685
  }
4638
4686
  ]
4639
4687
  };
@@ -4723,20 +4771,18 @@ function registerAnalyticsTools(server) {
4723
4771
  lines.push(` Errors: ${errored}`);
4724
4772
  }
4725
4773
  if (format === "json") {
4774
+ const structuredContent = asEnvelope4({
4775
+ success: true,
4776
+ postsProcessed: result.postsProcessed,
4777
+ queued,
4778
+ errored
4779
+ });
4726
4780
  return {
4781
+ structuredContent,
4727
4782
  content: [
4728
4783
  {
4729
4784
  type: "text",
4730
- text: JSON.stringify(
4731
- asEnvelope3({
4732
- success: true,
4733
- postsProcessed: result.postsProcessed,
4734
- queued,
4735
- errored
4736
- }),
4737
- null,
4738
- 2
4739
- )
4785
+ text: JSON.stringify(structuredContent, null, 2)
4740
4786
  }
4741
4787
  ]
4742
4788
  };
@@ -4746,11 +4792,11 @@ function registerAnalyticsTools(server) {
4746
4792
  );
4747
4793
  }
4748
4794
  function formatAnalytics(summary, days, format) {
4795
+ const structuredContent = asEnvelope4({ ...summary, days });
4749
4796
  if (format === "json") {
4750
4797
  return {
4751
- content: [
4752
- { type: "text", text: JSON.stringify(asEnvelope3({ ...summary, days }), null, 2) }
4753
- ]
4798
+ structuredContent,
4799
+ content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }]
4754
4800
  };
4755
4801
  }
4756
4802
  const lines = [
@@ -4775,6 +4821,7 @@ function formatAnalytics(summary, days, format) {
4775
4821
  }
4776
4822
  }
4777
4823
  return {
4824
+ structuredContent,
4778
4825
  content: [{ type: "text", text: lines.join("\n") }]
4779
4826
  };
4780
4827
  }
@@ -4782,7 +4829,7 @@ function formatAnalytics(summary, days, format) {
4782
4829
  // src/tools/brand.ts
4783
4830
  import { z as z6 } from "zod";
4784
4831
  init_supabase();
4785
- function asEnvelope4(data) {
4832
+ function asEnvelope5(data) {
4786
4833
  return {
4787
4834
  _meta: {
4788
4835
  version: MCP_VERSION,
@@ -4837,8 +4884,10 @@ function registerBrandTools(server) {
4837
4884
  };
4838
4885
  }
4839
4886
  if ((response_format || "text") === "json") {
4887
+ const structuredContent = asEnvelope5(data);
4840
4888
  return {
4841
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(data), null, 2) }]
4889
+ structuredContent,
4890
+ content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }]
4842
4891
  };
4843
4892
  }
4844
4893
  const lines = [
@@ -4879,18 +4928,10 @@ function registerBrandTools(server) {
4879
4928
  },
4880
4929
  async ({ project_id, response_format }) => {
4881
4930
  const projectId = project_id || await getDefaultProjectId();
4882
- if (!projectId) {
4883
- return {
4884
- content: [
4885
- {
4886
- type: "text",
4887
- text: "No project_id provided and no default project is configured."
4888
- }
4889
- ],
4890
- isError: true
4891
- };
4892
- }
4893
- const { data: result, error: efError } = await callEdgeFunction("mcp-data", { action: "brand-profile", projectId });
4931
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
4932
+ action: "brand-profile",
4933
+ ...projectId ? { projectId } : {}
4934
+ });
4894
4935
  if (efError || result && !result.success) {
4895
4936
  return {
4896
4937
  content: [
@@ -4910,20 +4951,24 @@ function registerBrandTools(server) {
4910
4951
  ]
4911
4952
  };
4912
4953
  }
4954
+ const structuredContent = asEnvelope5(data);
4913
4955
  if ((response_format || "text") === "json") {
4914
4956
  return {
4915
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(data), null, 2) }]
4957
+ structuredContent,
4958
+ content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }]
4916
4959
  };
4917
4960
  }
4961
+ const brandContext = data.brand_context;
4918
4962
  const lines = [
4919
4963
  `Active Brand Profile`,
4920
- `Project: ${projectId}`,
4921
- `Brand Name: ${data.brand_name || data.brand_context?.name || "N/A"}`,
4964
+ `Project: ${data.project_id || projectId || "default"}`,
4965
+ `Brand Name: ${data.brand_name || brandContext?.name || "N/A"}`,
4922
4966
  `Version: ${data.version ?? "N/A"}`,
4923
4967
  `Updated: ${data.updated_at || "N/A"}`,
4924
4968
  `Extraction Method: ${data.extraction_method || "manual"}`
4925
4969
  ];
4926
4970
  return {
4971
+ structuredContent,
4927
4972
  content: [{ type: "text", text: lines.join("\n") }]
4928
4973
  };
4929
4974
  }
@@ -4998,7 +5043,7 @@ function registerBrandTools(server) {
4998
5043
  };
4999
5044
  if ((response_format || "text") === "json") {
5000
5045
  return {
5001
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(payload), null, 2) }]
5046
+ content: [{ type: "text", text: JSON.stringify(asEnvelope5(payload), null, 2) }]
5002
5047
  };
5003
5048
  }
5004
5049
  return {
@@ -5088,7 +5133,7 @@ Version: ${payload.version ?? "N/A"}`
5088
5133
  };
5089
5134
  if ((response_format || "text") === "json") {
5090
5135
  return {
5091
- content: [{ type: "text", text: JSON.stringify(asEnvelope4(payload), null, 2) }],
5136
+ content: [{ type: "text", text: JSON.stringify(asEnvelope5(payload), null, 2) }],
5092
5137
  isError: false
5093
5138
  };
5094
5139
  }
@@ -5794,7 +5839,7 @@ var PLATFORM_ENUM = [
5794
5839
  "bluesky"
5795
5840
  ];
5796
5841
  var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
5797
- function asEnvelope5(data) {
5842
+ function asEnvelope6(data) {
5798
5843
  return {
5799
5844
  _meta: {
5800
5845
  version: MCP_VERSION,
@@ -5840,19 +5885,17 @@ function registerInsightsTools(server) {
5840
5885
  }
5841
5886
  if (rows.length === 0) {
5842
5887
  if (format === "json") {
5888
+ const structuredContent2 = asEnvelope6({
5889
+ insights: [],
5890
+ days: lookbackDays,
5891
+ insightType: insight_type ?? null
5892
+ });
5843
5893
  return {
5894
+ structuredContent: structuredContent2,
5844
5895
  content: [
5845
5896
  {
5846
5897
  type: "text",
5847
- text: JSON.stringify(
5848
- asEnvelope5({
5849
- insights: [],
5850
- days: lookbackDays,
5851
- insightType: insight_type ?? null
5852
- }),
5853
- null,
5854
- 2
5855
- )
5898
+ text: JSON.stringify(structuredContent2, null, 2)
5856
5899
  }
5857
5900
  ]
5858
5901
  };
@@ -5867,20 +5910,18 @@ function registerInsightsTools(server) {
5867
5910
  };
5868
5911
  }
5869
5912
  const insights = rows;
5913
+ const structuredContent = asEnvelope6({
5914
+ insights,
5915
+ days: lookbackDays,
5916
+ insightType: insight_type ?? null
5917
+ });
5870
5918
  if (format === "json") {
5871
5919
  return {
5920
+ structuredContent,
5872
5921
  content: [
5873
5922
  {
5874
5923
  type: "text",
5875
- text: JSON.stringify(
5876
- asEnvelope5({
5877
- insights,
5878
- days: lookbackDays,
5879
- insightType: insight_type ?? null
5880
- }),
5881
- null,
5882
- 2
5883
- )
5924
+ text: JSON.stringify(structuredContent, null, 2)
5884
5925
  }
5885
5926
  ]
5886
5927
  };
@@ -5905,6 +5946,7 @@ function registerInsightsTools(server) {
5905
5946
  lines.push(line);
5906
5947
  }
5907
5948
  return {
5949
+ structuredContent,
5908
5950
  content: [{ type: "text", text: lines.join("\n") }]
5909
5951
  };
5910
5952
  }
@@ -5998,7 +6040,7 @@ function registerInsightsTools(server) {
5998
6040
  {
5999
6041
  type: "text",
6000
6042
  text: JSON.stringify(
6001
- asEnvelope5({
6043
+ asEnvelope6({
6002
6044
  platform: platform2 ?? null,
6003
6045
  days: lookbackDays,
6004
6046
  recordsAnalyzed: rows.length,
@@ -6035,7 +6077,7 @@ function registerInsightsTools(server) {
6035
6077
 
6036
6078
  // src/tools/youtube-analytics.ts
6037
6079
  import { z as z10 } from "zod";
6038
- function asEnvelope6(data) {
6080
+ function asEnvelope7(data) {
6039
6081
  return {
6040
6082
  _meta: {
6041
6083
  version: MCP_VERSION,
@@ -6090,7 +6132,7 @@ function registerYouTubeAnalyticsTools(server) {
6090
6132
  {
6091
6133
  type: "text",
6092
6134
  text: JSON.stringify(
6093
- asEnvelope6({ action, startDate: start_date, endDate: end_date, analytics: a }),
6135
+ asEnvelope7({ action, startDate: start_date, endDate: end_date, analytics: a }),
6094
6136
  null,
6095
6137
  2
6096
6138
  )
@@ -6127,7 +6169,7 @@ function registerYouTubeAnalyticsTools(server) {
6127
6169
  {
6128
6170
  type: "text",
6129
6171
  text: JSON.stringify(
6130
- asEnvelope6({
6172
+ asEnvelope7({
6131
6173
  action,
6132
6174
  startDate: start_date,
6133
6175
  endDate: end_date,
@@ -6156,7 +6198,7 @@ function registerYouTubeAnalyticsTools(server) {
6156
6198
  {
6157
6199
  type: "text",
6158
6200
  text: JSON.stringify(
6159
- asEnvelope6({
6201
+ asEnvelope7({
6160
6202
  action,
6161
6203
  videoId: video_id,
6162
6204
  startDate: start_date,
@@ -6196,7 +6238,7 @@ function registerYouTubeAnalyticsTools(server) {
6196
6238
  {
6197
6239
  type: "text",
6198
6240
  text: JSON.stringify(
6199
- asEnvelope6({
6241
+ asEnvelope7({
6200
6242
  action,
6201
6243
  startDate: start_date,
6202
6244
  endDate: end_date,
@@ -6222,7 +6264,7 @@ function registerYouTubeAnalyticsTools(server) {
6222
6264
  }
6223
6265
  if (format === "json") {
6224
6266
  return {
6225
- content: [{ type: "text", text: JSON.stringify(asEnvelope6(result), null, 2) }]
6267
+ content: [{ type: "text", text: JSON.stringify(asEnvelope7(result), null, 2) }]
6226
6268
  };
6227
6269
  }
6228
6270
  return {
@@ -6235,7 +6277,7 @@ function registerYouTubeAnalyticsTools(server) {
6235
6277
  // src/tools/comments.ts
6236
6278
  import { z as z11 } from "zod";
6237
6279
  init_supabase();
6238
- function asEnvelope7(data) {
6280
+ function asEnvelope8(data) {
6239
6281
  return {
6240
6282
  _meta: {
6241
6283
  version: MCP_VERSION,
@@ -6280,7 +6322,7 @@ function registerCommentsTools(server) {
6280
6322
  {
6281
6323
  type: "text",
6282
6324
  text: JSON.stringify(
6283
- asEnvelope7({ comments, nextPageToken: result.nextPageToken ?? null }),
6325
+ asEnvelope8({ comments, nextPageToken: result.nextPageToken ?? null }),
6284
6326
  null,
6285
6327
  2
6286
6328
  )
@@ -6350,7 +6392,7 @@ function registerCommentsTools(server) {
6350
6392
  const result = data;
6351
6393
  if (format === "json") {
6352
6394
  return {
6353
- content: [{ type: "text", text: JSON.stringify(asEnvelope7(result), null, 2) }]
6395
+ content: [{ type: "text", text: JSON.stringify(asEnvelope8(result), null, 2) }]
6354
6396
  };
6355
6397
  }
6356
6398
  return {
@@ -6402,7 +6444,7 @@ function registerCommentsTools(server) {
6402
6444
  const result = data;
6403
6445
  if (format === "json") {
6404
6446
  return {
6405
- content: [{ type: "text", text: JSON.stringify(asEnvelope7(result), null, 2) }]
6447
+ content: [{ type: "text", text: JSON.stringify(asEnvelope8(result), null, 2) }]
6406
6448
  };
6407
6449
  }
6408
6450
  return {
@@ -6457,7 +6499,7 @@ function registerCommentsTools(server) {
6457
6499
  {
6458
6500
  type: "text",
6459
6501
  text: JSON.stringify(
6460
- asEnvelope7({
6502
+ asEnvelope8({
6461
6503
  success: true,
6462
6504
  commentId: comment_id,
6463
6505
  moderationStatus: moderation_status
@@ -6516,7 +6558,7 @@ function registerCommentsTools(server) {
6516
6558
  content: [
6517
6559
  {
6518
6560
  type: "text",
6519
- text: JSON.stringify(asEnvelope7({ success: true, commentId: comment_id }), null, 2)
6561
+ text: JSON.stringify(asEnvelope8({ success: true, commentId: comment_id }), null, 2)
6520
6562
  }
6521
6563
  ]
6522
6564
  };
@@ -6531,7 +6573,7 @@ function registerCommentsTools(server) {
6531
6573
  // src/tools/ideation-context.ts
6532
6574
  init_supabase();
6533
6575
  import { z as z12 } from "zod";
6534
- function asEnvelope8(data) {
6576
+ function asEnvelope9(data) {
6535
6577
  return {
6536
6578
  _meta: {
6537
6579
  version: MCP_VERSION,
@@ -6583,7 +6625,7 @@ function registerIdeationContextTools(server) {
6583
6625
  const context = result.context;
6584
6626
  if (format === "json") {
6585
6627
  return {
6586
- content: [{ type: "text", text: JSON.stringify(asEnvelope8(context), null, 2) }]
6628
+ content: [{ type: "text", text: JSON.stringify(asEnvelope9(context), null, 2) }]
6587
6629
  };
6588
6630
  }
6589
6631
  const lines = [
@@ -6603,7 +6645,7 @@ function registerIdeationContextTools(server) {
6603
6645
 
6604
6646
  // src/tools/credits.ts
6605
6647
  import { z as z13 } from "zod";
6606
- function asEnvelope9(data) {
6648
+ function asEnvelope10(data) {
6607
6649
  return {
6608
6650
  _meta: {
6609
6651
  version: MCP_VERSION,
@@ -6640,7 +6682,7 @@ function registerCreditsTools(server) {
6640
6682
  };
6641
6683
  if ((response_format || "text") === "json") {
6642
6684
  return {
6643
- content: [{ type: "text", text: JSON.stringify(asEnvelope9(payload), null, 2) }]
6685
+ content: [{ type: "text", text: JSON.stringify(asEnvelope10(payload), null, 2) }]
6644
6686
  };
6645
6687
  }
6646
6688
  return {
@@ -6674,7 +6716,7 @@ Monthly used: ${payload.monthlyUsed}` + (payload.monthlyLimit ? ` / ${payload.mo
6674
6716
  };
6675
6717
  if ((response_format || "text") === "json") {
6676
6718
  return {
6677
- content: [{ type: "text", text: JSON.stringify(asEnvelope9(payload), null, 2) }]
6719
+ content: [{ type: "text", text: JSON.stringify(asEnvelope10(payload), null, 2) }]
6678
6720
  };
6679
6721
  }
6680
6722
  return {
@@ -6698,7 +6740,7 @@ Assets remaining: ${payload.remainingAssets ?? "unlimited"}`
6698
6740
  // src/tools/loop-summary.ts
6699
6741
  init_supabase();
6700
6742
  import { z as z14 } from "zod";
6701
- function asEnvelope10(data) {
6743
+ function asEnvelope11(data) {
6702
6744
  return {
6703
6745
  _meta: {
6704
6746
  version: MCP_VERSION,
@@ -6749,7 +6791,7 @@ function registerLoopSummaryTools(server) {
6749
6791
  };
6750
6792
  if ((response_format || "text") === "json") {
6751
6793
  return {
6752
- content: [{ type: "text", text: JSON.stringify(asEnvelope10(payload), null, 2) }]
6794
+ content: [{ type: "text", text: JSON.stringify(asEnvelope11(payload), null, 2) }]
6753
6795
  };
6754
6796
  }
6755
6797
  return {
@@ -6771,7 +6813,7 @@ Next Action: ${payload.recommendedNextAction}`
6771
6813
 
6772
6814
  // src/tools/usage.ts
6773
6815
  import { z as z15 } from "zod";
6774
- function asEnvelope11(data) {
6816
+ function asEnvelope12(data) {
6775
6817
  return {
6776
6818
  _meta: {
6777
6819
  version: MCP_VERSION,
@@ -6804,7 +6846,7 @@ function registerUsageTools(server) {
6804
6846
  content: [
6805
6847
  {
6806
6848
  type: "text",
6807
- text: JSON.stringify(asEnvelope11({ tools: rows, totalCalls, totalCredits }), null, 2)
6849
+ text: JSON.stringify(asEnvelope12({ tools: rows, totalCalls, totalCredits }), null, 2)
6808
6850
  }
6809
6851
  ]
6810
6852
  };
@@ -6843,7 +6885,7 @@ ${"=".repeat(40)}
6843
6885
 
6844
6886
  // src/tools/autopilot.ts
6845
6887
  import { z as z16 } from "zod";
6846
- function asEnvelope12(data) {
6888
+ function asEnvelope13(data) {
6847
6889
  return {
6848
6890
  _meta: {
6849
6891
  version: MCP_VERSION,
@@ -6883,7 +6925,7 @@ function registerAutopilotTools(server) {
6883
6925
  content: [
6884
6926
  {
6885
6927
  type: "text",
6886
- text: JSON.stringify(asEnvelope12(configs), null, 2)
6928
+ text: JSON.stringify(asEnvelope13(configs), null, 2)
6887
6929
  }
6888
6930
  ]
6889
6931
  };
@@ -7024,7 +7066,7 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
7024
7066
  content: [
7025
7067
  {
7026
7068
  type: "text",
7027
- text: JSON.stringify(asEnvelope12(statusData), null, 2)
7069
+ text: JSON.stringify(asEnvelope13(statusData), null, 2)
7028
7070
  }
7029
7071
  ]
7030
7072
  };
@@ -7108,7 +7150,7 @@ ${"=".repeat(40)}
7108
7150
  }
7109
7151
  if (format === "json") {
7110
7152
  return {
7111
- content: [{ type: "text", text: JSON.stringify(asEnvelope12(created), null, 2) }]
7153
+ content: [{ type: "text", text: JSON.stringify(asEnvelope13(created), null, 2) }]
7112
7154
  };
7113
7155
  }
7114
7156
  return {
@@ -7129,7 +7171,7 @@ Active: ${is_active}`
7129
7171
 
7130
7172
  // src/tools/recipes.ts
7131
7173
  import { z as z17 } from "zod";
7132
- function asEnvelope13(data) {
7174
+ function asEnvelope14(data) {
7133
7175
  return {
7134
7176
  _meta: {
7135
7177
  version: MCP_VERSION,
@@ -7178,7 +7220,7 @@ function registerRecipeTools(server) {
7178
7220
  content: [
7179
7221
  {
7180
7222
  type: "text",
7181
- text: JSON.stringify(asEnvelope13(recipes))
7223
+ text: JSON.stringify(asEnvelope14(recipes))
7182
7224
  }
7183
7225
  ]
7184
7226
  };
@@ -7247,7 +7289,7 @@ ${lines.join("\n\n")}`
7247
7289
  content: [
7248
7290
  {
7249
7291
  type: "text",
7250
- text: JSON.stringify(asEnvelope13(recipe))
7292
+ text: JSON.stringify(asEnvelope14(recipe))
7251
7293
  }
7252
7294
  ]
7253
7295
  };
@@ -7284,7 +7326,7 @@ ${lines.join("\n\n")}`
7284
7326
  "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.",
7285
7327
  {
7286
7328
  slug: z17.string().describe('Recipe slug (e.g., "weekly-instagram-calendar")'),
7287
- inputs: z17.record(z17.unknown()).describe(
7329
+ inputs: z17.record(z17.string(), z17.unknown()).describe(
7288
7330
  "Input values matching the recipe input schema. Use get_recipe_details to see required inputs."
7289
7331
  ),
7290
7332
  response_format: z17.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
@@ -7307,7 +7349,7 @@ ${lines.join("\n\n")}`
7307
7349
  content: [
7308
7350
  {
7309
7351
  type: "text",
7310
- text: JSON.stringify(asEnvelope13(result))
7352
+ text: JSON.stringify(asEnvelope14(result))
7311
7353
  }
7312
7354
  ]
7313
7355
  };
@@ -7363,7 +7405,7 @@ ${result?.message || "Use get_recipe_run_status to check progress."}`
7363
7405
  content: [
7364
7406
  {
7365
7407
  type: "text",
7366
- text: JSON.stringify(asEnvelope13(run))
7408
+ text: JSON.stringify(asEnvelope14(run))
7367
7409
  }
7368
7410
  ]
7369
7411
  };
@@ -7395,14 +7437,111 @@ ${JSON.stringify(run.outputs, null, 2)}
7395
7437
 
7396
7438
  // src/tools/extraction.ts
7397
7439
  import { z as z18 } from "zod";
7398
- function asEnvelope14(data) {
7399
- return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
7400
- }
7401
- function isYouTubeUrl(url) {
7440
+
7441
+ // src/lib/urlExtraction.ts
7442
+ function classifyYouTubeUrl(url) {
7402
7443
  if (/youtube\.com\/watch|youtu\.be\//.test(url)) return "video";
7403
7444
  if (/youtube\.com\/@/.test(url)) return "channel";
7404
7445
  return false;
7405
7446
  }
7447
+ function unwrapEf(result, label) {
7448
+ if (result.error) return { error: result.error };
7449
+ const env = result.data;
7450
+ if (!env || env.success === false) {
7451
+ return { error: env?.error ?? `No data returned from ${label}` };
7452
+ }
7453
+ return { payload: env.data ?? void 0 };
7454
+ }
7455
+ function segmentsToTranscript(segments) {
7456
+ if (!Array.isArray(segments)) return "";
7457
+ return segments.map((s) => (s?.text ?? "").trim()).filter(Boolean).join(" ");
7458
+ }
7459
+ async function extractUrlContent(url, opts = {}) {
7460
+ const extractType = opts.extractType ?? "auto";
7461
+ const youtubeType = classifyYouTubeUrl(url);
7462
+ if (youtubeType === "video") {
7463
+ const [txRes, metaRes] = await Promise.all([
7464
+ callEdgeFunction(
7465
+ "scrape-youtube",
7466
+ { action: "transcript", videoUrl: url },
7467
+ { timeoutMs: 3e4 }
7468
+ ),
7469
+ callEdgeFunction(
7470
+ "scrape-youtube",
7471
+ { action: "metadata", videoUrl: url },
7472
+ { timeoutMs: 3e4 }
7473
+ )
7474
+ ]);
7475
+ const tx = unwrapEf(txRes, "scrape-youtube transcript");
7476
+ const meta = unwrapEf(metaRes, "scrape-youtube metadata");
7477
+ if (tx.error && meta.error) {
7478
+ return { error: `Failed to extract YouTube video: ${meta.error ?? tx.error}` };
7479
+ }
7480
+ const m = meta.payload;
7481
+ return {
7482
+ content: {
7483
+ source_type: "youtube_video",
7484
+ url,
7485
+ title: m?.title ?? "",
7486
+ description: m?.description ?? "",
7487
+ transcript: segmentsToTranscript(tx.payload?.segments),
7488
+ video_metadata: m ? {
7489
+ views: typeof m.viewCount === "number" ? m.viewCount : 0,
7490
+ likes: typeof m.likes === "number" ? m.likes : 0,
7491
+ duration: typeof m.duration === "number" ? m.duration : 0,
7492
+ tags: Array.isArray(m.tags) ? m.tags : [],
7493
+ channel_name: m.channelName ?? ""
7494
+ } : void 0
7495
+ }
7496
+ };
7497
+ }
7498
+ if (youtubeType === "channel") {
7499
+ const res2 = await callEdgeFunction(
7500
+ "scrape-youtube",
7501
+ { action: "channel_videos", videoUrl: url },
7502
+ { timeoutMs: 3e4 }
7503
+ );
7504
+ const ch = unwrapEf(res2, "scrape-youtube channel");
7505
+ if (ch.error) return { error: `Failed to extract YouTube channel: ${ch.error}` };
7506
+ const videos = ch.payload?.videos ?? [];
7507
+ return {
7508
+ content: {
7509
+ source_type: "youtube_channel",
7510
+ url,
7511
+ title: url,
7512
+ description: videos.length ? `${videos.length} recent videos:
7513
+ ${videos.map((v) => `- ${v.title ?? "Untitled"}`).join("\n")}` : "No videos found for this channel."
7514
+ }
7515
+ };
7516
+ }
7517
+ const res = await callEdgeFunction(
7518
+ "fetch-url-content",
7519
+ { url, extractType: extractType === "auto" ? "product" : extractType },
7520
+ { timeoutMs: 3e4 }
7521
+ );
7522
+ const result = unwrapEf(res, "fetch-url-content");
7523
+ if (result.error || !result.payload) {
7524
+ return { error: `Failed to extract URL content: ${result.error ?? "No data returned"}` };
7525
+ }
7526
+ const info = result.payload;
7527
+ return {
7528
+ content: {
7529
+ source_type: extractType === "product" ? "product" : "article",
7530
+ url,
7531
+ title: info.name ?? "",
7532
+ description: info.description ?? "",
7533
+ features: info.features,
7534
+ benefits: info.benefits,
7535
+ usp: info.usp,
7536
+ suggested_hooks: info.suggestedHookAngles
7537
+ }
7538
+ };
7539
+ }
7540
+
7541
+ // src/tools/extraction.ts
7542
+ function asEnvelope15(data) {
7543
+ return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
7544
+ }
7406
7545
  function formatExtractedContentAsText(content) {
7407
7546
  const lines = [];
7408
7547
  lines.push(`Source: ${content.source_type} (${content.url})`);
@@ -7447,15 +7586,15 @@ ${content.suggested_hooks.map((h) => ` - ${h}`).join("\n")}`
7447
7586
  function registerExtractionTools(server) {
7448
7587
  server.tool(
7449
7588
  "extract_url_content",
7450
- "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.",
7589
+ "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.",
7451
7590
  {
7452
7591
  url: z18.string().url().describe("URL to extract content from"),
7453
- extract_type: z18.enum(["auto", "transcript", "article", "product"]).default("auto").describe("Type of extraction"),
7454
- include_comments: z18.boolean().default(false).describe("Include top comments (YouTube only)"),
7455
- max_results: z18.number().min(1).max(100).default(10).describe("Max comments to include"),
7592
+ extract_type: z18.enum(["auto", "transcript", "article", "product"]).default("auto").describe(
7593
+ "auto = product-style extraction; transcript = YouTube (auto-detected by URL); article = blog/news main text + key points; product = e-commerce features/benefits/USP."
7594
+ ),
7456
7595
  response_format: z18.enum(["text", "json"]).default("text")
7457
7596
  },
7458
- async ({ url, extract_type, include_comments, max_results, response_format }) => {
7597
+ async ({ url, extract_type, response_format }) => {
7459
7598
  const ssrfCheck = await validateUrlForSSRF(url);
7460
7599
  if (!ssrfCheck.isValid) {
7461
7600
  return {
@@ -7463,106 +7602,20 @@ function registerExtractionTools(server) {
7463
7602
  isError: true
7464
7603
  };
7465
7604
  }
7466
- const youtubeType = isYouTubeUrl(url);
7467
7605
  try {
7468
- let extracted;
7469
- if (youtubeType === "video") {
7470
- const { data, error } = await callEdgeFunction(
7471
- "scrape-youtube",
7472
- {
7473
- url,
7474
- includeComments: include_comments,
7475
- maxComments: max_results
7476
- },
7477
- { timeoutMs: 3e4 }
7478
- );
7479
- if (error || !data) {
7480
- return {
7481
- content: [
7482
- {
7483
- type: "text",
7484
- text: `Failed to extract YouTube video: ${error ?? "No data returned"}`
7485
- }
7486
- ],
7487
- isError: true
7488
- };
7489
- }
7490
- extracted = {
7491
- source_type: "youtube_video",
7492
- url,
7493
- title: data.title ?? "",
7494
- description: data.description ?? "",
7495
- transcript: data.transcript,
7496
- video_metadata: data.metadata ? {
7497
- views: data.metadata.views ?? 0,
7498
- likes: data.metadata.likes ?? 0,
7499
- duration: data.metadata.duration ?? 0,
7500
- tags: data.metadata.tags ?? [],
7501
- channel_name: data.metadata.channelName ?? ""
7502
- } : void 0
7503
- };
7504
- } else if (youtubeType === "channel") {
7505
- const { data, error } = await callEdgeFunction(
7506
- "scrape-youtube",
7507
- {
7508
- url,
7509
- type: "channel"
7510
- },
7511
- { timeoutMs: 3e4 }
7512
- );
7513
- if (error || !data) {
7514
- return {
7515
- content: [
7516
- {
7517
- type: "text",
7518
- text: `Failed to extract YouTube channel: ${error ?? "No data returned"}`
7519
- }
7520
- ],
7521
- isError: true
7522
- };
7523
- }
7524
- extracted = {
7525
- source_type: "youtube_channel",
7526
- url,
7527
- title: data.title ?? "",
7528
- description: data.description ?? ""
7529
- };
7530
- } else {
7531
- const body = { url };
7532
- if (extract_type !== "auto") body.type = extract_type;
7533
- const { data, error } = await callEdgeFunction(
7534
- "fetch-url-content",
7535
- body,
7536
- { timeoutMs: 3e4 }
7537
- );
7538
- if (error || !data) {
7539
- return {
7540
- content: [
7541
- {
7542
- type: "text",
7543
- text: `Failed to extract URL content: ${error ?? "No data returned"}`
7544
- }
7545
- ],
7546
- isError: true
7547
- };
7548
- }
7549
- const sourceType = extract_type === "product" ? "product" : data.type === "product" ? "product" : "article";
7550
- extracted = {
7551
- source_type: sourceType,
7552
- url,
7553
- title: data.title ?? "",
7554
- description: data.description ?? "",
7555
- transcript: data.content,
7556
- features: data.features,
7557
- benefits: data.benefits,
7558
- usp: data.usp,
7559
- suggested_hooks: data.suggestedHooks
7606
+ const { content: extracted, error } = await extractUrlContent(url, {
7607
+ extractType: extract_type
7608
+ });
7609
+ if (error || !extracted) {
7610
+ return {
7611
+ content: [{ type: "text", text: error ?? "No data returned" }],
7612
+ isError: true
7560
7613
  };
7561
7614
  }
7562
7615
  if (response_format === "json") {
7563
7616
  return {
7564
7617
  content: [
7565
- { type: "text", text: JSON.stringify(asEnvelope14(extracted), null, 2) }
7618
+ { type: "text", text: JSON.stringify(asEnvelope15(extracted), null, 2) }
7566
7619
  ],
7567
7620
  isError: false
7568
7621
  };
@@ -7584,7 +7637,7 @@ function registerExtractionTools(server) {
7584
7637
 
7585
7638
  // src/tools/quality.ts
7586
7639
  import { z as z19 } from "zod";
7587
- function asEnvelope15(data) {
7640
+ function asEnvelope16(data) {
7588
7641
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
7589
7642
  }
7590
7643
  function registerQualityTools(server) {
@@ -7637,7 +7690,7 @@ function registerQualityTools(server) {
7637
7690
  });
7638
7691
  if (response_format === "json") {
7639
7692
  return {
7640
- content: [{ type: "text", text: JSON.stringify(asEnvelope15(result), null, 2) }],
7693
+ content: [{ type: "text", text: JSON.stringify(asEnvelope16(result), null, 2) }],
7641
7694
  isError: false
7642
7695
  };
7643
7696
  }
@@ -7712,7 +7765,7 @@ function registerQualityTools(server) {
7712
7765
  content: [
7713
7766
  {
7714
7767
  type: "text",
7715
- text: JSON.stringify(asEnvelope15({ posts: postsWithQuality, summary }), null, 2)
7768
+ text: JSON.stringify(asEnvelope16({ posts: postsWithQuality, summary }), null, 2)
7716
7769
  }
7717
7770
  ],
7718
7771
  isError: false
@@ -8046,7 +8099,7 @@ function buildCorrectiveHint(overflowIssues, spellingIssues) {
8046
8099
  }
8047
8100
 
8048
8101
  // src/tools/visualQuality.ts
8049
- function asEnvelope16(data) {
8102
+ function asEnvelope17(data) {
8050
8103
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
8051
8104
  }
8052
8105
  var VALID_STYLES = ["dark-cinematic", "clean-editorial", "bold-authority"];
@@ -8171,7 +8224,7 @@ function registerVisualQualityTools(server) {
8171
8224
  {
8172
8225
  type: "text",
8173
8226
  text: JSON.stringify(
8174
- asEnvelope16({ ...result, correctiveHint: hint || null }),
8227
+ asEnvelope17({ ...result, correctiveHint: hint || null }),
8175
8228
  null,
8176
8229
  2
8177
8230
  )
@@ -8241,7 +8294,7 @@ function registerVisualQualityTools(server) {
8241
8294
  return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
8242
8295
  }
8243
8296
  return {
8244
- content: [{ type: "text", text: JSON.stringify(asEnvelope16(data), null, 2) }],
8297
+ content: [{ type: "text", text: JSON.stringify(asEnvelope17(data), null, 2) }],
8245
8298
  isError: false
8246
8299
  };
8247
8300
  }
@@ -8277,7 +8330,7 @@ function extractJsonArray(text) {
8277
8330
  function toRecord(value) {
8278
8331
  return value && typeof value === "object" ? value : void 0;
8279
8332
  }
8280
- function asEnvelope17(data) {
8333
+ function asEnvelope18(data) {
8281
8334
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
8282
8335
  }
8283
8336
  function tomorrowIsoDate() {
@@ -8290,9 +8343,6 @@ function addDaysToIsoDate(dateStr, days) {
8290
8343
  d.setDate(d.getDate() + days);
8291
8344
  return d.toISOString().split("T")[0];
8292
8345
  }
8293
- function isYouTubeUrl2(url) {
8294
- return /youtube\.com\/watch|youtu\.be\/|youtube\.com\/@/.test(url);
8295
- }
8296
8346
  function formatPlanAsText(plan) {
8297
8347
  const lines = [];
8298
8348
  lines.push(`WEEKLY CONTENT PLAN: "${plan.topic}"`);
@@ -8390,20 +8440,22 @@ function registerPlanningTools(server) {
8390
8440
  let sourceContext = "";
8391
8441
  if (source_url) {
8392
8442
  try {
8393
- const fnName = isYouTubeUrl2(source_url) ? "scrape-youtube" : "fetch-url-content";
8394
- const { data } = await callEdgeFunction(
8395
- fnName,
8396
- { url: source_url },
8397
- { timeoutMs: 3e4 }
8398
- );
8399
- if (data) {
8400
- const parts = [
8401
- data.title ? String(data.title) : "",
8402
- data.description ? String(data.description) : "",
8403
- data.transcript ? String(data.transcript).slice(0, 2e3) : "",
8404
- data.content ? String(data.content).slice(0, 2e3) : ""
8405
- ].filter(Boolean);
8406
- sourceContext = parts.join("\n\n");
8443
+ const ssrf = await validateUrlForSSRF(source_url);
8444
+ if (ssrf.isValid) {
8445
+ const { content } = await extractUrlContent(source_url);
8446
+ if (content) {
8447
+ const parts = [
8448
+ content.title,
8449
+ content.description,
8450
+ content.transcript ? content.transcript.slice(0, 2e3) : "",
8451
+ content.features?.length ? `Features:
8452
+ ${content.features.join("\n")}` : "",
8453
+ content.benefits?.length ? `Benefits:
8454
+ ${content.benefits.join("\n")}` : "",
8455
+ content.usp ?? ""
8456
+ ].filter(Boolean);
8457
+ sourceContext = parts.join("\n\n");
8458
+ }
8407
8459
  }
8408
8460
  } catch {
8409
8461
  }
@@ -8609,13 +8661,16 @@ ${rawText.slice(0, 1e3)}`
8609
8661
  };
8610
8662
  }
8611
8663
  }
8664
+ const structuredContent = asEnvelope18(plan);
8612
8665
  if (response_format === "json") {
8613
8666
  return {
8614
- content: [{ type: "text", text: JSON.stringify(asEnvelope17(plan), null, 2) }],
8667
+ structuredContent,
8668
+ content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }],
8615
8669
  isError: false
8616
8670
  };
8617
8671
  }
8618
8672
  return {
8673
+ structuredContent,
8619
8674
  content: [{ type: "text", text: formatPlanAsText(plan) }],
8620
8675
  isError: false
8621
8676
  };
@@ -8686,7 +8741,7 @@ ${rawText.slice(0, 1e3)}`
8686
8741
  };
8687
8742
  if (response_format === "json") {
8688
8743
  return {
8689
- content: [{ type: "text", text: JSON.stringify(asEnvelope17(result), null, 2) }],
8744
+ content: [{ type: "text", text: JSON.stringify(asEnvelope18(result), null, 2) }],
8690
8745
  isError: false
8691
8746
  };
8692
8747
  }
@@ -8745,7 +8800,7 @@ ${rawText.slice(0, 1e3)}`
8745
8800
  };
8746
8801
  if (response_format === "json") {
8747
8802
  return {
8748
- content: [{ type: "text", text: JSON.stringify(asEnvelope17(payload), null, 2) }],
8803
+ content: [{ type: "text", text: JSON.stringify(asEnvelope18(payload), null, 2) }],
8749
8804
  isError: false
8750
8805
  };
8751
8806
  }
@@ -8817,7 +8872,7 @@ ${rawText.slice(0, 1e3)}`
8817
8872
  };
8818
8873
  if (response_format === "json") {
8819
8874
  return {
8820
- content: [{ type: "text", text: JSON.stringify(asEnvelope17(payload), null, 2) }],
8875
+ content: [{ type: "text", text: JSON.stringify(asEnvelope18(payload), null, 2) }],
8821
8876
  isError: false
8822
8877
  };
8823
8878
  }
@@ -8870,7 +8925,7 @@ ${rawText.slice(0, 1e3)}`
8870
8925
  };
8871
8926
  if (response_format === "json") {
8872
8927
  return {
8873
- content: [{ type: "text", text: JSON.stringify(asEnvelope17(payload), null, 2) }],
8928
+ content: [{ type: "text", text: JSON.stringify(asEnvelope18(payload), null, 2) }],
8874
8929
  isError: false
8875
8930
  };
8876
8931
  }
@@ -8890,7 +8945,7 @@ ${rawText.slice(0, 1e3)}`
8890
8945
  // src/tools/plan-approvals.ts
8891
8946
  import { z as z22 } from "zod";
8892
8947
  init_supabase();
8893
- function asEnvelope18(data) {
8948
+ function asEnvelope19(data) {
8894
8949
  return {
8895
8950
  _meta: {
8896
8951
  version: MCP_VERSION,
@@ -8965,7 +9020,7 @@ function registerPlanApprovalTools(server) {
8965
9020
  };
8966
9021
  if ((response_format || "text") === "json") {
8967
9022
  return {
8968
- content: [{ type: "text", text: JSON.stringify(asEnvelope18(payload), null, 2) }],
9023
+ content: [{ type: "text", text: JSON.stringify(asEnvelope19(payload), null, 2) }],
8969
9024
  isError: false
8970
9025
  };
8971
9026
  }
@@ -9017,7 +9072,7 @@ function registerPlanApprovalTools(server) {
9017
9072
  };
9018
9073
  if ((response_format || "text") === "json") {
9019
9074
  return {
9020
- content: [{ type: "text", text: JSON.stringify(asEnvelope18(payload), null, 2) }],
9075
+ content: [{ type: "text", text: JSON.stringify(asEnvelope19(payload), null, 2) }],
9021
9076
  isError: false
9022
9077
  };
9023
9078
  }
@@ -9095,7 +9150,7 @@ function registerPlanApprovalTools(server) {
9095
9150
  }
9096
9151
  if ((response_format || "text") === "json") {
9097
9152
  return {
9098
- content: [{ type: "text", text: JSON.stringify(asEnvelope18(data), null, 2) }],
9153
+ content: [{ type: "text", text: JSON.stringify(asEnvelope19(data), null, 2) }],
9099
9154
  isError: false
9100
9155
  };
9101
9156
  }
@@ -9304,13 +9359,15 @@ var TOOL_CATALOG = [
9304
9359
  name: "capture_screenshot",
9305
9360
  description: "Capture a screenshot of a URL",
9306
9361
  module: "screenshot",
9307
- scope: "mcp:read"
9362
+ scope: "mcp:read",
9363
+ localOnly: true
9308
9364
  },
9309
9365
  {
9310
9366
  name: "capture_app_page",
9311
9367
  description: "Capture a screenshot of an app page",
9312
9368
  module: "screenshot",
9313
- scope: "mcp:read"
9369
+ scope: "mcp:read",
9370
+ localOnly: true
9314
9371
  },
9315
9372
  // remotion
9316
9373
  {
@@ -9498,7 +9555,7 @@ var TOOL_CATALOG = [
9498
9555
  // niche research
9499
9556
  {
9500
9557
  name: "find_winning_content",
9501
- 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).",
9558
+ 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.",
9502
9559
  module: "research",
9503
9560
  scope: "mcp:read"
9504
9561
  },
@@ -9523,6 +9580,18 @@ var TOOL_CATALOG = [
9523
9580
  module: "discovery",
9524
9581
  scope: "mcp:read"
9525
9582
  },
9583
+ {
9584
+ name: "search",
9585
+ description: "Search public Social Neuron product, integration, developer, and MCP tool knowledge using the ChatGPT-compatible search schema.",
9586
+ module: "discovery",
9587
+ scope: "mcp:read"
9588
+ },
9589
+ {
9590
+ name: "fetch",
9591
+ description: "Fetch one public Social Neuron knowledge document by ID using the ChatGPT-compatible fetch schema.",
9592
+ module: "discovery",
9593
+ scope: "mcp:read"
9594
+ },
9526
9595
  // pipeline
9527
9596
  {
9528
9597
  name: "check_pipeline_readiness",
@@ -9637,58 +9706,93 @@ var TOOL_CATALOG = [
9637
9706
  // agentic-harness — learning loop write-back
9638
9707
  {
9639
9708
  name: "write_agent_reflection",
9640
- 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.",
9709
+ 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.",
9641
9710
  module: "harness",
9642
- scope: "mcp:write"
9711
+ scope: "mcp:write",
9712
+ internal: true
9643
9713
  },
9644
9714
  {
9645
9715
  name: "record_outcome",
9646
- description: "Record an outcome for a published decision event. Idempotent on (decision_event_id, horizon). Only horizon=24h triggers a content_bandits posterior update.",
9716
+ description: "Record an outcome for a published decision event. Idempotent on (decision_event_id, horizon). Only horizon=24h triggers a learning-loop update.",
9647
9717
  module: "harness",
9648
- scope: "mcp:write"
9718
+ scope: "mcp:write",
9719
+ internal: true
9649
9720
  },
9650
9721
  // agentic-harness — learning loop read-back
9651
9722
  {
9652
9723
  name: "read_agent_reflection",
9653
9724
  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.",
9654
9725
  module: "harness",
9655
- scope: "mcp:read"
9726
+ scope: "mcp:read",
9727
+ internal: true
9656
9728
  },
9657
9729
  // hermes — autonomous agent integration (closed-loop content)
9658
9730
  {
9659
9731
  name: "save_draft_to_library",
9660
- description: "Save a draft post to the SN content library with origin=hermes. Used by Hermes to persist drafts before the founder approves them.",
9732
+ description: "Save a draft post to the SN content library for review before publishing. Drafts land in the content library pending approval.",
9661
9733
  module: "hermes",
9662
- scope: "mcp:write"
9734
+ scope: "mcp:write",
9735
+ internal: true
9663
9736
  },
9664
9737
  {
9665
9738
  name: "record_voice_lesson",
9666
- description: "Persist a learned voice lesson to brand_profiles.brand_context.voiceProfile.voice_lessons via atomic RPC. Used by Hermes reflection cron.",
9739
+ description: "Persist a learned voice lesson to the brand voice profile.",
9667
9740
  module: "hermes",
9668
- scope: "mcp:write"
9741
+ scope: "mcp:write",
9742
+ internal: true
9669
9743
  },
9670
9744
  {
9671
9745
  name: "record_observation",
9672
- description: 'Record an agent observation (e.g. "topic X engagement up 23%") for the UnifiedAnalytics > Playbook surface.',
9746
+ description: 'Record an agent observation (e.g. "topic X engagement up 23%") for the analytics playbook.',
9673
9747
  module: "hermes",
9674
- scope: "mcp:write"
9748
+ scope: "mcp:write",
9749
+ internal: true
9675
9750
  },
9676
9751
  {
9677
9752
  name: "record_intel_signal",
9678
- description: "Record a research/trend signal from Hermes watchers (news, HN, competitor, etc.) for Niche Intelligence. Dedupes by URL.",
9753
+ description: "Record a research/trend signal (news, competitor, community sources) for niche intelligence. Dedupes by URL.",
9679
9754
  module: "hermes",
9680
- scope: "mcp:write"
9755
+ scope: "mcp:write",
9756
+ internal: true
9681
9757
  },
9682
9758
  {
9683
9759
  name: "record_campaign_spend",
9684
- description: "Log a campaign cost line (hermes_drafts, carousel_renders, analytics_pulls, paid_amplification, other). Ownership-checked.",
9760
+ description: "Log a campaign cost line item. Ownership-checked.",
9685
9761
  module: "hermes",
9686
- scope: "mcp:write"
9762
+ scope: "mcp:write",
9763
+ internal: true
9687
9764
  },
9688
9765
  {
9689
9766
  name: "get_active_campaigns",
9690
- description: "List currently-running campaigns with thesis, budget, hero format, and current spend. Used by Hermes pitch skill to bias drafts.",
9767
+ description: "List currently-running campaigns with thesis, budget, hero format, and current spend.",
9691
9768
  module: "hermes",
9769
+ scope: "mcp:read",
9770
+ internal: true
9771
+ },
9772
+ // skills (workflow skills — multi-step brand-locked content pipelines)
9773
+ {
9774
+ name: "list_skills",
9775
+ 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.).",
9776
+ module: "skills",
9777
+ scope: "mcp:read"
9778
+ },
9779
+ {
9780
+ name: "run_skill",
9781
+ 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.",
9782
+ module: "skills",
9783
+ scope: "mcp:write"
9784
+ },
9785
+ // loop observability (growth-loop KPIs + content learning state)
9786
+ {
9787
+ name: "get_loop_pulse",
9788
+ 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.",
9789
+ module: "loop",
9790
+ scope: "mcp:read"
9791
+ },
9792
+ {
9793
+ name: "get_bandit_state",
9794
+ 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.",
9795
+ module: "loop",
9692
9796
  scope: "mcp:read"
9693
9797
  }
9694
9798
  ];
@@ -9706,7 +9810,194 @@ function searchTools(query) {
9706
9810
  }
9707
9811
 
9708
9812
  // src/tools/discovery.ts
9813
+ init_supabase();
9814
+ init_request_context();
9815
+ var KNOWLEDGE_BASE_URL = "https://socialneuron.com/for-developers";
9816
+ var KNOWLEDGE_SEARCH_LIMIT = 10;
9817
+ var SearchOutputSchema = {
9818
+ results: z23.array(
9819
+ z23.object({
9820
+ id: z23.string(),
9821
+ title: z23.string(),
9822
+ url: z23.string().url()
9823
+ })
9824
+ )
9825
+ };
9826
+ var FetchOutputSchema = {
9827
+ id: z23.string(),
9828
+ title: z23.string(),
9829
+ text: z23.string(),
9830
+ url: z23.string().url(),
9831
+ metadata: z23.record(z23.string(), z23.string()).optional()
9832
+ };
9833
+ var STATIC_KNOWLEDGE_DOCUMENTS = [
9834
+ {
9835
+ id: "overview",
9836
+ title: "Social Neuron MCP Overview",
9837
+ url: `${KNOWLEDGE_BASE_URL}#mcp`,
9838
+ text: [
9839
+ "Social Neuron exposes an MCP server for creating, scheduling, and optimizing social content.",
9840
+ "Use the hosted streamable HTTP endpoint at https://mcp.socialneuron.com/mcp for ChatGPT, Claude, and other remote MCP clients.",
9841
+ "The npm package provides stdio transport for local tools and Codex-style workflows."
9842
+ ].join("\n"),
9843
+ metadata: { source: "public-developer-docs", category: "overview" }
9844
+ },
9845
+ {
9846
+ id: "integrations",
9847
+ title: "Supported Social Integrations",
9848
+ url: "https://socialneuron.com/integrations",
9849
+ text: [
9850
+ "Social Neuron tracks platform availability on the integrations page.",
9851
+ "YouTube, TikTok, Instagram, LinkedIn, X, and Facebook are live for supported posting workflows.",
9852
+ "Threads and Bluesky are supported surfaces where live availability depends on the current integration status."
9853
+ ].join("\n"),
9854
+ metadata: { source: "public-integrations-page", category: "integrations" }
9855
+ },
9856
+ {
9857
+ id: "chatgpt-connector",
9858
+ title: "ChatGPT Connector Setup",
9859
+ url: `${KNOWLEDGE_BASE_URL}#chatgpt`,
9860
+ text: [
9861
+ "In ChatGPT Developer Mode, create an MCP app using https://mcp.socialneuron.com/mcp as the MCP URL.",
9862
+ "The connector uses OAuth for account linking and tool scopes for read, write, distribution, analytics, comments, and autopilot access.",
9863
+ "Publishing tools should be treated as externally visible actions and require the distribute scope."
9864
+ ].join("\n"),
9865
+ metadata: { source: "public-developer-docs", category: "chatgpt" }
9866
+ },
9867
+ {
9868
+ id: "privacy-security",
9869
+ title: "Connector Security and Data Minimization",
9870
+ url: `${KNOWLEDGE_BASE_URL}#security`,
9871
+ text: [
9872
+ "Social Neuron MCP tools enforce OAuth or API-key scopes before tool execution.",
9873
+ "Read-only discovery tools expose public product and tool metadata, not private account content.",
9874
+ "User-owned content and analytics require authenticated scopes and organization or project membership checks in backend functions."
9875
+ ].join("\n"),
9876
+ metadata: { source: "public-developer-docs", category: "security" }
9877
+ }
9878
+ ];
9879
+ function toolKnowledgeDocument(tool) {
9880
+ const lines = [
9881
+ `Tool: ${tool.name}`,
9882
+ `Description: ${tool.description}`,
9883
+ `Module: ${tool.module}`,
9884
+ `Required scope: ${tool.scope}`
9885
+ ];
9886
+ if (tool.task_intent) lines.push(`Task intent: ${tool.task_intent}`);
9887
+ if (tool.use_when) lines.push(`Use when: ${tool.use_when}`);
9888
+ if (tool.avoid_when) lines.push(`Avoid when: ${tool.avoid_when}`);
9889
+ if (tool.next_tools?.length) lines.push(`Common next tools: ${tool.next_tools.join(", ")}`);
9890
+ return {
9891
+ id: `tool:${tool.name}`,
9892
+ title: `MCP tool: ${tool.name}`,
9893
+ url: `${KNOWLEDGE_BASE_URL}#tool-${tool.name}`,
9894
+ text: lines.join("\n"),
9895
+ metadata: {
9896
+ source: "mcp-tool-catalog",
9897
+ category: "tool",
9898
+ module: tool.module,
9899
+ scope: tool.scope
9900
+ }
9901
+ };
9902
+ }
9903
+ function getKnowledgeDocuments() {
9904
+ return [
9905
+ ...STATIC_KNOWLEDGE_DOCUMENTS,
9906
+ ...TOOL_CATALOG.filter((t) => !t.internal).map(toolKnowledgeDocument)
9907
+ ];
9908
+ }
9909
+ function tokenize(input) {
9910
+ return input.toLowerCase().split(/[^a-z0-9:_-]+/).map((token) => token.trim()).filter(Boolean);
9911
+ }
9912
+ function scoreDocument(queryTokens, doc) {
9913
+ const title = doc.title.toLowerCase();
9914
+ const text = doc.text.toLowerCase();
9915
+ const metadata = JSON.stringify(doc.metadata ?? {}).toLowerCase();
9916
+ return queryTokens.reduce((score, token) => {
9917
+ if (doc.id.toLowerCase() === token) return score + 12;
9918
+ if (doc.id.toLowerCase().includes(token)) score += 8;
9919
+ if (title.includes(token)) score += 5;
9920
+ if (text.includes(token)) score += 2;
9921
+ if (metadata.includes(token)) score += 1;
9922
+ return score;
9923
+ }, 0);
9924
+ }
9925
+ function searchKnowledge(query) {
9926
+ const docs = getKnowledgeDocuments();
9927
+ const tokens = tokenize(query);
9928
+ if (tokens.length === 0) {
9929
+ return docs.slice(0, KNOWLEDGE_SEARCH_LIMIT);
9930
+ }
9931
+ 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);
9932
+ }
9709
9933
  function registerDiscoveryTools(server) {
9934
+ server.registerTool(
9935
+ "search",
9936
+ {
9937
+ title: "Search Social Neuron Knowledge",
9938
+ 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.",
9939
+ inputSchema: {
9940
+ query: z23.string().describe("Search query.")
9941
+ },
9942
+ outputSchema: SearchOutputSchema,
9943
+ annotations: {
9944
+ readOnlyHint: true,
9945
+ destructiveHint: false,
9946
+ idempotentHint: true,
9947
+ openWorldHint: false
9948
+ }
9949
+ },
9950
+ async ({ query }) => {
9951
+ const structuredContent = {
9952
+ results: searchKnowledge(query).map((doc) => ({
9953
+ id: doc.id,
9954
+ title: doc.title,
9955
+ url: doc.url
9956
+ }))
9957
+ };
9958
+ return {
9959
+ structuredContent,
9960
+ content: [{ type: "text", text: JSON.stringify(structuredContent) }]
9961
+ };
9962
+ }
9963
+ );
9964
+ server.registerTool(
9965
+ "fetch",
9966
+ {
9967
+ title: "Fetch Social Neuron Knowledge",
9968
+ description: "Fetch a public Social Neuron knowledge document by ID. Use IDs returned by the search tool.",
9969
+ inputSchema: {
9970
+ id: z23.string().describe("Document ID returned by search.")
9971
+ },
9972
+ outputSchema: FetchOutputSchema,
9973
+ annotations: {
9974
+ readOnlyHint: true,
9975
+ destructiveHint: false,
9976
+ idempotentHint: true,
9977
+ openWorldHint: false
9978
+ }
9979
+ },
9980
+ async ({ id }) => {
9981
+ const doc = getKnowledgeDocuments().find((candidate) => candidate.id === id);
9982
+ if (!doc) {
9983
+ return {
9984
+ content: [{ type: "text", text: `Document not found: ${id}` }],
9985
+ isError: true
9986
+ };
9987
+ }
9988
+ const structuredContent = {
9989
+ id: doc.id,
9990
+ title: doc.title,
9991
+ text: doc.text,
9992
+ url: doc.url,
9993
+ ...doc.metadata ? { metadata: doc.metadata } : {}
9994
+ };
9995
+ return {
9996
+ structuredContent,
9997
+ content: [{ type: "text", text: JSON.stringify(structuredContent) }]
9998
+ };
9999
+ }
10000
+ );
9710
10001
  server.tool(
9711
10002
  "search_tools",
9712
10003
  '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.',
@@ -9716,13 +10007,20 @@ function registerDiscoveryTools(server) {
9716
10007
  scope: z23.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
9717
10008
  detail: z23.enum(["name", "summary", "full"]).default("summary").describe(
9718
10009
  'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
10010
+ ),
10011
+ available_only: z23.boolean().default(false).describe(
10012
+ "When true, only return tools allowed by the current API key/OAuth scopes. Use this after a permission_denied error."
9719
10013
  )
9720
10014
  },
9721
- async ({ query, module, scope, detail }) => {
10015
+ async ({ query, module, scope, detail, available_only }) => {
10016
+ const currentScopes = getRequestScopes() ?? getAuthenticatedScopes();
10017
+ const hasKnownScopes = currentScopes.length > 0;
10018
+ const isAvailable = (tool) => !hasKnownScopes || hasScope(currentScopes, tool.scope);
9722
10019
  let results = [...TOOL_CATALOG];
9723
10020
  if (query) {
9724
10021
  results = searchTools(query);
9725
10022
  }
10023
+ results = results.filter((t) => !t.internal);
9726
10024
  if (module) {
9727
10025
  const moduleTools = getToolsByModule(module);
9728
10026
  results = results.filter((t) => moduleTools.some((mt) => mt.name === t.name));
@@ -9731,24 +10029,48 @@ function registerDiscoveryTools(server) {
9731
10029
  const scopeTools = getToolsByScope(scope);
9732
10030
  results = results.filter((t) => scopeTools.some((st) => st.name === t.name));
9733
10031
  }
10032
+ if (available_only) {
10033
+ results = results.filter(isAvailable);
10034
+ }
10035
+ const unavailableCount = hasKnownScopes ? results.filter((t) => !isAvailable(t)).length : 0;
9734
10036
  let output;
9735
10037
  switch (detail) {
9736
10038
  case "name":
9737
10039
  output = results.map((t) => t.name);
9738
10040
  break;
9739
10041
  case "summary":
9740
- output = results.map((t) => ({ name: t.name, description: t.description }));
10042
+ output = results.map((t) => ({
10043
+ name: t.name,
10044
+ description: t.description,
10045
+ ...hasKnownScopes || scope ? { required_scope: t.scope } : {},
10046
+ ...hasKnownScopes ? { available: isAvailable(t) } : {},
10047
+ ...t.task_intent ? { task_intent: t.task_intent } : {},
10048
+ ...t.use_when ? { use_when: t.use_when } : {},
10049
+ ...t.avoid_when ? { avoid_when: t.avoid_when } : {}
10050
+ }));
9741
10051
  break;
9742
10052
  case "full":
9743
10053
  default:
9744
- output = results;
10054
+ output = results.map((tool) => ({
10055
+ ...tool,
10056
+ required_scope: tool.scope,
10057
+ ...hasKnownScopes ? { available: isAvailable(tool) } : {}
10058
+ }));
9745
10059
  break;
9746
10060
  }
9747
10061
  return {
9748
10062
  content: [
9749
10063
  {
9750
10064
  type: "text",
9751
- text: JSON.stringify({ toolCount: results.length, tools: output }, null, 2)
10065
+ text: JSON.stringify(
10066
+ {
10067
+ toolCount: results.length,
10068
+ ...hasKnownScopes ? { scopes: { available: currentScopes, unavailable_matches: unavailableCount } } : {},
10069
+ tools: output
10070
+ },
10071
+ null,
10072
+ detail === "full" ? 2 : 0
10073
+ )
9752
10074
  }
9753
10075
  ]
9754
10076
  };
@@ -9760,7 +10082,7 @@ function registerDiscoveryTools(server) {
9760
10082
  import { z as z24 } from "zod";
9761
10083
  import { randomUUID as randomUUID3 } from "node:crypto";
9762
10084
  init_supabase();
9763
- function asEnvelope19(data) {
10085
+ function asEnvelope20(data) {
9764
10086
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
9765
10087
  }
9766
10088
  var PLATFORM_ENUM2 = z24.enum([
@@ -9775,6 +10097,7 @@ var PLATFORM_ENUM2 = z24.enum([
9775
10097
  ]);
9776
10098
  var BASE_PLAN_CREDITS = 15;
9777
10099
  var SOURCE_EXTRACTION_CREDITS = 5;
10100
+ var SCHEDULE_POST_CREDITS = 1;
9778
10101
  function registerPipelineTools(server) {
9779
10102
  server.tool(
9780
10103
  "check_pipeline_readiness",
@@ -9804,6 +10127,7 @@ function registerPipelineTools(server) {
9804
10127
  throw new Error(readinessError ?? "No response from mcp-data");
9805
10128
  }
9806
10129
  const credits = readiness.credits;
10130
+ const isUnlimited = readiness.is_unlimited === true;
9807
10131
  const connectedPlatforms = readiness.connected_platforms;
9808
10132
  const missingPlatforms = readiness.missing_platforms;
9809
10133
  const hasBrand = readiness.has_brand;
@@ -9812,7 +10136,7 @@ function registerPipelineTools(server) {
9812
10136
  const insightsFresh = readiness.insights_fresh;
9813
10137
  const blockers = [];
9814
10138
  const warnings = [];
9815
- if (credits < estimatedCost) {
10139
+ if (!isUnlimited && credits < estimatedCost) {
9816
10140
  blockers.push(`Insufficient credits: ${credits} available, ~${estimatedCost} needed`);
9817
10141
  }
9818
10142
  if (missingPlatforms.length > 0) {
@@ -9851,7 +10175,7 @@ function registerPipelineTools(server) {
9851
10175
  };
9852
10176
  if (format === "json") {
9853
10177
  return {
9854
- content: [{ type: "text", text: JSON.stringify(asEnvelope19(result), null, 2) }]
10178
+ content: [{ type: "text", text: JSON.stringify(asEnvelope20(result), null, 2) }]
9855
10179
  };
9856
10180
  }
9857
10181
  const lines = [];
@@ -9890,7 +10214,7 @@ function registerPipelineTools(server) {
9890
10214
  );
9891
10215
  server.tool(
9892
10216
  "run_content_pipeline",
9893
- "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.",
10217
+ "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.",
9894
10218
  {
9895
10219
  project_id: z24.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
9896
10220
  topic: z24.string().optional().describe("Content topic (required if no source_url)"),
@@ -9906,6 +10230,9 @@ function registerPipelineTools(server) {
9906
10230
  ),
9907
10231
  max_credits: z24.number().optional().describe("Credit budget cap"),
9908
10232
  dry_run: z24.boolean().default(false).describe("If true, skip scheduling and return plan only"),
10233
+ schedule_confirmed: z24.boolean().default(false).describe(
10234
+ "Required to schedule posts. Set true only after explicit user confirmation to publish/schedule."
10235
+ ),
9909
10236
  skip_stages: z24.array(z24.enum(["research", "quality", "schedule"])).optional().describe("Stages to skip"),
9910
10237
  response_format: z24.enum(["text", "json"]).default("json")
9911
10238
  },
@@ -9920,6 +10247,7 @@ function registerPipelineTools(server) {
9920
10247
  auto_approve_threshold,
9921
10248
  max_credits,
9922
10249
  dry_run,
10250
+ schedule_confirmed,
9923
10251
  skip_stages,
9924
10252
  response_format
9925
10253
  }) => {
@@ -9935,9 +10263,32 @@ function registerPipelineTools(server) {
9935
10263
  };
9936
10264
  }
9937
10265
  const skipSet = new Set(skip_stages ?? []);
9938
- try {
9939
- const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
9940
- const estimatedCost = BASE_PLAN_CREDITS + (source_url ? SOURCE_EXTRACTION_CREDITS : 0);
10266
+ const schedulingRequested = !dry_run && !skipSet.has("schedule");
10267
+ if (schedulingRequested && !schedule_confirmed) {
10268
+ return {
10269
+ content: [
10270
+ {
10271
+ type: "text",
10272
+ text: 'Scheduling requires explicit confirmation. Re-run with schedule_confirmed=true after the user approves publishing, or set dry_run=true / skip_stages=["schedule"].'
10273
+ }
10274
+ ],
10275
+ isError: true
10276
+ };
10277
+ }
10278
+ if (schedulingRequested && skipSet.has("quality")) {
10279
+ return {
10280
+ content: [
10281
+ {
10282
+ type: "text",
10283
+ text: "Scheduling cannot run when the quality stage is skipped."
10284
+ }
10285
+ ],
10286
+ isError: true
10287
+ };
10288
+ }
10289
+ try {
10290
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
10291
+ const estimatedCost = BASE_PLAN_CREDITS + (source_url ? SOURCE_EXTRACTION_CREDITS : 0);
9941
10292
  const { data: budgetData } = await callEdgeFunction(
9942
10293
  "mcp-data",
9943
10294
  {
@@ -9948,8 +10299,9 @@ function registerPipelineTools(server) {
9948
10299
  { timeoutMs: 1e4 }
9949
10300
  );
9950
10301
  const availableCredits = budgetData?.credits ?? 0;
10302
+ const isUnlimited = budgetData?.is_unlimited === true;
9951
10303
  const creditLimit = max_credits ?? availableCredits;
9952
- if (availableCredits < estimatedCost) {
10304
+ if (!isUnlimited && availableCredits < estimatedCost) {
9953
10305
  return {
9954
10306
  content: [
9955
10307
  {
@@ -9976,6 +10328,7 @@ function registerPipelineTools(server) {
9976
10328
  approval_mode,
9977
10329
  auto_approve_threshold,
9978
10330
  dry_run,
10331
+ schedule_confirmed,
9979
10332
  skip_stages: skip_stages ?? []
9980
10333
  },
9981
10334
  current_stage: "planning",
@@ -10041,7 +10394,9 @@ function registerPipelineTools(server) {
10041
10394
  stagesCompleted.push("planning");
10042
10395
  const rawText = String(planData.text ?? planData.content ?? "");
10043
10396
  const postsArray = extractJsonArray(rawText);
10044
- const posts = (postsArray ?? []).map((p) => ({
10397
+ const requestedPlatformSet = new Set(platforms);
10398
+ const maxPosts = platforms.length * days * posts_per_day;
10399
+ const parsedPosts = (postsArray ?? []).map((p) => ({
10045
10400
  id: String(p.id ?? randomUUID3().slice(0, 8)),
10046
10401
  day: Number(p.day ?? 1),
10047
10402
  date: String(p.date ?? ""),
@@ -10055,6 +10410,23 @@ function registerPipelineTools(server) {
10055
10410
  visual_direction: p.visual_direction ? String(p.visual_direction) : void 0,
10056
10411
  media_type: p.media_type ? String(p.media_type) : void 0
10057
10412
  }));
10413
+ const platformFilteredPosts = parsedPosts.filter(
10414
+ (post) => requestedPlatformSet.has(post.platform)
10415
+ );
10416
+ const posts = platformFilteredPosts.slice(0, maxPosts);
10417
+ if (parsedPosts.length > maxPosts) {
10418
+ errors.push({
10419
+ stage: "planning",
10420
+ message: `AI returned ${parsedPosts.length} posts; truncated to ${maxPosts}.`
10421
+ });
10422
+ }
10423
+ const invalidPlatformCount = parsedPosts.length - platformFilteredPosts.length;
10424
+ if (invalidPlatformCount > 0) {
10425
+ errors.push({
10426
+ stage: "planning",
10427
+ message: `Dropped ${invalidPlatformCount} post(s) with unrequested or invalid platform.`
10428
+ });
10429
+ }
10058
10430
  let postsApproved = 0;
10059
10431
  let postsFlagged = 0;
10060
10432
  if (!skipSet.has("quality")) {
@@ -10175,21 +10547,27 @@ function registerPipelineTools(server) {
10175
10547
  let postsScheduled = 0;
10176
10548
  if (!dry_run && !skipSet.has("schedule") && postsApproved > 0) {
10177
10549
  const approvedPosts = posts.filter((p) => p.status === "approved");
10550
+ const scheduleBase = /* @__PURE__ */ new Date();
10551
+ scheduleBase.setDate(scheduleBase.getDate() + 1);
10552
+ const scheduleBaseMs = scheduleBase.getTime();
10178
10553
  for (const post of approvedPosts) {
10179
10554
  if (creditsUsed >= creditLimit) {
10180
10555
  errors.push({ stage: "schedule", message: "Credit limit reached" });
10181
10556
  break;
10182
10557
  }
10558
+ const scheduledAt = post.schedule_at ?? new Date(scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5).toISOString();
10183
10559
  try {
10184
10560
  const { error: schedError } = await callEdgeFunction(
10185
10561
  "schedule-post",
10186
10562
  {
10187
- platform: post.platform,
10563
+ platforms: [post.platform],
10188
10564
  caption: post.caption,
10189
10565
  title: post.title,
10190
10566
  hashtags: post.hashtags,
10191
- media_url: post.media_url,
10192
- scheduled_at: post.schedule_at,
10567
+ mediaUrl: post.media_url,
10568
+ scheduledAt,
10569
+ planId,
10570
+ idempotencyKey: `pipeline-${planId}-${post.id}`,
10193
10571
  ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
10194
10572
  },
10195
10573
  { timeoutMs: 15e3 }
@@ -10201,6 +10579,7 @@ function registerPipelineTools(server) {
10201
10579
  });
10202
10580
  } else {
10203
10581
  postsScheduled++;
10582
+ creditsUsed += SCHEDULE_POST_CREDITS;
10204
10583
  }
10205
10584
  } catch (schedErr) {
10206
10585
  errors.push({
@@ -10255,7 +10634,7 @@ function registerPipelineTools(server) {
10255
10634
  if (response_format === "json") {
10256
10635
  return {
10257
10636
  content: [
10258
- { type: "text", text: JSON.stringify(asEnvelope19(resultPayload), null, 2) }
10637
+ { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
10259
10638
  ]
10260
10639
  };
10261
10640
  }
@@ -10336,7 +10715,7 @@ function registerPipelineTools(server) {
10336
10715
  }
10337
10716
  if (format === "json") {
10338
10717
  return {
10339
- content: [{ type: "text", text: JSON.stringify(asEnvelope19(data), null, 2) }]
10718
+ content: [{ type: "text", text: JSON.stringify(asEnvelope20(data), null, 2) }]
10340
10719
  };
10341
10720
  }
10342
10721
  const lines = [];
@@ -10470,7 +10849,7 @@ function registerPipelineTools(server) {
10470
10849
  if (response_format === "json") {
10471
10850
  return {
10472
10851
  content: [
10473
- { type: "text", text: JSON.stringify(asEnvelope19(resultPayload), null, 2) }
10852
+ { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
10474
10853
  ]
10475
10854
  };
10476
10855
  }
@@ -10518,7 +10897,7 @@ function buildPlanPrompt(topic, platforms, days, postsPerDay, sourceUrl) {
10518
10897
 
10519
10898
  // src/tools/suggest.ts
10520
10899
  import { z as z25 } from "zod";
10521
- function asEnvelope20(data) {
10900
+ function asEnvelope21(data) {
10522
10901
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
10523
10902
  }
10524
10903
  function registerSuggestTools(server) {
@@ -10626,7 +11005,7 @@ function registerSuggestTools(server) {
10626
11005
  if (format === "json") {
10627
11006
  return {
10628
11007
  content: [
10629
- { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
11008
+ { type: "text", text: JSON.stringify(asEnvelope21(resultPayload), null, 2) }
10630
11009
  ]
10631
11010
  };
10632
11011
  }
@@ -10761,7 +11140,7 @@ function detectAnomalies(currentData, previousData, sensitivity = "medium", aver
10761
11140
  }
10762
11141
 
10763
11142
  // src/tools/digest.ts
10764
- function asEnvelope21(data) {
11143
+ function asEnvelope22(data) {
10765
11144
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
10766
11145
  }
10767
11146
  var PLATFORM_ENUM3 = z26.enum([
@@ -10930,7 +11309,7 @@ function registerDigestTools(server) {
10930
11309
  };
10931
11310
  if (format === "json") {
10932
11311
  return {
10933
- content: [{ type: "text", text: JSON.stringify(asEnvelope21(digest), null, 2) }]
11312
+ content: [{ type: "text", text: JSON.stringify(asEnvelope22(digest), null, 2) }]
10934
11313
  };
10935
11314
  }
10936
11315
  const lines = [];
@@ -11029,7 +11408,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
11029
11408
  if (format === "json") {
11030
11409
  return {
11031
11410
  content: [
11032
- { type: "text", text: JSON.stringify(asEnvelope21(resultPayload), null, 2) }
11411
+ { type: "text", text: JSON.stringify(asEnvelope22(resultPayload), null, 2) }
11033
11412
  ]
11034
11413
  };
11035
11414
  }
@@ -11076,18 +11455,129 @@ var WEIGHTS = {
11076
11455
  structuralPatterns: 0.05
11077
11456
  };
11078
11457
  function norm(content) {
11079
- return content.toLowerCase().replace(/[^a-z0-9\s]/g, " ");
11458
+ return content.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
11080
11459
  }
11081
11460
  function findMatches(content, terms) {
11082
11461
  const n = norm(content);
11083
- return terms.filter((t) => n.includes(t.toLowerCase()));
11462
+ return terms.filter((t) => {
11463
+ const nt = norm(t);
11464
+ return nt.length > 0 && n.includes(nt);
11465
+ });
11084
11466
  }
11085
11467
  function findMissing(content, terms) {
11086
11468
  const n = norm(content);
11087
- return terms.filter((t) => !n.includes(t.toLowerCase()));
11469
+ return terms.filter((t) => {
11470
+ const nt = norm(t);
11471
+ return nt.length > 0 && !n.includes(nt);
11472
+ });
11473
+ }
11474
+ function asStringArray(v) {
11475
+ if (Array.isArray(v)) return v.map((x) => String(x ?? "").trim()).filter(Boolean);
11476
+ if (typeof v === "string") {
11477
+ return v.split(/[,;\n]/).map((s) => s.trim()).filter(Boolean);
11478
+ }
11479
+ return [];
11480
+ }
11481
+ function extractAtomicTerms(entries) {
11482
+ const out = [];
11483
+ for (const raw of entries) {
11484
+ const e = (raw || "").trim();
11485
+ if (!e) continue;
11486
+ const quoted = e.match(/['"“”‘’]([^'"“”‘’]{2,40})['"“”‘’]/g);
11487
+ if (quoted) {
11488
+ for (const q of quoted) out.push(q.replace(/['"“”‘’]/g, "").trim());
11489
+ continue;
11490
+ }
11491
+ const words = e.split(/\s+/);
11492
+ const isGuidancePhrase = words.length > 2 && /\b(tone|voice|style|approach|language)\b/i.test(e);
11493
+ if (!e.includes("(") && words.length <= 3 && !isGuidancePhrase) out.push(e);
11494
+ }
11495
+ return Array.from(new Set(out.map((t) => t.trim()).filter(Boolean)));
11496
+ }
11497
+ function hasField(obj, key) {
11498
+ return Object.prototype.hasOwnProperty.call(obj, key) && obj[key] != null;
11499
+ }
11500
+ function unionTerms(existing, incoming) {
11501
+ const out = [];
11502
+ const seen = /* @__PURE__ */ new Set();
11503
+ for (const t of [...existing, ...incoming]) {
11504
+ if (t && !seen.has(t)) {
11505
+ seen.add(t);
11506
+ out.push(t);
11507
+ }
11508
+ }
11509
+ return out;
11510
+ }
11511
+ function mergeVoiceVocabulary(resolved, raw) {
11512
+ const voice = raw.voice;
11513
+ if (!voice || typeof voice !== "object") return resolved;
11514
+ const vp = asStringArray(voice.preferredTerms);
11515
+ const vb = asStringArray(voice.bannedTerms);
11516
+ if (!vp.length && !vb.length) return resolved;
11517
+ const existing = resolved.vocabularyRules ?? {};
11518
+ return {
11519
+ ...resolved,
11520
+ vocabularyRules: {
11521
+ preferredTerms: unionTerms(asStringArray(existing.preferredTerms), vp),
11522
+ bannedTerms: unionTerms(asStringArray(existing.bannedTerms), vb)
11523
+ }
11524
+ };
11525
+ }
11526
+ function resolveBrandProfile(raw) {
11527
+ if (!raw || typeof raw !== "object") return {};
11528
+ const r = raw;
11529
+ const name = typeof r.name === "string" ? r.name : void 0;
11530
+ const voiceProfile = r.voiceProfile;
11531
+ const vocabularyRules = r.vocabularyRules;
11532
+ if (voiceProfile && vocabularyRules) {
11533
+ return mergeVoiceVocabulary(raw, r);
11534
+ }
11535
+ if (voiceProfile) {
11536
+ return mergeVoiceVocabulary(
11537
+ {
11538
+ name,
11539
+ voiceProfile,
11540
+ vocabularyRules: {
11541
+ bannedTerms: extractAtomicTerms(asStringArray(voiceProfile.avoidPatterns)),
11542
+ preferredTerms: asStringArray(voiceProfile.languagePatterns)
11543
+ },
11544
+ targetAudience: r.targetAudience,
11545
+ writingStyleRules: r.writingStyleRules
11546
+ },
11547
+ r
11548
+ );
11549
+ }
11550
+ const isFlat = hasField(r, "voiceTone") || hasField(r, "voiceTags") || hasField(r, "discouragedTerms") || hasField(r, "preferredTerms");
11551
+ if (isFlat) {
11552
+ const ta = r.targetAudience;
11553
+ const psy = ta?.psychographics;
11554
+ const painPoints = ta ? asStringArray(psy?.painPoints ?? ta.painPoints) : [];
11555
+ const interests = ta ? asStringArray(psy?.interests ?? ta.interests) : [];
11556
+ return mergeVoiceVocabulary(
11557
+ {
11558
+ name,
11559
+ voiceProfile: {
11560
+ tone: [...asStringArray(r.voiceTags), ...asStringArray(r.voiceTone)],
11561
+ style: asStringArray(r.voiceStyle),
11562
+ languagePatterns: [],
11563
+ avoidPatterns: []
11564
+ },
11565
+ vocabularyRules: {
11566
+ bannedTerms: asStringArray(r.discouragedTerms),
11567
+ preferredTerms: asStringArray(r.preferredTerms)
11568
+ },
11569
+ targetAudience: painPoints.length || interests.length ? { psychographics: { painPoints, interests } } : void 0
11570
+ },
11571
+ r
11572
+ );
11573
+ }
11574
+ const stub = { name };
11575
+ if (vocabularyRules)
11576
+ stub.vocabularyRules = vocabularyRules;
11577
+ return mergeVoiceVocabulary(stub, r);
11088
11578
  }
11089
11579
  var FABRICATION_PATTERNS = [
11090
- { regex: /\b\d+[,.]?\d*\s*(%|percent)/gi, label: "unverified percentage" },
11580
+ { regex: /\b\d+(?:[,.]\d+)?\s*(?:%|percent\b)/gi, label: "unverified percentage" },
11091
11581
  { regex: /\b(award[- ]?winning|best[- ]selling|#\s*1)\b/gi, label: "unverified ranking" },
11092
11582
  {
11093
11583
  regex: /\b(guaranteed|proven to|studies show|scientifically proven)\b/gi,
@@ -11303,24 +11793,25 @@ function computeBrandConsistency(content, profile, threshold = 60) {
11303
11793
  fabricationWarnings: []
11304
11794
  };
11305
11795
  }
11796
+ const resolved = resolveBrandProfile(profile);
11306
11797
  const dimensions = {
11307
- toneAlignment: scoreTone(content, profile),
11308
- vocabularyAdherence: scoreVocab(content, profile),
11309
- avoidCompliance: scoreAvoid(content, profile),
11310
- audienceRelevance: scoreAudience(content, profile),
11311
- brandMentions: scoreBrand(content, profile),
11312
- structuralPatterns: scoreStructure(content, profile)
11798
+ toneAlignment: scoreTone(content, resolved),
11799
+ vocabularyAdherence: scoreVocab(content, resolved),
11800
+ avoidCompliance: scoreAvoid(content, resolved),
11801
+ audienceRelevance: scoreAudience(content, resolved),
11802
+ brandMentions: scoreBrand(content, resolved),
11803
+ structuralPatterns: scoreStructure(content, resolved)
11313
11804
  };
11314
11805
  const overall = Math.round(
11315
11806
  Object.values(dimensions).reduce((sum, d) => sum + d.score * d.weight, 0)
11316
11807
  );
11317
11808
  const preferred = [
11318
- ...profile.voiceProfile?.languagePatterns || [],
11319
- ...profile.vocabularyRules?.preferredTerms || []
11809
+ ...resolved.voiceProfile?.languagePatterns || [],
11810
+ ...resolved.vocabularyRules?.preferredTerms || []
11320
11811
  ];
11321
11812
  const banned = [
11322
- ...profile.voiceProfile?.avoidPatterns || [],
11323
- ...profile.vocabularyRules?.bannedTerms || []
11813
+ ...resolved.voiceProfile?.avoidPatterns || [],
11814
+ ...resolved.vocabularyRules?.bannedTerms || []
11324
11815
  ];
11325
11816
  const fabrications = detectFabricationPatterns(content);
11326
11817
  return {
@@ -11351,11 +11842,11 @@ function hexToLab(hex) {
11351
11842
  const lb = srgbToLinear(b);
11352
11843
  const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
11353
11844
  const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
11354
- const z36 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
11845
+ const z39 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
11355
11846
  const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
11356
11847
  const fx = f(x / 0.95047);
11357
11848
  const fy = f(y / 1);
11358
- const fz = f(z36 / 1.08883);
11849
+ const fz = f(z39 / 1.08883);
11359
11850
  return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
11360
11851
  }
11361
11852
  function deltaE2000(lab1, lab2) {
@@ -11507,7 +11998,7 @@ function exportFigma(palette, typography) {
11507
11998
  }
11508
11999
 
11509
12000
  // src/tools/brandRuntime.ts
11510
- function asEnvelope22(data) {
12001
+ function asEnvelope23(data) {
11511
12002
  return {
11512
12003
  _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
11513
12004
  data
@@ -11588,8 +12079,9 @@ function registerBrandRuntimeTools(server) {
11588
12079
  pagesScraped: meta.pagesScraped || 0
11589
12080
  }
11590
12081
  };
11591
- const envelope = asEnvelope22(runtime);
12082
+ const envelope = asEnvelope23(runtime);
11592
12083
  return {
12084
+ structuredContent: envelope,
11593
12085
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
11594
12086
  };
11595
12087
  }
@@ -11730,7 +12222,7 @@ function registerBrandRuntimeTools(server) {
11730
12222
  }
11731
12223
  const profile = row.profile_data;
11732
12224
  const checkResult = computeBrandConsistency(content, profile);
11733
- const envelope = asEnvelope22(checkResult);
12225
+ const envelope = asEnvelope23(checkResult);
11734
12226
  return {
11735
12227
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
11736
12228
  };
@@ -11764,7 +12256,7 @@ function registerBrandRuntimeTools(server) {
11764
12256
  content_colors,
11765
12257
  threshold ?? 10
11766
12258
  );
11767
- const envelope = asEnvelope22(auditResult);
12259
+ const envelope = asEnvelope23(auditResult);
11768
12260
  return {
11769
12261
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
11770
12262
  };
@@ -11799,7 +12291,7 @@ function registerBrandRuntimeTools(server) {
11799
12291
  row.profile_data.typography,
11800
12292
  format
11801
12293
  );
11802
- const envelope = asEnvelope22({ format, tokens: output });
12294
+ const envelope = asEnvelope23({ format, tokens: output });
11803
12295
  return {
11804
12296
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
11805
12297
  };
@@ -11810,57 +12302,6 @@ function registerBrandRuntimeTools(server) {
11810
12302
  // src/tools/carousel.ts
11811
12303
  import { z as z28 } from "zod";
11812
12304
  init_supabase();
11813
- init_request_context();
11814
- var MAX_CREDITS_PER_RUN2 = Math.max(0, Number(process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN || 0));
11815
- var MAX_ASSETS_PER_RUN2 = Math.max(0, Number(process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN || 0));
11816
- var _globalCreditsUsed2 = 0;
11817
- var _globalAssetsGenerated2 = 0;
11818
- function getCreditsUsed2() {
11819
- const ctx = requestContext.getStore();
11820
- return ctx ? ctx.creditsUsed : _globalCreditsUsed2;
11821
- }
11822
- function addCreditsUsed2(amount) {
11823
- const ctx = requestContext.getStore();
11824
- if (ctx) {
11825
- ctx.creditsUsed += amount;
11826
- } else {
11827
- _globalCreditsUsed2 += amount;
11828
- }
11829
- }
11830
- function getAssetsGenerated2() {
11831
- const ctx = requestContext.getStore();
11832
- return ctx ? ctx.assetsGenerated : _globalAssetsGenerated2;
11833
- }
11834
- function addAssetsGenerated2(count) {
11835
- const ctx = requestContext.getStore();
11836
- if (ctx) {
11837
- ctx.assetsGenerated += count;
11838
- } else {
11839
- _globalAssetsGenerated2 += count;
11840
- }
11841
- }
11842
- function checkCreditBudget2(estimatedCost) {
11843
- if (MAX_CREDITS_PER_RUN2 <= 0) return { ok: true };
11844
- const used = getCreditsUsed2();
11845
- if (used + estimatedCost > MAX_CREDITS_PER_RUN2) {
11846
- return {
11847
- ok: false,
11848
- message: `Credit budget exceeded: ${used} used + ${estimatedCost} estimated > ${MAX_CREDITS_PER_RUN2} limit. Use a smaller slide count or cheaper image model.`
11849
- };
11850
- }
11851
- return { ok: true };
11852
- }
11853
- function checkAssetBudget2() {
11854
- if (MAX_ASSETS_PER_RUN2 <= 0) return { ok: true };
11855
- const gen = getAssetsGenerated2();
11856
- if (gen >= MAX_ASSETS_PER_RUN2) {
11857
- return {
11858
- ok: false,
11859
- message: `Asset limit reached: ${gen}/${MAX_ASSETS_PER_RUN2} assets generated this run.`
11860
- };
11861
- }
11862
- return { ok: true };
11863
- }
11864
12305
  var IMAGE_CREDIT_ESTIMATES2 = {
11865
12306
  midjourney: 20,
11866
12307
  "nano-banana": 15,
@@ -11869,7 +12310,7 @@ var IMAGE_CREDIT_ESTIMATES2 = {
11869
12310
  "flux-max": 50,
11870
12311
  "gpt4o-image": 40,
11871
12312
  imagen4: 35,
11872
- "imagen4-fast": 25,
12313
+ "imagen4-fast": 35,
11873
12314
  seedream: 20
11874
12315
  };
11875
12316
  async function fetchBrandVisualContext(projectId) {
@@ -11993,14 +12434,14 @@ function registerCarouselTools(server) {
11993
12434
  const carouselTextCost = 10 + slideCount * 2;
11994
12435
  const perImageCost = IMAGE_CREDIT_ESTIMATES2[image_model] ?? 30;
11995
12436
  const totalEstimatedCost = carouselTextCost + slideCount * perImageCost;
11996
- const budgetCheck = checkCreditBudget2(totalEstimatedCost);
12437
+ const budgetCheck = checkCreditBudget(totalEstimatedCost);
11997
12438
  if (!budgetCheck.ok) {
11998
12439
  return {
11999
12440
  content: [{ type: "text", text: budgetCheck.message }],
12000
12441
  isError: true
12001
12442
  };
12002
12443
  }
12003
- const assetBudget = checkAssetBudget2();
12444
+ const assetBudget = checkAssetBudget(slideCount);
12004
12445
  if (!assetBudget.ok) {
12005
12446
  return {
12006
12447
  content: [{ type: "text", text: assetBudget.message }],
@@ -12047,8 +12488,15 @@ function registerCarouselTools(server) {
12047
12488
  };
12048
12489
  }
12049
12490
  const carousel = carouselData.carousel;
12491
+ const imageAssetBudget = checkAssetBudget(carousel.slides.length);
12492
+ if (!imageAssetBudget.ok) {
12493
+ return {
12494
+ content: [{ type: "text", text: imageAssetBudget.message }],
12495
+ isError: true
12496
+ };
12497
+ }
12050
12498
  const textCredits = carousel.credits?.used ?? carouselTextCost;
12051
- addCreditsUsed2(textCredits);
12499
+ addCreditsUsed(textCredits);
12052
12500
  const imageJobs = await Promise.all(
12053
12501
  carousel.slides.map(async (slide) => {
12054
12502
  const promptParts = [];
@@ -12078,8 +12526,8 @@ function registerCarouselTools(server) {
12078
12526
  }
12079
12527
  const jobId = data.asyncJobId ?? data.taskId ?? null;
12080
12528
  if (jobId) {
12081
- addCreditsUsed2(perImageCost);
12082
- addAssetsGenerated2(1);
12529
+ addCreditsUsed(perImageCost);
12530
+ addAssetsGenerated(1);
12083
12531
  }
12084
12532
  return {
12085
12533
  slideNumber: slide.slideNumber,
@@ -12185,19 +12633,19 @@ function registerCarouselTools(server) {
12185
12633
 
12186
12634
  // src/tools/niche-research.ts
12187
12635
  import { z as z29 } from "zod";
12188
- function asEnvelope23(data) {
12636
+ function asEnvelope24(data) {
12189
12637
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
12190
12638
  }
12191
12639
  function registerNicheResearchTools(server) {
12192
12640
  server.tool(
12193
12641
  "find_winning_content",
12194
- "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).",
12642
+ "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.",
12195
12643
  {
12196
12644
  project_id: z29.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
12197
12645
  platform: z29.enum(["tiktok", "instagram", "youtube", "reddit", "twitter"]).optional().describe("Filter to one platform. Omit for all platforms."),
12198
12646
  days: z29.number().int().min(1).max(365).default(30).describe("Window: only return winners scanned within this many days."),
12199
12647
  limit: z29.number().int().min(1).max(50).default(10).describe("Number of winners to return (1-50)."),
12200
- 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."),
12648
+ min_qa_score: z29.number().min(0).max(1).default(0.5).describe("Minimum QA score (0..1). Default 0.5."),
12201
12649
  response_format: z29.enum(["text", "json"]).optional()
12202
12650
  },
12203
12651
  async ({ project_id, platform: platform2, days, limit, min_qa_score, response_format }) => {
@@ -12219,7 +12667,7 @@ function registerNicheResearchTools(server) {
12219
12667
  {
12220
12668
  type: "text",
12221
12669
  text: JSON.stringify(
12222
- asEnvelope23({
12670
+ asEnvelope24({
12223
12671
  winners,
12224
12672
  count: winners.length,
12225
12673
  filters: result?.filters ?? {}
@@ -12516,6 +12964,16 @@ import { z as z31 } from "zod";
12516
12964
  import fs from "node:fs/promises";
12517
12965
  import path from "node:path";
12518
12966
  var CALENDAR_URI = "ui://content-calendar/mcp-app.html";
12967
+ var RecentPostOutputSchema = z31.object({
12968
+ id: z31.string(),
12969
+ platform: z31.string(),
12970
+ status: z31.string(),
12971
+ title: z31.string().nullable(),
12972
+ external_post_id: z31.string().nullable(),
12973
+ published_at: z31.string().nullable(),
12974
+ scheduled_at: z31.string().nullable(),
12975
+ created_at: z31.string()
12976
+ });
12519
12977
  function startOfCurrentWeekMonday() {
12520
12978
  const now = /* @__PURE__ */ new Date();
12521
12979
  const day = now.getDay();
@@ -12535,6 +12993,11 @@ function registerContentCalendarApp(server) {
12535
12993
  "ISO date for the week start (YYYY-MM-DD); defaults to the current week's Monday."
12536
12994
  )
12537
12995
  },
12996
+ outputSchema: {
12997
+ start_date: z31.string(),
12998
+ posts: z31.array(RecentPostOutputSchema),
12999
+ scopes: z31.array(z31.string())
13000
+ },
12538
13001
  _meta: {
12539
13002
  ui: {
12540
13003
  resourceUri: CALENDAR_URI,
@@ -12573,11 +13036,17 @@ function registerContentCalendarApp(server) {
12573
13036
  if (!ts) return false;
12574
13037
  return ts.split("T")[0] >= fromDate;
12575
13038
  });
13039
+ const structuredContent = {
13040
+ start_date: fromDate,
13041
+ posts,
13042
+ scopes: userScopes
13043
+ };
12576
13044
  return {
13045
+ structuredContent,
12577
13046
  content: [
12578
13047
  {
12579
13048
  type: "text",
12580
- text: JSON.stringify({ posts, scopes: userScopes })
13049
+ text: `Loaded ${posts.length} calendar post${posts.length === 1 ? "" : "s"} from ${fromDate}.`
12581
13050
  }
12582
13051
  ]
12583
13052
  };
@@ -12834,7 +13303,7 @@ var writeReflectionSchema = {
12834
13303
  prm_score_ids: z33.array(z33.string()).optional().describe("PRM score IDs that informed this reflection."),
12835
13304
  handoff_ids: z33.array(z33.string()).optional().describe("Handoff event IDs that triggered this reflection.")
12836
13305
  }).strict().describe(
12837
- "Source evidence for this reflection. Only these four keys are accepted (Anti-Goodhart guard \u2014 unknown keys are rejected to prevent spurious provenance claims)."
13306
+ "Source evidence for this reflection. Only these four keys are accepted \u2014 unknown keys are rejected to prevent spurious provenance claims."
12838
13307
  ),
12839
13308
  brand_id: z33.string().describe("Brand profile UUID this reflection belongs to."),
12840
13309
  pipeline_id: z33.string().optional().describe("Optional: pipeline run UUID."),
@@ -12852,7 +13321,7 @@ var readReflectionSchema = {
12852
13321
  var recordOutcomeSchema = {
12853
13322
  decision_event_id: z33.string().describe("UUID of the decision_events row to record an outcome for."),
12854
13323
  horizon: z33.enum(["1h", "6h", "24h"]).describe(
12855
- "Observation horizon. Only horizon=24h with a non-null reward triggers a content_bandits posterior update; 1h/6h are stored but inert for learning."
13324
+ "Observation horizon. Only horizon=24h with a non-null reward triggers a learning-loop update; 1h/6h are stored but inert for learning."
12856
13325
  ),
12857
13326
  reward: z33.number().min(0).max(1).describe("Normalised reward signal in [0, 1]. Higher = better outcome."),
12858
13327
  outcome_metrics: z33.record(z33.string(), z33.number()).optional().describe(
@@ -12862,7 +13331,7 @@ var recordOutcomeSchema = {
12862
13331
  function registerHarnessTools(server, _ctx) {
12863
13332
  server.tool(
12864
13333
  "write_agent_reflection",
12865
- "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.",
13334
+ "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.",
12866
13335
  writeReflectionSchema,
12867
13336
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
12868
13337
  async (args) => {
@@ -12890,7 +13359,7 @@ function registerHarnessTools(server, _ctx) {
12890
13359
  );
12891
13360
  server.tool(
12892
13361
  "record_outcome",
12893
- "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.",
13362
+ "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.",
12894
13363
  recordOutcomeSchema,
12895
13364
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
12896
13365
  async (args) => {
@@ -12952,7 +13421,7 @@ function registerHarnessTools(server, _ctx) {
12952
13421
 
12953
13422
  // src/tools/hermes.ts
12954
13423
  import { z as z34 } from "zod";
12955
- function asEnvelope24(data) {
13424
+ function asEnvelope25(data) {
12956
13425
  return {
12957
13426
  _meta: {
12958
13427
  version: MCP_VERSION,
@@ -12974,13 +13443,13 @@ var PLATFORM = z34.enum([
12974
13443
  function registerHermesTools(server) {
12975
13444
  server.tool(
12976
13445
  "save_draft_to_library",
12977
- "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.",
13446
+ "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.",
12978
13447
  {
12979
13448
  platform: PLATFORM.describe("Target platform for the draft."),
12980
13449
  copy: z34.string().min(1).max(8e3).describe("The draft post body."),
12981
13450
  project_id: z34.string().optional().describe("SN project UUID. Optional but recommended."),
12982
13451
  media_url: z34.string().url().optional().describe("Optional cover/media URL (R2 signed URL)."),
12983
- hermes_run_id: z34.string().optional().describe("Hermes cron run id, for traceability."),
13452
+ hermes_run_id: z34.string().optional().describe("Agent run id, for traceability."),
12984
13453
  source_intel_ids: z34.array(z34.string()).optional().describe("Optional list of intel_signals.id rows that informed this draft."),
12985
13454
  response_format: z34.enum(["text", "json"]).optional()
12986
13455
  },
@@ -13003,7 +13472,7 @@ function registerHermesTools(server) {
13003
13472
  const payload = { content_id: data?.content_id ?? null };
13004
13473
  if (format === "json") {
13005
13474
  return {
13006
- content: [{ type: "text", text: JSON.stringify(asEnvelope24(payload), null, 2) }]
13475
+ content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
13007
13476
  };
13008
13477
  }
13009
13478
  return {
@@ -13047,7 +13516,7 @@ function registerHermesTools(server) {
13047
13516
  };
13048
13517
  if (response_format === "json") {
13049
13518
  return {
13050
- content: [{ type: "text", text: JSON.stringify(asEnvelope24(payload), null, 2) }]
13519
+ content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
13051
13520
  };
13052
13521
  }
13053
13522
  return {
@@ -13062,11 +13531,11 @@ function registerHermesTools(server) {
13062
13531
  );
13063
13532
  server.tool(
13064
13533
  "record_observation",
13065
- 'Record an agent observation ("Hermes noticed X this week"). Surfaces in UnifiedAnalytics > Playbook tab. Use for weekly reflection digests and mid-campaign pulse summaries.',
13534
+ '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.',
13066
13535
  {
13067
- summary: z34.string().min(1).max(2e3).describe("One-paragraph summary, founder-facing."),
13536
+ summary: z34.string().min(1).max(2e3).describe("One-paragraph summary, shown to the account owner."),
13068
13537
  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})."),
13069
- run_id: z34.string().optional().describe("Hermes cron run id for traceability."),
13538
+ run_id: z34.string().optional().describe("Agent run id for traceability."),
13070
13539
  response_format: z34.enum(["text", "json"]).optional()
13071
13540
  },
13072
13541
  async ({ summary, deltas, run_id, response_format }) => {
@@ -13077,7 +13546,7 @@ function registerHermesTools(server) {
13077
13546
  const payload = { observation_id: data?.observation_id ?? null };
13078
13547
  if (response_format === "json") {
13079
13548
  return {
13080
- content: [{ type: "text", text: JSON.stringify(asEnvelope24(payload), null, 2) }]
13549
+ content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
13081
13550
  };
13082
13551
  }
13083
13552
  return {
@@ -13121,7 +13590,7 @@ function registerHermesTools(server) {
13121
13590
  };
13122
13591
  if (response_format === "json") {
13123
13592
  return {
13124
- content: [{ type: "text", text: JSON.stringify(asEnvelope24(payload), null, 2) }]
13593
+ content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
13125
13594
  };
13126
13595
  }
13127
13596
  return {
@@ -13136,7 +13605,7 @@ function registerHermesTools(server) {
13136
13605
  );
13137
13606
  server.tool(
13138
13607
  "record_campaign_spend",
13139
- "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.",
13608
+ "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.",
13140
13609
  {
13141
13610
  campaign_id: z34.string().min(1).max(200).describe("Campaign identifier (slug)."),
13142
13611
  category: z34.enum([
@@ -13165,7 +13634,7 @@ function registerHermesTools(server) {
13165
13634
  };
13166
13635
  if (response_format === "json") {
13167
13636
  return {
13168
- content: [{ type: "text", text: JSON.stringify(asEnvelope24(payload), null, 2) }]
13637
+ content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
13169
13638
  };
13170
13639
  }
13171
13640
  return {
@@ -13180,7 +13649,7 @@ function registerHermesTools(server) {
13180
13649
  );
13181
13650
  server.tool(
13182
13651
  "get_active_campaigns",
13183
- "List currently-running campaigns with thesis, budget, hero format, and current spend. Used by Hermes pitch skill to bias drafts toward active campaign themes.",
13652
+ "List currently-running campaigns with thesis, budget, hero format, and current spend. Use to bias drafts toward active campaign themes.",
13184
13653
  {
13185
13654
  response_format: z34.enum(["text", "json"]).optional()
13186
13655
  },
@@ -13195,7 +13664,7 @@ function registerHermesTools(server) {
13195
13664
  content: [
13196
13665
  {
13197
13666
  type: "text",
13198
- text: JSON.stringify(asEnvelope24({ campaigns }), null, 2)
13667
+ text: JSON.stringify(asEnvelope25({ campaigns }), null, 2)
13199
13668
  }
13200
13669
  ]
13201
13670
  };
@@ -13226,6 +13695,338 @@ ${"=".repeat(40)}
13226
13695
  );
13227
13696
  }
13228
13697
 
13698
+ // src/tools/skills.ts
13699
+ import { z as z35 } from "zod";
13700
+
13701
+ // src/lib/skills-manifest.ts
13702
+ var SKILLS_MANIFEST = [
13703
+ {
13704
+ id: "skill-brand-locked-viral-hook-reel",
13705
+ name: "Brand-locked viral hook reel",
13706
+ studio: "video",
13707
+ category: "hook",
13708
+ 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.",
13709
+ hookFormula: "Pattern interrupt in first 0.5s + escalating stakes every 2s + brand-locked voice + cloned-from-winning-past-post structure.",
13710
+ estimatedCredits: 580,
13711
+ estimatedSeconds: 158,
13712
+ featured: true,
13713
+ stepCount: 9,
13714
+ inspiredBy: ["MrBeast", "Alex Hormozi"]
13715
+ }
13716
+ ];
13717
+ function getSkill(id) {
13718
+ return SKILLS_MANIFEST.find((s) => s.id === id);
13719
+ }
13720
+ function listSkills(opts) {
13721
+ let entries = SKILLS_MANIFEST;
13722
+ if (opts?.studio) entries = entries.filter((s) => s.studio === opts.studio);
13723
+ if (opts?.featuredOnly) entries = entries.filter((s) => s.featured);
13724
+ return entries;
13725
+ }
13726
+
13727
+ // src/tools/skills.ts
13728
+ function asEnvelope26(data) {
13729
+ return {
13730
+ _meta: {
13731
+ version: MCP_VERSION,
13732
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
13733
+ },
13734
+ data
13735
+ };
13736
+ }
13737
+ var STUDIO_VALUES = ["video", "avatar", "carousel", "voice", "caption", "edit"];
13738
+ function renderSkillSummary(skill) {
13739
+ const lines = [];
13740
+ lines.push(`${skill.name} (${skill.id})`);
13741
+ lines.push(` Studio: ${skill.studio} \xB7 Category: ${skill.category}`);
13742
+ if (skill.featured) lines.push(" \u2B50 Featured");
13743
+ lines.push(` ${skill.shortDescription}`);
13744
+ lines.push(` Hook: ${skill.hookFormula}`);
13745
+ lines.push(
13746
+ ` Cost: ~${skill.estimatedCredits} credits \xB7 ~${skill.estimatedSeconds}s \xB7 ${skill.stepCount} steps`
13747
+ );
13748
+ lines.push(` Inspired by: ${skill.inspiredBy.join(", ")}`);
13749
+ return lines.join("\n");
13750
+ }
13751
+ function registerSkillsTools(server) {
13752
+ server.tool(
13753
+ "list_skills",
13754
+ '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.',
13755
+ {
13756
+ studio: z35.enum(STUDIO_VALUES).optional().describe("Filter to one studio (video, avatar, carousel, voice, caption, edit)."),
13757
+ featured_only: z35.boolean().optional().describe("Return only featured (recommended) skills. Defaults to false."),
13758
+ response_format: z35.enum(["text", "json"]).optional().describe("Response format. Defaults to text \u2014 human-readable summary.")
13759
+ },
13760
+ async ({ studio, featured_only, response_format }) => {
13761
+ const skills = listSkills({ studio, featuredOnly: featured_only });
13762
+ const format = response_format ?? "text";
13763
+ if (format === "json") {
13764
+ return {
13765
+ content: [
13766
+ {
13767
+ type: "text",
13768
+ text: JSON.stringify(asEnvelope26({ count: skills.length, skills }), null, 2)
13769
+ }
13770
+ ]
13771
+ };
13772
+ }
13773
+ if (skills.length === 0) {
13774
+ return {
13775
+ content: [
13776
+ {
13777
+ type: "text",
13778
+ 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(", ")
13779
+ }
13780
+ ]
13781
+ };
13782
+ }
13783
+ const blocks = skills.map(renderSkillSummary).join("\n\n");
13784
+ const header = `${skills.length} skill${skills.length === 1 ? "" : "s"} available
13785
+ ${"=".repeat(40)}`;
13786
+ return {
13787
+ content: [{ type: "text", text: `${header}
13788
+
13789
+ ${blocks}` }]
13790
+ };
13791
+ }
13792
+ );
13793
+ server.tool(
13794
+ "run_skill",
13795
+ "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.",
13796
+ {
13797
+ skill_id: z35.string().describe(
13798
+ 'The id of the skill to run (e.g. "skill-brand-locked-viral-hook-reel"). Use list_skills to discover available ids.'
13799
+ ),
13800
+ topic: z35.string().min(1).describe('What the content is about (e.g. "why we built Social Neuron").'),
13801
+ audience: z35.string().optional().describe(
13802
+ 'Who the content is for (e.g. "first-time founders"). Defaults to brand persona.'
13803
+ ),
13804
+ hook: z35.string().optional().describe(
13805
+ "Optional explicit hook. If omitted, the skill drafts and scores 3-5 candidates."
13806
+ ),
13807
+ cta: z35.string().optional().describe("Optional call-to-action override. Defaults to brand standard CTA."),
13808
+ response_format: z35.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
13809
+ },
13810
+ async ({ skill_id, topic, audience, hook, cta, response_format }) => {
13811
+ const skill = getSkill(skill_id);
13812
+ const format = response_format ?? "text";
13813
+ if (!skill) {
13814
+ const knownIds = SKILLS_MANIFEST.map((s) => s.id).join(", ");
13815
+ return {
13816
+ content: [
13817
+ {
13818
+ type: "text",
13819
+ text: `Unknown skill_id "${skill_id}". Available skills: ${knownIds}. Call list_skills for descriptions.`
13820
+ }
13821
+ ],
13822
+ isError: true
13823
+ };
13824
+ }
13825
+ const runUrl = "https://socialneuron.com/dashboard/creation?skill=" + encodeURIComponent(skill.id);
13826
+ const inputs = { topic, audience, hook, cta };
13827
+ const previewedAt = (/* @__PURE__ */ new Date()).toISOString();
13828
+ if (format === "json") {
13829
+ return {
13830
+ content: [
13831
+ {
13832
+ type: "text",
13833
+ text: JSON.stringify(
13834
+ asEnvelope26({
13835
+ status: "preview",
13836
+ skill,
13837
+ inputs,
13838
+ runUrl,
13839
+ previewedAt,
13840
+ note: "This release returns a preview. A future release will execute in-process."
13841
+ }),
13842
+ null,
13843
+ 2
13844
+ )
13845
+ }
13846
+ ]
13847
+ };
13848
+ }
13849
+ const lines = [
13850
+ `Ready to run: ${skill.name}`,
13851
+ "=".repeat(40),
13852
+ "",
13853
+ skill.shortDescription,
13854
+ "",
13855
+ `Hook formula: ${skill.hookFormula}`,
13856
+ `Inspired by: ${skill.inspiredBy.join(", ")}`,
13857
+ "",
13858
+ "Inputs",
13859
+ ` topic : ${topic}`,
13860
+ ` audience : ${audience ?? "(brand persona)"}`,
13861
+ ` hook : ${hook ?? "(auto-drafted from 3-5 candidates, scored)"}`,
13862
+ ` cta : ${cta ?? "(brand default)"}`,
13863
+ "",
13864
+ "Plan",
13865
+ ` ${skill.stepCount} steps \xB7 ~${skill.estimatedCredits} credits \xB7 ~${skill.estimatedSeconds}s wall time`,
13866
+ "",
13867
+ `Launch in dashboard: ${runUrl}`,
13868
+ "",
13869
+ "\u26A0\uFE0F Preview only. End-to-end MCP execution arrives in a future release."
13870
+ ];
13871
+ return {
13872
+ content: [{ type: "text", text: lines.join("\n") }]
13873
+ };
13874
+ }
13875
+ );
13876
+ }
13877
+
13878
+ // src/tools/loopPulse.ts
13879
+ import { z as z36 } from "zod";
13880
+ function asEnvelope27(data) {
13881
+ return {
13882
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
13883
+ data
13884
+ };
13885
+ }
13886
+ function registerLoopPulseTools(server) {
13887
+ server.tool(
13888
+ "get_loop_pulse",
13889
+ '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.',
13890
+ {
13891
+ response_format: z36.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
13892
+ },
13893
+ async ({ response_format }) => {
13894
+ const format = response_format ?? "text";
13895
+ const { data, error } = await callEdgeFunction("mc-loop-pulse", {});
13896
+ if (error || !data) {
13897
+ return {
13898
+ content: [
13899
+ {
13900
+ type: "text",
13901
+ text: `Failed to read loop pulse: ${error || "no data"}`
13902
+ }
13903
+ ],
13904
+ isError: true
13905
+ };
13906
+ }
13907
+ if (format === "json") {
13908
+ return {
13909
+ content: [{ type: "text", text: JSON.stringify(asEnvelope27(data), null, 2) }]
13910
+ };
13911
+ }
13912
+ const rank = { bad: 0, warn: 1, unknown: 2, ok: 3 };
13913
+ const sorted = [...data.kpis].sort((a, b) => rank[a.status] - rank[b.status]);
13914
+ const lines = [
13915
+ `LOOP PULSE \u2014 overall: ${data.overall.toUpperCase()}`,
13916
+ "",
13917
+ ...sorted.map((k) => {
13918
+ const unit = k.unit ?? "";
13919
+ const val = k.value == null ? "\u2014" : `${k.value}${unit}`;
13920
+ return `[${k.status.toUpperCase().padEnd(4)}] ${k.label}: ${val}
13921
+ ${k.why}`;
13922
+ }),
13923
+ "",
13924
+ `Generated ${data.generated_at}`
13925
+ ];
13926
+ return {
13927
+ content: [{ type: "text", text: lines.join("\n") }]
13928
+ };
13929
+ }
13930
+ );
13931
+ }
13932
+
13933
+ // src/tools/banditState.ts
13934
+ import { z as z37 } from "zod";
13935
+ init_supabase();
13936
+ function asEnvelope28(data) {
13937
+ return {
13938
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
13939
+ data
13940
+ };
13941
+ }
13942
+ function registerBanditStateTools(server) {
13943
+ server.tool(
13944
+ "get_bandit_state",
13945
+ "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).",
13946
+ {
13947
+ project_id: z37.string().uuid().optional().describe("Project UUID. Defaults to the authenticated user default project."),
13948
+ platform: z37.enum([
13949
+ "instagram",
13950
+ "tiktok",
13951
+ "youtube",
13952
+ "linkedin",
13953
+ "twitter",
13954
+ "facebook",
13955
+ "threads",
13956
+ "bluesky"
13957
+ ]).optional().describe(
13958
+ "Lowercase platform name. Omit to return both platform-scoped and legacy platform-agnostic arms."
13959
+ ),
13960
+ arm_type: z37.enum([
13961
+ "hook_family",
13962
+ "hook_type",
13963
+ "format",
13964
+ "timing_slot",
13965
+ "topic_cluster",
13966
+ "caption_style",
13967
+ "platform",
13968
+ "story_type",
13969
+ "emoji_type",
13970
+ "length_bucket",
13971
+ "posting_time_bucket",
13972
+ "content_format"
13973
+ ]).optional().describe("Arm dimension. Omit to return all types grouped."),
13974
+ top_k: z37.number().min(1).max(50).optional().describe("Max arms per (arm_type) group. Default 5."),
13975
+ response_format: z37.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
13976
+ },
13977
+ async ({ project_id, platform: platform2, arm_type, top_k, response_format }) => {
13978
+ const format = response_format ?? "text";
13979
+ const projectId = project_id ?? await getDefaultProjectId();
13980
+ if (!projectId) {
13981
+ return {
13982
+ content: [
13983
+ { type: "text", text: "No project_id provided and no default project available." }
13984
+ ],
13985
+ isError: true
13986
+ };
13987
+ }
13988
+ const { data, error } = await callEdgeFunction("mc-bandit-state", {
13989
+ project_id: projectId,
13990
+ platform: platform2,
13991
+ arm_type,
13992
+ top_k
13993
+ });
13994
+ if (error || !data) {
13995
+ return {
13996
+ content: [{ type: "text", text: `Failed to read bandit state: ${error || "no data"}` }],
13997
+ isError: true
13998
+ };
13999
+ }
14000
+ if (format === "json") {
14001
+ return { content: [{ type: "text", text: JSON.stringify(asEnvelope28(data), null, 2) }] };
14002
+ }
14003
+ const lines = [
14004
+ `BANDIT STATE \u2014 project ${data.project_id.slice(0, 8)}${data.platform_filter ? ` \xB7 ${data.platform_filter}` : ""}`,
14005
+ `${data.total_arms} arms across ${data.groups.length} arm types`,
14006
+ ""
14007
+ ];
14008
+ for (const group of data.groups) {
14009
+ lines.push(`[${group.arm_type}] ${group.summary}`);
14010
+ const renderArm = (a) => {
14011
+ const conf = a.posterior_stdev < 0.05 ? "high" : a.posterior_stdev < 0.15 ? "med" : "low";
14012
+ 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)`;
14013
+ };
14014
+ if (group.platform_scoped.length > 0) {
14015
+ lines.push(` Per-platform top arms:`);
14016
+ group.platform_scoped.forEach((a) => lines.push(renderArm(a)));
14017
+ }
14018
+ if (group.platform_agnostic.length > 0) {
14019
+ lines.push(` Platform-agnostic (legacy) top arms:`);
14020
+ group.platform_agnostic.forEach((a) => lines.push(renderArm(a)));
14021
+ }
14022
+ lines.push("");
14023
+ }
14024
+ lines.push(`Generated ${data.generated_at}`);
14025
+ return { content: [{ type: "text", text: lines.join("\n") }] };
14026
+ }
14027
+ );
14028
+ }
14029
+
13229
14030
  // src/lib/register-tools.ts
13230
14031
  function wrapToolWithScanner(toolName, handler) {
13231
14032
  return async function scannerWrappedHandler(...handlerArgs) {
@@ -13278,7 +14079,8 @@ function wrapToolWithScanner(toolName, handler) {
13278
14079
  }
13279
14080
  function applyScopeEnforcement(server, scopeResolver) {
13280
14081
  const originalTool = server.tool.bind(server);
13281
- server.tool = function wrappedTool(...args) {
14082
+ const originalRegisterTool = server.registerTool?.bind(server);
14083
+ const wrapRegistration = (...args) => {
13282
14084
  const name = args[0];
13283
14085
  const requiredScope = TOOL_SCOPES[name];
13284
14086
  const handlerIndex = args.findIndex(
@@ -13289,27 +14091,11 @@ function applyScopeEnforcement(server, scopeResolver) {
13289
14091
  const scannerWrappedHandler = wrapToolWithScanner(name, originalHandler);
13290
14092
  args[handlerIndex] = async function scopeEnforcedHandler(...handlerArgs) {
13291
14093
  if (!requiredScope) {
13292
- return {
13293
- content: [
13294
- {
13295
- type: "text",
13296
- text: `Permission denied: '${name}' has no scope defined. Contact support.`
13297
- }
13298
- ],
13299
- isError: true
13300
- };
14094
+ return scopeDeniedResult(name, void 0, scopeResolver());
13301
14095
  }
13302
14096
  const userScopes = scopeResolver();
13303
14097
  if (!hasScope(userScopes, requiredScope)) {
13304
- return {
13305
- content: [
13306
- {
13307
- type: "text",
13308
- text: `Permission denied: '${name}' requires scope '${requiredScope}'. Generate a new key with the required scope at https://socialneuron.com/settings/developer`
13309
- }
13310
- ],
13311
- isError: true
13312
- };
14098
+ return scopeDeniedResult(name, requiredScope, userScopes);
13313
14099
  }
13314
14100
  const startedAt = Date.now();
13315
14101
  try {
@@ -13336,8 +14122,58 @@ function applyScopeEnforcement(server, scopeResolver) {
13336
14122
  }
13337
14123
  };
13338
14124
  }
13339
- return originalTool(...args);
14125
+ return args;
14126
+ };
14127
+ server.tool = function wrappedTool(...args) {
14128
+ return originalTool(...wrapRegistration(...args));
13340
14129
  };
14130
+ if (originalRegisterTool) {
14131
+ server.registerTool = function wrappedRegisterTool(...args) {
14132
+ return originalRegisterTool(...wrapRegistration(...args));
14133
+ };
14134
+ }
14135
+ }
14136
+ function scopeDeniedResult(name, requiredScope, userScopes) {
14137
+ const error = requiredScope ? {
14138
+ error: "permission_denied",
14139
+ tool: name,
14140
+ required_scope: requiredScope,
14141
+ available_scopes: userScopes,
14142
+ recover_with: [
14143
+ "Call search_tools with available_only=true to find tools this key can use.",
14144
+ "Use a read-only alternative if one is available for the task.",
14145
+ "Regenerate the API key with the required scope or upgrade the plan tier."
14146
+ ],
14147
+ developer_url: "https://socialneuron.com/settings/developer"
14148
+ } : {
14149
+ error: "tool_scope_missing",
14150
+ tool: name,
14151
+ available_scopes: userScopes,
14152
+ recover_with: ["Contact support; this tool is not mapped to a required scope."]
14153
+ };
14154
+ const challenge = requiredScope ? buildWwwAuthenticateHeader({
14155
+ issuerUrl: getChallengeIssuerUrl(),
14156
+ error: "insufficient_scope",
14157
+ errorDescription: `Tool ${name} requires scope ${requiredScope}.`,
14158
+ scope: requiredScope
14159
+ }) : void 0;
14160
+ return {
14161
+ content: [{ type: "text", text: JSON.stringify(error, null, 2) }],
14162
+ ...challenge ? { _meta: { "mcp/www_authenticate": [challenge] } } : {},
14163
+ isError: true
14164
+ };
14165
+ }
14166
+ function getChallengeIssuerUrl() {
14167
+ if (process.env.OAUTH_ISSUER_URL) return process.env.OAUTH_ISSUER_URL;
14168
+ const mcpServerUrl = process.env.MCP_SERVER_URL;
14169
+ if (mcpServerUrl) {
14170
+ try {
14171
+ const parsed = new URL(mcpServerUrl);
14172
+ return `${parsed.protocol}//${parsed.host}`;
14173
+ } catch {
14174
+ }
14175
+ }
14176
+ return "https://mcp.socialneuron.com";
13341
14177
  }
13342
14178
  var RESPONSE_CHAR_LIMIT = 1e5;
13343
14179
  function truncateResponse(result) {
@@ -13408,6 +14244,9 @@ function registerAllTools(server, options) {
13408
14244
  registerConnectionTools(server);
13409
14245
  registerHarnessTools(server, void 0);
13410
14246
  registerHermesTools(server);
14247
+ registerSkillsTools(server);
14248
+ registerLoopPulseTools(server);
14249
+ registerBanditStateTools(server);
13411
14250
  if (!options?.skipApps) {
13412
14251
  registerContentCalendarApp(server);
13413
14252
  }
@@ -13415,17 +14254,17 @@ function registerAllTools(server, options) {
13415
14254
  }
13416
14255
 
13417
14256
  // src/prompts.ts
13418
- import { z as z35 } from "zod";
14257
+ import { z as z38 } from "zod";
13419
14258
  function registerPrompts(server) {
13420
14259
  server.prompt(
13421
14260
  "create_weekly_content_plan",
13422
14261
  "Generate a full week of social media content (7 days, multiple platforms). Returns a structured plan with topics, formats, and posting times.",
13423
14262
  {
13424
- niche: z35.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
13425
- platforms: z35.string().optional().describe(
14263
+ niche: z38.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
14264
+ platforms: z38.string().optional().describe(
13426
14265
  'Comma-separated platforms to target (default: "YouTube, Instagram, TikTok, LinkedIn")'
13427
14266
  ),
13428
- tone: z35.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
14267
+ tone: z38.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
13429
14268
  },
13430
14269
  ({ niche, platforms, tone }) => {
13431
14270
  const targetPlatforms = platforms || "YouTube, Instagram, TikTok, LinkedIn";
@@ -13467,8 +14306,8 @@ After building the plan, use \`save_content_plan\` to save it.`
13467
14306
  "analyze_top_content",
13468
14307
  "Analyze your best-performing posts to identify patterns and replicate success. Returns insights on hooks, formats, timing, and topics that resonate.",
13469
14308
  {
13470
- timeframe: z35.string().optional().describe('Analysis period (default: "30 days"). E.g., "7 days", "90 days"'),
13471
- platform: z35.string().optional().describe('Filter to a specific platform (e.g., "youtube", "instagram")')
14309
+ timeframe: z38.string().optional().describe('Analysis period (default: "30 days"). E.g., "7 days", "90 days"'),
14310
+ platform: z38.string().optional().describe('Filter to a specific platform (e.g., "youtube", "instagram")')
13472
14311
  },
13473
14312
  ({ timeframe, platform: platform2 }) => {
13474
14313
  const period = timeframe || "30 days";
@@ -13505,10 +14344,10 @@ Format as a clear, actionable performance report.`
13505
14344
  "repurpose_content",
13506
14345
  "Take one piece of content and transform it into 8-10 pieces across multiple platforms and formats.",
13507
14346
  {
13508
- source: z35.string().describe(
14347
+ source: z38.string().describe(
13509
14348
  "The source content to repurpose \u2014 a URL, transcript, or the content text itself"
13510
14349
  ),
13511
- target_platforms: z35.string().optional().describe(
14350
+ target_platforms: z38.string().optional().describe(
13512
14351
  'Comma-separated target platforms (default: "Twitter, LinkedIn, Instagram, TikTok, YouTube")'
13513
14352
  )
13514
14353
  },
@@ -13550,9 +14389,9 @@ For each piece, include the platform, format, character count, and suggested pos
13550
14389
  "setup_brand_voice",
13551
14390
  "Define or refine your brand voice profile so all generated content stays on-brand. Walks through tone, audience, values, and style.",
13552
14391
  {
13553
- brand_name: z35.string().describe("Your brand or business name"),
13554
- industry: z35.string().optional().describe('Your industry or niche (e.g., "B2B SaaS", "fitness coaching")'),
13555
- website: z35.string().optional().describe("Your website URL for context")
14392
+ brand_name: z38.string().describe("Your brand or business name"),
14393
+ industry: z38.string().optional().describe('Your industry or niche (e.g., "B2B SaaS", "fitness coaching")'),
14394
+ website: z38.string().optional().describe("Your website URL for context")
13556
14395
  },
13557
14396
  ({ brand_name, industry, website }) => {
13558
14397
  const industryContext = industry ? ` in the ${industry} space` : "";
@@ -13793,13 +14632,13 @@ function registerResources(server) {
13793
14632
  pro: {
13794
14633
  price: "$49/mo",
13795
14634
  credits: 1500,
13796
- mcp_access: "Read + Analytics",
14635
+ mcp_access: "Read + Analytics + Write + Distribute",
13797
14636
  features: [
13798
14637
  "All Starter features",
13799
14638
  "Closed-loop optimization",
13800
- "5 autopilot runs / month",
14639
+ "Full agent engine via MCP (generate, gate, publish)",
13801
14640
  "All 10 platforms",
13802
- "MCP read + analytics"
14641
+ "MCP read + analytics + write + distribute"
13803
14642
  ]
13804
14643
  },
13805
14644
  team: {
@@ -13904,6 +14743,7 @@ function registerResources(server) {
13904
14743
  init_request_context();
13905
14744
 
13906
14745
  // src/lib/token-verifier.ts
14746
+ import { createHash as createHash3 } from "node:crypto";
13907
14747
  import * as jose from "jose";
13908
14748
  var jwks = null;
13909
14749
  function getJWKS(supabaseUrl) {
@@ -13913,39 +14753,89 @@ function getJWKS(supabaseUrl) {
13913
14753
  }
13914
14754
  return jwks;
13915
14755
  }
13916
- var apiKeyCache = /* @__PURE__ */ new Map();
13917
- var API_KEY_CACHE_TTL_MS = 1e4;
14756
+ var tokenValidationCache = /* @__PURE__ */ new Map();
14757
+ var CONNECTOR_TOKEN_VALIDATION_CACHE_TTL_MS = 3e5;
14758
+ var DEFAULT_MCP_RESOURCE = "https://mcp.socialneuron.com";
14759
+ function cacheKey(token) {
14760
+ return createHash3("sha256").update(token).digest("hex");
14761
+ }
13918
14762
  function evictFromCache(token) {
13919
- apiKeyCache.delete(token);
14763
+ tokenValidationCache.delete(cacheKey(token));
14764
+ }
14765
+ async function verifyCachedOpaqueToken(token, validate, ttlMs) {
14766
+ const key = cacheKey(token);
14767
+ const cached2 = tokenValidationCache.get(key);
14768
+ const now = Date.now();
14769
+ if (cached2 && cached2.expiresAt > now) {
14770
+ const tokenExpiresAtMs2 = cached2.authInfo.expiresAt ? cached2.authInfo.expiresAt * 1e3 : void 0;
14771
+ if (!tokenExpiresAtMs2 || tokenExpiresAtMs2 > now) {
14772
+ return cached2.authInfo;
14773
+ }
14774
+ }
14775
+ if (cached2) tokenValidationCache.delete(key);
14776
+ const authInfo = await validate();
14777
+ const tokenExpiresAtMs = authInfo.expiresAt ? authInfo.expiresAt * 1e3 : void 0;
14778
+ const cacheExpiresAt = Math.min(Date.now() + ttlMs, tokenExpiresAtMs ?? Number.POSITIVE_INFINITY);
14779
+ if (cacheExpiresAt > Date.now()) {
14780
+ tokenValidationCache.set(key, {
14781
+ authInfo,
14782
+ expiresAt: cacheExpiresAt
14783
+ });
14784
+ }
14785
+ if (tokenValidationCache.size > 100) {
14786
+ const now2 = Date.now();
14787
+ for (const [k, v] of tokenValidationCache) {
14788
+ if (v.expiresAt <= now2) tokenValidationCache.delete(k);
14789
+ }
14790
+ }
14791
+ return authInfo;
13920
14792
  }
13921
14793
  function createTokenVerifier(options) {
13922
14794
  const { supabaseUrl, supabaseAnonKey } = options;
14795
+ const expectedResource = normalizeResource(
14796
+ options.resource ?? process.env.MCP_RESOURCE_URL ?? process.env.MCP_SERVER_URL
14797
+ ) ?? DEFAULT_MCP_RESOURCE;
13923
14798
  return {
13924
14799
  async verifyAccessToken(token) {
13925
14800
  if (token.startsWith("snk_")) {
13926
- const cached = apiKeyCache.get(token);
13927
- if (cached && cached.expiresAt > Date.now()) {
13928
- return cached.authInfo;
13929
- }
13930
- if (cached) apiKeyCache.delete(token);
13931
- const authInfo = await verifyApiKey(token, supabaseUrl, supabaseAnonKey);
13932
- apiKeyCache.set(token, { authInfo, expiresAt: Date.now() + API_KEY_CACHE_TTL_MS });
13933
- if (apiKeyCache.size > 100) {
13934
- const now = Date.now();
13935
- for (const [k, v] of apiKeyCache) {
13936
- if (v.expiresAt <= now) apiKeyCache.delete(k);
13937
- }
13938
- }
13939
- return authInfo;
14801
+ return verifyApiKey(token, supabaseUrl, supabaseAnonKey);
14802
+ }
14803
+ if (token.startsWith("sno_")) {
14804
+ return verifyCachedOpaqueToken(
14805
+ token,
14806
+ () => verifyConnectorToken(token, supabaseUrl, supabaseAnonKey, expectedResource),
14807
+ CONNECTOR_TOKEN_VALIDATION_CACHE_TTL_MS
14808
+ );
13940
14809
  }
13941
14810
  return verifySupabaseJwt(token, supabaseUrl);
13942
14811
  }
13943
14812
  };
13944
14813
  }
14814
+ function normalizeResource(value) {
14815
+ if (!value) return void 0;
14816
+ try {
14817
+ const parsed = new URL(value);
14818
+ return `${parsed.protocol}//${parsed.host}`;
14819
+ } catch {
14820
+ return value.replace(/\/$/, "");
14821
+ }
14822
+ }
14823
+ function audienceIncludesExpected(audience, expectedResource) {
14824
+ if (typeof audience === "string") {
14825
+ return normalizeResource(audience) === expectedResource;
14826
+ }
14827
+ if (Array.isArray(audience)) {
14828
+ return audience.some((item) => audienceIncludesExpected(item, expectedResource));
14829
+ }
14830
+ return false;
14831
+ }
13945
14832
  async function verifySupabaseJwt(token, supabaseUrl) {
13946
14833
  const jwksKeySet = getJWKS(supabaseUrl);
13947
14834
  const { payload } = await jose.jwtVerify(token, jwksKeySet, {
13948
- issuer: `${supabaseUrl}/auth/v1`
14835
+ issuer: `${supabaseUrl}/auth/v1`,
14836
+ audience: "authenticated",
14837
+ algorithms: ["RS256", "ES256"],
14838
+ clockTolerance: "30s"
13949
14839
  });
13950
14840
  const userId = payload.sub;
13951
14841
  if (!userId) {
@@ -14004,9 +14894,59 @@ async function verifyApiKey(apiKey, supabaseUrl, supabaseAnonKey) {
14004
14894
  throw err;
14005
14895
  }
14006
14896
  }
14897
+ async function verifyConnectorToken(accessToken, supabaseUrl, supabaseAnonKey, expectedResource) {
14898
+ const controller = new AbortController();
14899
+ const timer = setTimeout(() => controller.abort(), 1e4);
14900
+ try {
14901
+ const response = await fetch(
14902
+ `${supabaseUrl}/functions/v1/mcp-auth?action=validate-connector-token`,
14903
+ {
14904
+ method: "POST",
14905
+ headers: {
14906
+ Authorization: `Bearer ${supabaseAnonKey}`,
14907
+ "Content-Type": "application/json"
14908
+ },
14909
+ body: JSON.stringify({ access_token: accessToken, resource: expectedResource }),
14910
+ signal: controller.signal
14911
+ }
14912
+ );
14913
+ if (!response.ok) {
14914
+ throw new Error(`Connector token validation failed: HTTP ${response.status}`);
14915
+ }
14916
+ const data = await response.json();
14917
+ if (!data.valid || !data.userId) {
14918
+ throw new Error(data.error ?? "Invalid connector token");
14919
+ }
14920
+ const expiresAt = data.expiresAt ? Math.floor(new Date(data.expiresAt).getTime() / 1e3) : void 0;
14921
+ if (expiresAt && expiresAt < Math.floor(Date.now() / 1e3)) {
14922
+ throw new Error("Connector token expired");
14923
+ }
14924
+ const tokenResource = data.resource ?? data.audience ?? data.aud;
14925
+ if (!audienceIncludesExpected(tokenResource, expectedResource)) {
14926
+ throw new Error("Connector token audience/resource mismatch");
14927
+ }
14928
+ return {
14929
+ token: accessToken,
14930
+ clientId: data.clientId ?? "connector-oauth",
14931
+ scopes: data.scopes ?? ["mcp:read"],
14932
+ expiresAt,
14933
+ extra: { userId: data.userId, email: data.email, resource: expectedResource }
14934
+ };
14935
+ } catch (err) {
14936
+ if (err instanceof Error && err.name === "AbortError") {
14937
+ throw new Error("Connector token validation timed out");
14938
+ }
14939
+ throw err;
14940
+ } finally {
14941
+ clearTimeout(timer);
14942
+ }
14943
+ }
14007
14944
 
14008
14945
  // src/lib/oauth-provider.ts
14009
- import { InvalidClientMetadataError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
14946
+ import {
14947
+ InvalidClientMetadataError,
14948
+ TooManyRequestsError
14949
+ } from "@modelcontextprotocol/sdk/server/auth/errors.js";
14010
14950
  init_supabase();
14011
14951
  var ALLOWED_REDIRECT_URIS = /* @__PURE__ */ new Set([
14012
14952
  // Claude Code local callback
@@ -14024,11 +14964,18 @@ var ALLOWED_REDIRECT_URIS = /* @__PURE__ */ new Set([
14024
14964
  "https://glama.ai/callback",
14025
14965
  "https://mcp.so/callback"
14026
14966
  ]);
14967
+ var ALLOWED_LOOPBACK_CALLBACK_PATHS = /* @__PURE__ */ new Set([
14968
+ "/oauth/callback",
14969
+ "/oauth/callback/debug",
14970
+ // Codex uses an ephemeral loopback server with this callback path.
14971
+ "/callback"
14972
+ ]);
14973
+ var CODEX_LOOPBACK_CALLBACK_PATH_RE = /^\/callback\/[A-Za-z0-9_-]+$/;
14027
14974
  function isAllowedRedirectUri(uri) {
14028
14975
  if (ALLOWED_REDIRECT_URIS.has(uri)) return true;
14029
14976
  try {
14030
14977
  const parsed = new URL(uri);
14031
- if ((parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1") && (parsed.pathname === "/oauth/callback" || parsed.pathname === "/oauth/callback/debug") && parsed.protocol === "http:") {
14978
+ if ((parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1") && (ALLOWED_LOOPBACK_CALLBACK_PATHS.has(parsed.pathname) || CODEX_LOOPBACK_CALLBACK_PATH_RE.test(parsed.pathname)) && parsed.protocol === "http:") {
14032
14979
  return true;
14033
14980
  }
14034
14981
  if (parsed.protocol === "https:") {
@@ -14038,6 +14985,45 @@ function isAllowedRedirectUri(uri) {
14038
14985
  }
14039
14986
  return false;
14040
14987
  }
14988
+ var MAX_CLIENT_NAME_BYTES = 512;
14989
+ var MAX_REDIRECT_URIS = 10;
14990
+ var MAX_REDIRECT_URI_BYTES = 2048;
14991
+ var MAX_METADATA_BYTES = 8192;
14992
+ var MAX_CACHED_CLIENTS = 1e3;
14993
+ var MAX_PERSISTED_CLIENTS = 5e3;
14994
+ var OAUTH_CLIENT_RETENTION_DAYS = 90;
14995
+ function byteLength(value) {
14996
+ return Buffer.byteLength(value, "utf8");
14997
+ }
14998
+ function assertMaxBytes(label, value, maxBytes) {
14999
+ if (value !== void 0 && byteLength(value) > maxBytes) {
15000
+ throw new InvalidClientMetadataError(`${label} exceeds ${maxBytes} bytes`);
15001
+ }
15002
+ }
15003
+ function assertRegistrationWithinBounds(client3) {
15004
+ assertMaxBytes("client_name", client3.client_name, MAX_CLIENT_NAME_BYTES);
15005
+ if ((client3.redirect_uris?.length ?? 0) > MAX_REDIRECT_URIS) {
15006
+ throw new InvalidClientMetadataError(`redirect_uris exceeds ${MAX_REDIRECT_URIS} entries`);
15007
+ }
15008
+ for (const uri of client3.redirect_uris ?? []) {
15009
+ assertMaxBytes("redirect_uri", uri, MAX_REDIRECT_URI_BYTES);
15010
+ }
15011
+ const metadataBytes = byteLength(JSON.stringify(clientToRow(client3).metadata));
15012
+ if (metadataBytes > MAX_METADATA_BYTES) {
15013
+ throw new InvalidClientMetadataError(`client metadata exceeds ${MAX_METADATA_BYTES} bytes`);
15014
+ }
15015
+ }
15016
+ function cacheClient(cache, clientId, client3) {
15017
+ if (cache.has(clientId)) {
15018
+ cache.delete(clientId);
15019
+ }
15020
+ cache.set(clientId, client3);
15021
+ while (cache.size > MAX_CACHED_CLIENTS) {
15022
+ const oldestClientId = cache.keys().next().value;
15023
+ if (!oldestClientId) break;
15024
+ cache.delete(oldestClientId);
15025
+ }
15026
+ }
14041
15027
  function rowToClient(row) {
14042
15028
  return {
14043
15029
  client_id: row.client_id,
@@ -14082,8 +15068,8 @@ function createClientsStore() {
14082
15068
  }
14083
15069
  return {
14084
15070
  async getClient(clientId) {
14085
- const cached = cache.get(clientId);
14086
- if (cached) return cached;
15071
+ const cached2 = cache.get(clientId);
15072
+ if (cached2) return cached2;
14087
15073
  if (!supabaseAvailable) return void 0;
14088
15074
  try {
14089
15075
  const supabase = getSupabaseClient();
@@ -14094,7 +15080,7 @@ function createClientsStore() {
14094
15080
  }
14095
15081
  if (!data || Array.isArray(data) && data.length === 0) return void 0;
14096
15082
  const client3 = rowToClient(data);
14097
- cache.set(clientId, client3);
15083
+ cacheClient(cache, clientId, client3);
14098
15084
  void supabase.from("mcp_oauth_clients").update({ last_used_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("client_id", clientId);
14099
15085
  return client3;
14100
15086
  } catch (err) {
@@ -14110,18 +15096,38 @@ function createClientsStore() {
14110
15096
  }
14111
15097
  }
14112
15098
  }
14113
- cache.set(client3.client_id, client3);
15099
+ assertRegistrationWithinBounds(client3);
14114
15100
  if (supabaseAvailable) {
14115
15101
  try {
14116
15102
  const supabase = getSupabaseClient();
14117
- const { error } = await supabase.from("mcp_oauth_clients").insert(clientToRow(client3));
14118
- if (error) {
14119
- markUnavailable(error.message);
15103
+ const retentionCutoff = new Date(
15104
+ Date.now() - OAUTH_CLIENT_RETENTION_DAYS * 24 * 60 * 60 * 1e3
15105
+ ).toISOString();
15106
+ const { error: pruneError } = await supabase.from("mcp_oauth_clients").delete().lt("last_used_at", retentionCutoff);
15107
+ if (pruneError) {
15108
+ markUnavailable(pruneError.message);
15109
+ } else {
15110
+ const { count, error: countError } = await supabase.from("mcp_oauth_clients").select("client_id", { count: "exact", head: true });
15111
+ if (countError) {
15112
+ markUnavailable(countError.message);
15113
+ } else {
15114
+ if ((count ?? 0) >= MAX_PERSISTED_CLIENTS) {
15115
+ throw new TooManyRequestsError(
15116
+ "OAuth client registration capacity reached; retry later"
15117
+ );
15118
+ }
15119
+ const { error } = await supabase.from("mcp_oauth_clients").insert(clientToRow(client3));
15120
+ if (error) {
15121
+ markUnavailable(error.message);
15122
+ }
15123
+ }
14120
15124
  }
14121
15125
  } catch (err) {
15126
+ if (err instanceof TooManyRequestsError) throw err;
14122
15127
  markUnavailable(err instanceof Error ? err.message : "unknown error");
14123
15128
  }
14124
15129
  }
15130
+ cacheClient(cache, client3.client_id, client3);
14125
15131
  return client3;
14126
15132
  }
14127
15133
  };
@@ -14154,6 +15160,10 @@ function createOAuthProvider(options) {
14154
15160
  if (params.redirectUri) {
14155
15161
  consentUrl.searchParams.set("redirect_uri", params.redirectUri);
14156
15162
  }
15163
+ const resource = params.resource;
15164
+ if (resource) {
15165
+ consentUrl.searchParams.set("resource", resource.href);
15166
+ }
14157
15167
  res.redirect(consentUrl.toString());
14158
15168
  },
14159
15169
  async challengeForAuthorizationCode(_client, _authorizationCode) {
@@ -14161,7 +15171,7 @@ function createOAuthProvider(options) {
14161
15171
  "Local PKCE validation is disabled \u2014 challengeForAuthorizationCode should not be called. PKCE is verified server-side by the mcp-auth Edge Function."
14162
15172
  );
14163
15173
  },
14164
- async exchangeAuthorizationCode(_client, authorizationCode, codeVerifier, redirectUri) {
15174
+ async exchangeAuthorizationCode(client3, authorizationCode, codeVerifier, redirectUri, resource) {
14165
15175
  if (!codeVerifier) {
14166
15176
  throw new Error("code_verifier is required for PKCE exchange");
14167
15177
  }
@@ -14179,8 +15189,14 @@ function createOAuthProvider(options) {
14179
15189
  code_verifier: codeVerifier,
14180
15190
  authorization_code: authorizationCode,
14181
15191
  return_token: true,
15192
+ // Forward client_id so the Edge Function can match it against
15193
+ // the client that the code was originally issued to.
15194
+ client_id: client3.client_id,
14182
15195
  // Pass redirect_uri for server-side verification (OAuth spec)
14183
- ...redirectUri && { redirect_uri: redirectUri }
15196
+ ...redirectUri && { redirect_uri: redirectUri },
15197
+ // ChatGPT sends resource; backend should bind issued connector
15198
+ // tokens to that MCP resource/audience.
15199
+ ...resource && { resource: resource.href }
14184
15200
  }),
14185
15201
  signal: controller.signal
14186
15202
  });
@@ -14203,13 +15219,55 @@ function createOAuthProvider(options) {
14203
15219
  return {
14204
15220
  access_token: data.access_token,
14205
15221
  token_type: "bearer",
14206
- expires_in: data.expires_in ?? 7776e3,
14207
- // 90 days default
14208
- scope: data.scopes?.join(" ")
15222
+ expires_in: data.expires_in ?? (data.access_token.startsWith("snk_") ? 7776e3 : 3600),
15223
+ scope: data.scopes?.join(" "),
15224
+ ...data.refresh_token ? { refresh_token: data.refresh_token } : {}
14209
15225
  };
14210
15226
  },
14211
- async exchangeRefreshToken(_client, _refreshToken, _scopes) {
14212
- throw new Error("Refresh tokens are not supported. Generate a new API key.");
15227
+ async exchangeRefreshToken(client3, refreshToken, scopes, resource) {
15228
+ const controller = new AbortController();
15229
+ const timer = setTimeout(() => controller.abort(), 1e4);
15230
+ try {
15231
+ const response = await fetch(
15232
+ `${supabaseUrl}/functions/v1/mcp-auth?action=refresh-connector-token`,
15233
+ {
15234
+ method: "POST",
15235
+ headers: {
15236
+ Authorization: `Bearer ${supabaseAnonKey}`,
15237
+ "Content-Type": "application/json"
15238
+ },
15239
+ body: JSON.stringify({
15240
+ client_id: client3.client_id,
15241
+ refresh_token: refreshToken,
15242
+ ...scopes && scopes.length > 0 ? { scopes } : {},
15243
+ ...resource ? { resource: resource.href } : {}
15244
+ }),
15245
+ signal: controller.signal
15246
+ }
15247
+ );
15248
+ if (!response.ok) {
15249
+ const err = await response.json().catch(() => ({ error: "Refresh failed" }));
15250
+ throw new Error(err.error ?? `HTTP ${response.status}`);
15251
+ }
15252
+ const data = await response.json();
15253
+ if (!data.access_token) {
15254
+ throw new Error(data.error ?? "No access token returned from refresh");
15255
+ }
15256
+ return {
15257
+ access_token: data.access_token,
15258
+ token_type: "bearer",
15259
+ expires_in: data.expires_in ?? 3600,
15260
+ scope: data.scopes?.join(" "),
15261
+ ...data.refresh_token ? { refresh_token: data.refresh_token } : {}
15262
+ };
15263
+ } catch (err) {
15264
+ if (err instanceof Error && err.name === "AbortError") {
15265
+ throw new Error("Refresh token exchange timed out");
15266
+ }
15267
+ throw err;
15268
+ } finally {
15269
+ clearTimeout(timer);
15270
+ }
14213
15271
  },
14214
15272
  async verifyAccessToken(token) {
14215
15273
  return tokenVerifier2.verifyAccessToken(token);
@@ -14219,15 +15277,23 @@ function createOAuthProvider(options) {
14219
15277
  const controller = new AbortController();
14220
15278
  const timer = setTimeout(() => controller.abort(), 1e4);
14221
15279
  try {
14222
- await fetch(`${supabaseUrl}/functions/v1/mcp-auth?action=revoke-by-token`, {
15280
+ const action = request.token.startsWith("sno_") || request.token_type_hint === "refresh_token" ? "revoke-connector-token" : "revoke-by-token";
15281
+ const response = await fetch(`${supabaseUrl}/functions/v1/mcp-auth?action=${action}`, {
14223
15282
  method: "POST",
14224
15283
  headers: {
14225
15284
  Authorization: `Bearer ${supabaseAnonKey}`,
14226
15285
  "Content-Type": "application/json"
14227
15286
  },
14228
- body: JSON.stringify({ token: request.token }),
15287
+ body: JSON.stringify({
15288
+ token: request.token,
15289
+ token_type_hint: request.token_type_hint,
15290
+ client_id: _client.client_id
15291
+ }),
14229
15292
  signal: controller.signal
14230
15293
  });
15294
+ if (!response.ok) {
15295
+ throw new Error(`Token revocation failed: HTTP ${response.status}`);
15296
+ }
14231
15297
  } catch (err) {
14232
15298
  const msg = err instanceof Error ? err.message : "unknown";
14233
15299
  console.error(`[oauth] Token revocation call failed: ${msg}`);
@@ -14241,27 +15307,120 @@ function createOAuthProvider(options) {
14241
15307
  // src/http.ts
14242
15308
  init_posthog();
14243
15309
 
14244
- // src/lib/www-authenticate.ts
14245
- function buildWwwAuthenticateHeader(opts) {
14246
- const params = ['realm="socialneuron"'];
14247
- if (opts.error) {
14248
- params.push(`error="${opts.error.replace(/[\\"]/g, "_")}"`);
15310
+ // src/lib/discovery-catalog.ts
15311
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15312
+ var cached = null;
15313
+ function buildDiscoveryCatalog() {
15314
+ if (!cached) cached = computeDiscoveryCatalog();
15315
+ return cached;
15316
+ }
15317
+ async function computeDiscoveryCatalog() {
15318
+ const schemaByName = /* @__PURE__ */ new Map();
15319
+ try {
15320
+ const probe = new McpServer({ name: "discovery-probe", version: MCP_VERSION });
15321
+ registerAllTools(probe, { skipScreenshots: true });
15322
+ const handlers = probe.server._requestHandlers;
15323
+ const listHandler = handlers.get("tools/list");
15324
+ if (listHandler) {
15325
+ const out = await listHandler({ method: "tools/list", params: {} }, {});
15326
+ for (const t of out.tools) {
15327
+ const props = t.inputSchema?.properties;
15328
+ if (props && Object.keys(props).length > 0) {
15329
+ schemaByName.set(t.name, {
15330
+ type: "object",
15331
+ properties: props,
15332
+ ...t.inputSchema?.required ? { required: t.inputSchema.required } : {}
15333
+ });
15334
+ }
15335
+ }
15336
+ }
15337
+ } catch (err) {
15338
+ console.error(
15339
+ "[mcp] discovery schema build failed; serving names-only catalog:",
15340
+ err?.message
15341
+ );
14249
15342
  }
14250
- if (opts.errorDescription) {
14251
- params.push(`error_description="${opts.errorDescription.replace(/[\\"]/g, "_")}"`);
15343
+ return TOOL_CATALOG.filter((t) => !t.localOnly && !t.internal).map((t) => ({
15344
+ name: t.name,
15345
+ description: t.description,
15346
+ inputSchema: schemaByName.get(t.name) ?? { type: "object", properties: {} }
15347
+ }));
15348
+ }
15349
+
15350
+ // src/lib/origin-policy.ts
15351
+ var PRODUCTION_FALLBACK_ORIGINS = [
15352
+ "https://socialneuron.com",
15353
+ "https://www.socialneuron.com",
15354
+ "https://app.socialneuron.com"
15355
+ ];
15356
+ var DEVELOPMENT_FALLBACK_ORIGINS = [
15357
+ "http://localhost:3000",
15358
+ "http://localhost:3001",
15359
+ "http://localhost:5173",
15360
+ "http://localhost:8080",
15361
+ "http://127.0.0.1:3000",
15362
+ "http://127.0.0.1:3001",
15363
+ "http://127.0.0.1:5173",
15364
+ "http://127.0.0.1:8080"
15365
+ ];
15366
+ function normalizeOrigin(value) {
15367
+ const trimmed = value.trim();
15368
+ if (!trimmed || trimmed === "*" || trimmed.toLowerCase() === "null") return null;
15369
+ try {
15370
+ const url = new URL(trimmed);
15371
+ if (url.protocol !== "https:" && url.protocol !== "http:") return null;
15372
+ if (!url.hostname) return null;
15373
+ return url.origin;
15374
+ } catch {
15375
+ return null;
14252
15376
  }
14253
- const issuer = opts.issuerUrl.replace(/\/+$/, "");
14254
- params.push(`resource_metadata="${issuer}/.well-known/oauth-protected-resource"`);
14255
- return `Bearer ${params.join(", ")}`;
15377
+ }
15378
+ function parseAllowedOrigins(raw) {
15379
+ const origins = /* @__PURE__ */ new Set();
15380
+ for (const entry of (raw ?? "").split(",")) {
15381
+ const normalized = normalizeOrigin(entry);
15382
+ if (normalized) origins.add(normalized);
15383
+ }
15384
+ return origins;
15385
+ }
15386
+ function buildOriginPolicy(input) {
15387
+ const envOrigins = parseAllowedOrigins(input.allowedOriginsEnv);
15388
+ if (envOrigins.size > 0) {
15389
+ return { allowedOrigins: envOrigins, source: "env" };
15390
+ }
15391
+ const fallback = new Set(PRODUCTION_FALLBACK_ORIGINS);
15392
+ for (const configuredUrl of input.configuredUrls ?? []) {
15393
+ const normalized = normalizeOrigin(configuredUrl);
15394
+ if (normalized) fallback.add(normalized);
15395
+ }
15396
+ if (input.nodeEnv !== "production") {
15397
+ for (const origin of DEVELOPMENT_FALLBACK_ORIGINS) fallback.add(origin);
15398
+ }
15399
+ return { allowedOrigins: fallback, source: "fallback" };
15400
+ }
15401
+ function validateBrowserOrigin(originHeader, policy) {
15402
+ if (originHeader === void 0) return { allowed: true, origin: null };
15403
+ if (Array.isArray(originHeader)) return { allowed: false, reason: "invalid_origin" };
15404
+ const normalized = normalizeOrigin(originHeader);
15405
+ if (!normalized || !policy.allowedOrigins.has(normalized)) {
15406
+ return { allowed: false, reason: "invalid_origin" };
15407
+ }
15408
+ return { allowed: true, origin: normalized };
14256
15409
  }
14257
15410
 
14258
15411
  // src/http.ts
15412
+ process.env.MCP_TRANSPORT = "http";
14259
15413
  var PORT = parseInt(process.env.PORT ?? "8080", 10);
14260
15414
  var SUPABASE_URL2 = process.env.SUPABASE_URL ?? "";
14261
15415
  var SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY ?? "";
14262
15416
  var MCP_SERVER_URL = process.env.MCP_SERVER_URL ?? `http://localhost:${PORT}/mcp`;
14263
15417
  var APP_BASE_URL = process.env.APP_BASE_URL ?? "https://www.socialneuron.com";
14264
15418
  var NODE_ENV = process.env.NODE_ENV ?? "development";
15419
+ var ORIGIN_POLICY = buildOriginPolicy({
15420
+ allowedOriginsEnv: process.env.ALLOWED_ORIGINS,
15421
+ configuredUrls: [APP_BASE_URL, MCP_SERVER_URL],
15422
+ nodeEnv: NODE_ENV
15423
+ });
14265
15424
  function deriveOAuthIssuerUrl() {
14266
15425
  if (process.env.OAUTH_ISSUER_URL) {
14267
15426
  return process.env.OAUTH_ISSUER_URL;
@@ -14335,7 +15494,9 @@ var cleanupInterval = setInterval(
14335
15494
  );
14336
15495
  var app = express();
14337
15496
  app.disable("x-powered-by");
14338
- app.use(express.json());
15497
+ var defaultJsonParser = express.json({ limit: "100kb" });
15498
+ var authenticatedMcpJsonParser = express.json({ limit: "16mb" });
15499
+ var unauthenticatedMcpJsonParser = express.json({ limit: "100kb" });
14339
15500
  app.set("trust proxy", 1);
14340
15501
  var ipBuckets = /* @__PURE__ */ new Map();
14341
15502
  var IP_RATE_MAX = 60;
@@ -14378,20 +15539,35 @@ app.use((_req, res, next) => {
14378
15539
  res.setHeader("X-Content-Type-Options", "nosniff");
14379
15540
  next();
14380
15541
  });
14381
- app.use((_req, res, next) => {
14382
- res.setHeader("Access-Control-Allow-Origin", "*");
15542
+ app.use((req, res, next) => {
15543
+ const originCheck = validateBrowserOrigin(req.headers.origin, ORIGIN_POLICY);
15544
+ if (!originCheck.allowed) {
15545
+ res.status(403).json({
15546
+ error: "invalid_origin",
15547
+ error_description: "Request Origin is not allowed."
15548
+ });
15549
+ return;
15550
+ }
15551
+ if (originCheck.origin) {
15552
+ res.setHeader("Access-Control-Allow-Origin", originCheck.origin);
15553
+ res.setHeader("Vary", "Origin");
15554
+ }
14383
15555
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
14384
15556
  res.setHeader(
14385
15557
  "Access-Control-Allow-Headers",
14386
15558
  "Content-Type, Authorization, Mcp-Session-Id, MCP-Protocol-Version"
14387
15559
  );
14388
15560
  res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id, WWW-Authenticate");
14389
- if (_req.method === "OPTIONS") {
15561
+ if (req.method === "OPTIONS") {
14390
15562
  res.status(204).end();
14391
15563
  return;
14392
15564
  }
14393
15565
  next();
14394
15566
  });
15567
+ app.use((req, res, next) => {
15568
+ if (req.path === "/mcp") return next();
15569
+ defaultJsonParser(req, res, next);
15570
+ });
14395
15571
  var oauthProvider = createOAuthProvider({
14396
15572
  supabaseUrl: SUPABASE_URL2,
14397
15573
  supabaseAnonKey: SUPABASE_ANON_KEY,
@@ -14429,6 +15605,26 @@ var authRouter = mcpAuthRouter({
14429
15605
  serviceDocumentationUrl: new URL("https://socialneuron.com/for-developers"),
14430
15606
  scopesSupported: SCOPES_SUPPORTED
14431
15607
  });
15608
+ function normalizeOAuthResourceParam(value) {
15609
+ const values = Array.isArray(value) ? value : [value];
15610
+ const strings = values.filter(
15611
+ (item) => typeof item === "string" && item.length > 0
15612
+ );
15613
+ const issuer = OAUTH_ISSUER_URL.replace(/\/$/, "");
15614
+ return strings.find((item) => item.replace(/\/$/, "") === issuer) ?? strings[0];
15615
+ }
15616
+ app.use((req, _res, next) => {
15617
+ if ((req.path === "/authorize" || req.path === "/token") && Array.isArray(req.query.resource)) {
15618
+ const normalized = normalizeOAuthResourceParam(req.query.resource);
15619
+ if (normalized) {
15620
+ const normalizedUrl = new URL(req.originalUrl, OAUTH_ISSUER_URL);
15621
+ normalizedUrl.searchParams.delete("resource");
15622
+ normalizedUrl.searchParams.set("resource", normalized);
15623
+ req.url = `${normalizedUrl.pathname}${normalizedUrl.search}`;
15624
+ }
15625
+ }
15626
+ next();
15627
+ });
14432
15628
  app.use((req, res, next) => {
14433
15629
  authRouter(req, res, (err) => {
14434
15630
  if (err) {
@@ -14477,6 +15673,16 @@ async function authenticateRequest(req, res, next) {
14477
15673
  });
14478
15674
  }
14479
15675
  }
15676
+ app.use("/mcp", (req, res, next) => {
15677
+ if (req.method !== "POST") return next();
15678
+ if (!req.headers.authorization?.startsWith("Bearer ")) return next();
15679
+ authenticateRequest(req, res, next);
15680
+ });
15681
+ app.use("/mcp", (req, res, next) => {
15682
+ if (req.method !== "POST") return next();
15683
+ const parser = req.auth ? authenticatedMcpJsonParser : unauthenticatedMcpJsonParser;
15684
+ parser(req, res, next);
15685
+ });
14480
15686
  app.get("/.well-known/mcp/server-card.json", (_req, res) => {
14481
15687
  res.json({
14482
15688
  serverInfo: {
@@ -14487,8 +15693,8 @@ app.get("/.well-known/mcp/server-card.json", (_req, res) => {
14487
15693
  required: true,
14488
15694
  schemes: ["oauth2"]
14489
15695
  },
14490
- toolCount: TOOL_CATALOG.length,
14491
- tools: TOOL_CATALOG.map((t) => ({
15696
+ toolCount: TOOL_CATALOG.filter((t) => !t.localOnly && !t.internal).length,
15697
+ tools: TOOL_CATALOG.filter((t) => !t.localOnly && !t.internal).map((t) => ({
14492
15698
  name: t.name,
14493
15699
  description: t.description,
14494
15700
  module: t.module,
@@ -14566,19 +15772,32 @@ app.post("/mcp", (req, res, next) => {
14566
15772
  const body = req.body;
14567
15773
  if (body?.jsonrpc !== "2.0") return next();
14568
15774
  if (body.method === "tools/list") {
14569
- res.json({
14570
- jsonrpc: "2.0",
14571
- id: body.id ?? null,
14572
- result: {
14573
- tools: TOOL_CATALOG.map((t) => ({
14574
- name: t.name,
14575
- description: t.description,
14576
- inputSchema: { type: "object", properties: {} }
14577
- }))
14578
- }
15775
+ const hasBearer = req.headers.authorization?.startsWith("Bearer ");
15776
+ if (hasBearer) {
15777
+ next();
15778
+ return;
15779
+ }
15780
+ buildDiscoveryCatalog().then((tools) => {
15781
+ res.json({ jsonrpc: "2.0", id: body.id ?? null, result: { tools } });
15782
+ }).catch(() => {
15783
+ res.json({
15784
+ jsonrpc: "2.0",
15785
+ id: body.id ?? null,
15786
+ result: {
15787
+ tools: TOOL_CATALOG.map((t) => ({
15788
+ name: t.name,
15789
+ description: t.description,
15790
+ inputSchema: { type: "object", properties: {} }
15791
+ }))
15792
+ }
15793
+ });
14579
15794
  });
14580
15795
  return;
14581
15796
  }
15797
+ if (req.auth) {
15798
+ next();
15799
+ return;
15800
+ }
14582
15801
  authenticateRequest(req, res, next);
14583
15802
  });
14584
15803
  app.post("/mcp", async (req, res) => {
@@ -14631,7 +15850,7 @@ app.post("/mcp", async (req, res) => {
14631
15850
  });
14632
15851
  return;
14633
15852
  }
14634
- const server = new McpServer({
15853
+ const server = new McpServer2({
14635
15854
  name: "socialneuron",
14636
15855
  version: MCP_VERSION
14637
15856
  });
@@ -14717,9 +15936,17 @@ app.delete("/mcp", authenticateRequest, async (req, res) => {
14717
15936
  });
14718
15937
  app.use((err, _req, res, _next) => {
14719
15938
  console.error("[MCP HTTP] Unhandled Express error:", err.stack || err.message || err);
14720
- if (!res.headersSent) {
14721
- res.status(500).json({ error: "internal_error", error_description: sanitizeError(err) });
15939
+ if (res.headersSent) return;
15940
+ const e = err;
15941
+ const status = e.status ?? e.statusCode;
15942
+ if (status === 413 || e.type === "entity.too.large") {
15943
+ res.status(413).json({
15944
+ error: "payload_too_large",
15945
+ error_description: "Request body exceeds the allowed JSON limit."
15946
+ });
15947
+ return;
14722
15948
  }
15949
+ res.status(500).json({ error: "internal_error", error_description: sanitizeError(err) });
14723
15950
  });
14724
15951
  var httpServer = app.listen(PORT, "0.0.0.0", () => {
14725
15952
  console.log(`[MCP HTTP] Social Neuron MCP Server listening on 0.0.0.0:${PORT}`);