@socialneuron/mcp-server 1.7.12 → 1.7.14

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 — PR #4.4)
711
+ list_skills: "mcp:read",
712
+ run_skill: "mcp:write",
713
+ // mcp:read (Loop observability — growth-loop KPIs + bandit posteriors)
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.14";
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
  {
@@ -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",
@@ -9690,6 +9759,32 @@ var TOOL_CATALOG = [
9690
9759
  description: "List currently-running campaigns with thesis, budget, hero format, and current spend. Used by Hermes pitch skill to bias drafts.",
9691
9760
  module: "hermes",
9692
9761
  scope: "mcp:read"
9762
+ },
9763
+ // skills (workflow skills — multi-step brand-locked content pipelines)
9764
+ {
9765
+ name: "list_skills",
9766
+ 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.).",
9767
+ module: "skills",
9768
+ scope: "mcp:read"
9769
+ },
9770
+ {
9771
+ name: "run_skill",
9772
+ description: "Run a Social Neuron workflow skill end-to-end (brand-locked content production). PR #4.4 v1 returns a structured run preview with the step plan, credit cost, and a deep-link to launch in the SN dashboard.",
9773
+ module: "skills",
9774
+ scope: "mcp:write"
9775
+ },
9776
+ // loop observability (growth-loop KPIs + Thompson Sampling bandit posteriors)
9777
+ {
9778
+ name: "get_loop_pulse",
9779
+ description: "Read dynamic loop-health KPIs for the growth loop over the last 7 days (reflection/decision coverage, visual gate pass rate, bandit-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.",
9780
+ module: "loop",
9781
+ scope: "mcp:read"
9782
+ },
9783
+ {
9784
+ name: "get_bandit_state",
9785
+ description: "Read the current Thompson Sampling bandit posteriors for a project \u2014 top-K arms per (arm_type, platform) with Beta(alpha,beta) posterior mean and uncertainty. Use to reason about which hook family / format / timing slot the bandit currently prefers per platform.",
9786
+ module: "loop",
9787
+ scope: "mcp:read"
9693
9788
  }
9694
9789
  ];
9695
9790
  function getToolsByModule(module) {
@@ -9706,7 +9801,191 @@ function searchTools(query) {
9706
9801
  }
9707
9802
 
9708
9803
  // src/tools/discovery.ts
9804
+ init_supabase();
9805
+ init_request_context();
9806
+ var KNOWLEDGE_BASE_URL = "https://socialneuron.com/for-developers";
9807
+ var KNOWLEDGE_SEARCH_LIMIT = 10;
9808
+ var SearchOutputSchema = {
9809
+ results: z23.array(
9810
+ z23.object({
9811
+ id: z23.string(),
9812
+ title: z23.string(),
9813
+ url: z23.string().url()
9814
+ })
9815
+ )
9816
+ };
9817
+ var FetchOutputSchema = {
9818
+ id: z23.string(),
9819
+ title: z23.string(),
9820
+ text: z23.string(),
9821
+ url: z23.string().url(),
9822
+ metadata: z23.record(z23.string(), z23.string()).optional()
9823
+ };
9824
+ var STATIC_KNOWLEDGE_DOCUMENTS = [
9825
+ {
9826
+ id: "overview",
9827
+ title: "Social Neuron MCP Overview",
9828
+ url: `${KNOWLEDGE_BASE_URL}#mcp`,
9829
+ text: [
9830
+ "Social Neuron exposes an MCP server for creating, scheduling, and optimizing social content.",
9831
+ "Use the hosted streamable HTTP endpoint at https://mcp.socialneuron.com/mcp for ChatGPT, Claude, and other remote MCP clients.",
9832
+ "The npm package provides stdio transport for local tools and Codex-style workflows."
9833
+ ].join("\n"),
9834
+ metadata: { source: "public-developer-docs", category: "overview" }
9835
+ },
9836
+ {
9837
+ id: "integrations",
9838
+ title: "Supported Social Integrations",
9839
+ url: "https://socialneuron.com/integrations",
9840
+ text: [
9841
+ "Social Neuron tracks platform availability on the integrations page.",
9842
+ "YouTube, TikTok, Instagram, LinkedIn, X, and Facebook are live for supported posting workflows.",
9843
+ "Threads and Bluesky are supported surfaces where live availability depends on the current integration status."
9844
+ ].join("\n"),
9845
+ metadata: { source: "public-integrations-page", category: "integrations" }
9846
+ },
9847
+ {
9848
+ id: "chatgpt-connector",
9849
+ title: "ChatGPT Connector Setup",
9850
+ url: `${KNOWLEDGE_BASE_URL}#chatgpt`,
9851
+ text: [
9852
+ "In ChatGPT Developer Mode, create an MCP app using https://mcp.socialneuron.com/mcp as the MCP URL.",
9853
+ "The connector uses OAuth for account linking and tool scopes for read, write, distribution, analytics, comments, and autopilot access.",
9854
+ "Publishing tools should be treated as externally visible actions and require the distribute scope."
9855
+ ].join("\n"),
9856
+ metadata: { source: "public-developer-docs", category: "chatgpt" }
9857
+ },
9858
+ {
9859
+ id: "privacy-security",
9860
+ title: "Connector Security and Data Minimization",
9861
+ url: `${KNOWLEDGE_BASE_URL}#security`,
9862
+ text: [
9863
+ "Social Neuron MCP tools enforce OAuth or API-key scopes before tool execution.",
9864
+ "Read-only discovery tools expose public product and tool metadata, not private account content.",
9865
+ "User-owned content and analytics require authenticated scopes and organization or project membership checks in backend functions."
9866
+ ].join("\n"),
9867
+ metadata: { source: "public-developer-docs", category: "security" }
9868
+ }
9869
+ ];
9870
+ function toolKnowledgeDocument(tool) {
9871
+ const lines = [
9872
+ `Tool: ${tool.name}`,
9873
+ `Description: ${tool.description}`,
9874
+ `Module: ${tool.module}`,
9875
+ `Required scope: ${tool.scope}`
9876
+ ];
9877
+ if (tool.task_intent) lines.push(`Task intent: ${tool.task_intent}`);
9878
+ if (tool.use_when) lines.push(`Use when: ${tool.use_when}`);
9879
+ if (tool.avoid_when) lines.push(`Avoid when: ${tool.avoid_when}`);
9880
+ if (tool.next_tools?.length) lines.push(`Common next tools: ${tool.next_tools.join(", ")}`);
9881
+ return {
9882
+ id: `tool:${tool.name}`,
9883
+ title: `MCP tool: ${tool.name}`,
9884
+ url: `${KNOWLEDGE_BASE_URL}#tool-${tool.name}`,
9885
+ text: lines.join("\n"),
9886
+ metadata: {
9887
+ source: "mcp-tool-catalog",
9888
+ category: "tool",
9889
+ module: tool.module,
9890
+ scope: tool.scope
9891
+ }
9892
+ };
9893
+ }
9894
+ function getKnowledgeDocuments() {
9895
+ return [...STATIC_KNOWLEDGE_DOCUMENTS, ...TOOL_CATALOG.map(toolKnowledgeDocument)];
9896
+ }
9897
+ function tokenize(input) {
9898
+ return input.toLowerCase().split(/[^a-z0-9:_-]+/).map((token) => token.trim()).filter(Boolean);
9899
+ }
9900
+ function scoreDocument(queryTokens, doc) {
9901
+ const title = doc.title.toLowerCase();
9902
+ const text = doc.text.toLowerCase();
9903
+ const metadata = JSON.stringify(doc.metadata ?? {}).toLowerCase();
9904
+ return queryTokens.reduce((score, token) => {
9905
+ if (doc.id.toLowerCase() === token) return score + 12;
9906
+ if (doc.id.toLowerCase().includes(token)) score += 8;
9907
+ if (title.includes(token)) score += 5;
9908
+ if (text.includes(token)) score += 2;
9909
+ if (metadata.includes(token)) score += 1;
9910
+ return score;
9911
+ }, 0);
9912
+ }
9913
+ function searchKnowledge(query) {
9914
+ const docs = getKnowledgeDocuments();
9915
+ const tokens = tokenize(query);
9916
+ if (tokens.length === 0) {
9917
+ return docs.slice(0, KNOWLEDGE_SEARCH_LIMIT);
9918
+ }
9919
+ 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);
9920
+ }
9709
9921
  function registerDiscoveryTools(server) {
9922
+ server.registerTool(
9923
+ "search",
9924
+ {
9925
+ title: "Search Social Neuron Knowledge",
9926
+ 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.",
9927
+ inputSchema: {
9928
+ query: z23.string().describe("Search query.")
9929
+ },
9930
+ outputSchema: SearchOutputSchema,
9931
+ annotations: {
9932
+ readOnlyHint: true,
9933
+ destructiveHint: false,
9934
+ idempotentHint: true,
9935
+ openWorldHint: false
9936
+ }
9937
+ },
9938
+ async ({ query }) => {
9939
+ const structuredContent = {
9940
+ results: searchKnowledge(query).map((doc) => ({
9941
+ id: doc.id,
9942
+ title: doc.title,
9943
+ url: doc.url
9944
+ }))
9945
+ };
9946
+ return {
9947
+ structuredContent,
9948
+ content: [{ type: "text", text: JSON.stringify(structuredContent) }]
9949
+ };
9950
+ }
9951
+ );
9952
+ server.registerTool(
9953
+ "fetch",
9954
+ {
9955
+ title: "Fetch Social Neuron Knowledge",
9956
+ description: "Fetch a public Social Neuron knowledge document by ID. Use IDs returned by the search tool.",
9957
+ inputSchema: {
9958
+ id: z23.string().describe("Document ID returned by search.")
9959
+ },
9960
+ outputSchema: FetchOutputSchema,
9961
+ annotations: {
9962
+ readOnlyHint: true,
9963
+ destructiveHint: false,
9964
+ idempotentHint: true,
9965
+ openWorldHint: false
9966
+ }
9967
+ },
9968
+ async ({ id }) => {
9969
+ const doc = getKnowledgeDocuments().find((candidate) => candidate.id === id);
9970
+ if (!doc) {
9971
+ return {
9972
+ content: [{ type: "text", text: `Document not found: ${id}` }],
9973
+ isError: true
9974
+ };
9975
+ }
9976
+ const structuredContent = {
9977
+ id: doc.id,
9978
+ title: doc.title,
9979
+ text: doc.text,
9980
+ url: doc.url,
9981
+ ...doc.metadata ? { metadata: doc.metadata } : {}
9982
+ };
9983
+ return {
9984
+ structuredContent,
9985
+ content: [{ type: "text", text: JSON.stringify(structuredContent) }]
9986
+ };
9987
+ }
9988
+ );
9710
9989
  server.tool(
9711
9990
  "search_tools",
9712
9991
  '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,9 +9995,15 @@ function registerDiscoveryTools(server) {
9716
9995
  scope: z23.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
9717
9996
  detail: z23.enum(["name", "summary", "full"]).default("summary").describe(
9718
9997
  'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
9998
+ ),
9999
+ available_only: z23.boolean().default(false).describe(
10000
+ "When true, only return tools allowed by the current API key/OAuth scopes. Use this after a permission_denied error."
9719
10001
  )
9720
10002
  },
9721
- async ({ query, module, scope, detail }) => {
10003
+ async ({ query, module, scope, detail, available_only }) => {
10004
+ const currentScopes = getRequestScopes() ?? getAuthenticatedScopes();
10005
+ const hasKnownScopes = currentScopes.length > 0;
10006
+ const isAvailable = (tool) => !hasKnownScopes || hasScope(currentScopes, tool.scope);
9722
10007
  let results = [...TOOL_CATALOG];
9723
10008
  if (query) {
9724
10009
  results = searchTools(query);
@@ -9731,24 +10016,48 @@ function registerDiscoveryTools(server) {
9731
10016
  const scopeTools = getToolsByScope(scope);
9732
10017
  results = results.filter((t) => scopeTools.some((st) => st.name === t.name));
9733
10018
  }
10019
+ if (available_only) {
10020
+ results = results.filter(isAvailable);
10021
+ }
10022
+ const unavailableCount = hasKnownScopes ? results.filter((t) => !isAvailable(t)).length : 0;
9734
10023
  let output;
9735
10024
  switch (detail) {
9736
10025
  case "name":
9737
10026
  output = results.map((t) => t.name);
9738
10027
  break;
9739
10028
  case "summary":
9740
- output = results.map((t) => ({ name: t.name, description: t.description }));
10029
+ output = results.map((t) => ({
10030
+ name: t.name,
10031
+ description: t.description,
10032
+ ...hasKnownScopes || scope ? { required_scope: t.scope } : {},
10033
+ ...hasKnownScopes ? { available: isAvailable(t) } : {},
10034
+ ...t.task_intent ? { task_intent: t.task_intent } : {},
10035
+ ...t.use_when ? { use_when: t.use_when } : {},
10036
+ ...t.avoid_when ? { avoid_when: t.avoid_when } : {}
10037
+ }));
9741
10038
  break;
9742
10039
  case "full":
9743
10040
  default:
9744
- output = results;
10041
+ output = results.map((tool) => ({
10042
+ ...tool,
10043
+ required_scope: tool.scope,
10044
+ ...hasKnownScopes ? { available: isAvailable(tool) } : {}
10045
+ }));
9745
10046
  break;
9746
10047
  }
9747
10048
  return {
9748
10049
  content: [
9749
10050
  {
9750
10051
  type: "text",
9751
- text: JSON.stringify({ toolCount: results.length, tools: output }, null, 2)
10052
+ text: JSON.stringify(
10053
+ {
10054
+ toolCount: results.length,
10055
+ ...hasKnownScopes ? { scopes: { available: currentScopes, unavailable_matches: unavailableCount } } : {},
10056
+ tools: output
10057
+ },
10058
+ null,
10059
+ detail === "full" ? 2 : 0
10060
+ )
9752
10061
  }
9753
10062
  ]
9754
10063
  };
@@ -9760,7 +10069,7 @@ function registerDiscoveryTools(server) {
9760
10069
  import { z as z24 } from "zod";
9761
10070
  import { randomUUID as randomUUID3 } from "node:crypto";
9762
10071
  init_supabase();
9763
- function asEnvelope19(data) {
10072
+ function asEnvelope20(data) {
9764
10073
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
9765
10074
  }
9766
10075
  var PLATFORM_ENUM2 = z24.enum([
@@ -9804,6 +10113,7 @@ function registerPipelineTools(server) {
9804
10113
  throw new Error(readinessError ?? "No response from mcp-data");
9805
10114
  }
9806
10115
  const credits = readiness.credits;
10116
+ const isUnlimited = readiness.is_unlimited === true;
9807
10117
  const connectedPlatforms = readiness.connected_platforms;
9808
10118
  const missingPlatforms = readiness.missing_platforms;
9809
10119
  const hasBrand = readiness.has_brand;
@@ -9812,7 +10122,7 @@ function registerPipelineTools(server) {
9812
10122
  const insightsFresh = readiness.insights_fresh;
9813
10123
  const blockers = [];
9814
10124
  const warnings = [];
9815
- if (credits < estimatedCost) {
10125
+ if (!isUnlimited && credits < estimatedCost) {
9816
10126
  blockers.push(`Insufficient credits: ${credits} available, ~${estimatedCost} needed`);
9817
10127
  }
9818
10128
  if (missingPlatforms.length > 0) {
@@ -9851,7 +10161,7 @@ function registerPipelineTools(server) {
9851
10161
  };
9852
10162
  if (format === "json") {
9853
10163
  return {
9854
- content: [{ type: "text", text: JSON.stringify(asEnvelope19(result), null, 2) }]
10164
+ content: [{ type: "text", text: JSON.stringify(asEnvelope20(result), null, 2) }]
9855
10165
  };
9856
10166
  }
9857
10167
  const lines = [];
@@ -9948,8 +10258,9 @@ function registerPipelineTools(server) {
9948
10258
  { timeoutMs: 1e4 }
9949
10259
  );
9950
10260
  const availableCredits = budgetData?.credits ?? 0;
10261
+ const isUnlimited = budgetData?.is_unlimited === true;
9951
10262
  const creditLimit = max_credits ?? availableCredits;
9952
- if (availableCredits < estimatedCost) {
10263
+ if (!isUnlimited && availableCredits < estimatedCost) {
9953
10264
  return {
9954
10265
  content: [
9955
10266
  {
@@ -10175,21 +10486,27 @@ function registerPipelineTools(server) {
10175
10486
  let postsScheduled = 0;
10176
10487
  if (!dry_run && !skipSet.has("schedule") && postsApproved > 0) {
10177
10488
  const approvedPosts = posts.filter((p) => p.status === "approved");
10489
+ const scheduleBase = /* @__PURE__ */ new Date();
10490
+ scheduleBase.setDate(scheduleBase.getDate() + 1);
10491
+ const scheduleBaseMs = scheduleBase.getTime();
10178
10492
  for (const post of approvedPosts) {
10179
10493
  if (creditsUsed >= creditLimit) {
10180
10494
  errors.push({ stage: "schedule", message: "Credit limit reached" });
10181
10495
  break;
10182
10496
  }
10497
+ const scheduledAt = post.schedule_at ?? new Date(scheduleBaseMs + (Math.max(1, post.day) - 1) * 864e5).toISOString();
10183
10498
  try {
10184
10499
  const { error: schedError } = await callEdgeFunction(
10185
10500
  "schedule-post",
10186
10501
  {
10187
- platform: post.platform,
10502
+ platforms: [post.platform],
10188
10503
  caption: post.caption,
10189
10504
  title: post.title,
10190
10505
  hashtags: post.hashtags,
10191
- media_url: post.media_url,
10192
- scheduled_at: post.schedule_at,
10506
+ mediaUrl: post.media_url,
10507
+ scheduledAt,
10508
+ planId,
10509
+ idempotencyKey: `pipeline-${planId}-${post.id}`,
10193
10510
  ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
10194
10511
  },
10195
10512
  { timeoutMs: 15e3 }
@@ -10255,7 +10572,7 @@ function registerPipelineTools(server) {
10255
10572
  if (response_format === "json") {
10256
10573
  return {
10257
10574
  content: [
10258
- { type: "text", text: JSON.stringify(asEnvelope19(resultPayload), null, 2) }
10575
+ { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
10259
10576
  ]
10260
10577
  };
10261
10578
  }
@@ -10336,7 +10653,7 @@ function registerPipelineTools(server) {
10336
10653
  }
10337
10654
  if (format === "json") {
10338
10655
  return {
10339
- content: [{ type: "text", text: JSON.stringify(asEnvelope19(data), null, 2) }]
10656
+ content: [{ type: "text", text: JSON.stringify(asEnvelope20(data), null, 2) }]
10340
10657
  };
10341
10658
  }
10342
10659
  const lines = [];
@@ -10470,7 +10787,7 @@ function registerPipelineTools(server) {
10470
10787
  if (response_format === "json") {
10471
10788
  return {
10472
10789
  content: [
10473
- { type: "text", text: JSON.stringify(asEnvelope19(resultPayload), null, 2) }
10790
+ { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
10474
10791
  ]
10475
10792
  };
10476
10793
  }
@@ -10518,7 +10835,7 @@ function buildPlanPrompt(topic, platforms, days, postsPerDay, sourceUrl) {
10518
10835
 
10519
10836
  // src/tools/suggest.ts
10520
10837
  import { z as z25 } from "zod";
10521
- function asEnvelope20(data) {
10838
+ function asEnvelope21(data) {
10522
10839
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
10523
10840
  }
10524
10841
  function registerSuggestTools(server) {
@@ -10626,7 +10943,7 @@ function registerSuggestTools(server) {
10626
10943
  if (format === "json") {
10627
10944
  return {
10628
10945
  content: [
10629
- { type: "text", text: JSON.stringify(asEnvelope20(resultPayload), null, 2) }
10946
+ { type: "text", text: JSON.stringify(asEnvelope21(resultPayload), null, 2) }
10630
10947
  ]
10631
10948
  };
10632
10949
  }
@@ -10761,7 +11078,7 @@ function detectAnomalies(currentData, previousData, sensitivity = "medium", aver
10761
11078
  }
10762
11079
 
10763
11080
  // src/tools/digest.ts
10764
- function asEnvelope21(data) {
11081
+ function asEnvelope22(data) {
10765
11082
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
10766
11083
  }
10767
11084
  var PLATFORM_ENUM3 = z26.enum([
@@ -10930,7 +11247,7 @@ function registerDigestTools(server) {
10930
11247
  };
10931
11248
  if (format === "json") {
10932
11249
  return {
10933
- content: [{ type: "text", text: JSON.stringify(asEnvelope21(digest), null, 2) }]
11250
+ content: [{ type: "text", text: JSON.stringify(asEnvelope22(digest), null, 2) }]
10934
11251
  };
10935
11252
  }
10936
11253
  const lines = [];
@@ -11029,7 +11346,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
11029
11346
  if (format === "json") {
11030
11347
  return {
11031
11348
  content: [
11032
- { type: "text", text: JSON.stringify(asEnvelope21(resultPayload), null, 2) }
11349
+ { type: "text", text: JSON.stringify(asEnvelope22(resultPayload), null, 2) }
11033
11350
  ]
11034
11351
  };
11035
11352
  }
@@ -11076,19 +11393,130 @@ var WEIGHTS = {
11076
11393
  structuralPatterns: 0.05
11077
11394
  };
11078
11395
  function norm(content) {
11079
- return content.toLowerCase().replace(/[^a-z0-9\s]/g, " ");
11396
+ return content.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
11080
11397
  }
11081
11398
  function findMatches(content, terms) {
11082
11399
  const n = norm(content);
11083
- return terms.filter((t) => n.includes(t.toLowerCase()));
11400
+ return terms.filter((t) => {
11401
+ const nt = norm(t);
11402
+ return nt.length > 0 && n.includes(nt);
11403
+ });
11084
11404
  }
11085
11405
  function findMissing(content, terms) {
11086
11406
  const n = norm(content);
11087
- return terms.filter((t) => !n.includes(t.toLowerCase()));
11407
+ return terms.filter((t) => {
11408
+ const nt = norm(t);
11409
+ return nt.length > 0 && !n.includes(nt);
11410
+ });
11088
11411
  }
11089
- var FABRICATION_PATTERNS = [
11090
- { regex: /\b\d+[,.]?\d*\s*(%|percent)/gi, label: "unverified percentage" },
11091
- { regex: /\b(award[- ]?winning|best[- ]selling|#\s*1)\b/gi, label: "unverified ranking" },
11412
+ function asStringArray(v) {
11413
+ if (Array.isArray(v)) return v.map((x) => String(x ?? "").trim()).filter(Boolean);
11414
+ if (typeof v === "string") {
11415
+ return v.split(/[,;\n]/).map((s) => s.trim()).filter(Boolean);
11416
+ }
11417
+ return [];
11418
+ }
11419
+ function extractAtomicTerms(entries) {
11420
+ const out = [];
11421
+ for (const raw of entries) {
11422
+ const e = (raw || "").trim();
11423
+ if (!e) continue;
11424
+ const quoted = e.match(/['"“”‘’]([^'"“”‘’]{2,40})['"“”‘’]/g);
11425
+ if (quoted) {
11426
+ for (const q of quoted) out.push(q.replace(/['"“”‘’]/g, "").trim());
11427
+ continue;
11428
+ }
11429
+ const words = e.split(/\s+/);
11430
+ const isGuidancePhrase = words.length > 2 && /\b(tone|voice|style|approach|language)\b/i.test(e);
11431
+ if (!e.includes("(") && words.length <= 3 && !isGuidancePhrase) out.push(e);
11432
+ }
11433
+ return Array.from(new Set(out.map((t) => t.trim()).filter(Boolean)));
11434
+ }
11435
+ function hasField(obj, key) {
11436
+ return Object.prototype.hasOwnProperty.call(obj, key) && obj[key] != null;
11437
+ }
11438
+ function unionTerms(existing, incoming) {
11439
+ const out = [];
11440
+ const seen = /* @__PURE__ */ new Set();
11441
+ for (const t of [...existing, ...incoming]) {
11442
+ if (t && !seen.has(t)) {
11443
+ seen.add(t);
11444
+ out.push(t);
11445
+ }
11446
+ }
11447
+ return out;
11448
+ }
11449
+ function mergeVoiceVocabulary(resolved, raw) {
11450
+ const voice = raw.voice;
11451
+ if (!voice || typeof voice !== "object") return resolved;
11452
+ const vp = asStringArray(voice.preferredTerms);
11453
+ const vb = asStringArray(voice.bannedTerms);
11454
+ if (!vp.length && !vb.length) return resolved;
11455
+ const existing = resolved.vocabularyRules ?? {};
11456
+ return {
11457
+ ...resolved,
11458
+ vocabularyRules: {
11459
+ preferredTerms: unionTerms(asStringArray(existing.preferredTerms), vp),
11460
+ bannedTerms: unionTerms(asStringArray(existing.bannedTerms), vb)
11461
+ }
11462
+ };
11463
+ }
11464
+ function resolveBrandProfile(raw) {
11465
+ if (!raw || typeof raw !== "object") return {};
11466
+ const r = raw;
11467
+ const name = typeof r.name === "string" ? r.name : void 0;
11468
+ const voiceProfile = r.voiceProfile;
11469
+ const vocabularyRules = r.vocabularyRules;
11470
+ if (voiceProfile && vocabularyRules) {
11471
+ return mergeVoiceVocabulary(raw, r);
11472
+ }
11473
+ if (voiceProfile) {
11474
+ return mergeVoiceVocabulary(
11475
+ {
11476
+ name,
11477
+ voiceProfile,
11478
+ vocabularyRules: {
11479
+ bannedTerms: extractAtomicTerms(asStringArray(voiceProfile.avoidPatterns)),
11480
+ preferredTerms: asStringArray(voiceProfile.languagePatterns)
11481
+ },
11482
+ targetAudience: r.targetAudience,
11483
+ writingStyleRules: r.writingStyleRules
11484
+ },
11485
+ r
11486
+ );
11487
+ }
11488
+ const isFlat = hasField(r, "voiceTone") || hasField(r, "voiceTags") || hasField(r, "discouragedTerms") || hasField(r, "preferredTerms");
11489
+ if (isFlat) {
11490
+ const ta = r.targetAudience;
11491
+ const psy = ta?.psychographics;
11492
+ const painPoints = ta ? asStringArray(psy?.painPoints ?? ta.painPoints) : [];
11493
+ const interests = ta ? asStringArray(psy?.interests ?? ta.interests) : [];
11494
+ return mergeVoiceVocabulary(
11495
+ {
11496
+ name,
11497
+ voiceProfile: {
11498
+ tone: [...asStringArray(r.voiceTags), ...asStringArray(r.voiceTone)],
11499
+ style: asStringArray(r.voiceStyle),
11500
+ languagePatterns: [],
11501
+ avoidPatterns: []
11502
+ },
11503
+ vocabularyRules: {
11504
+ bannedTerms: asStringArray(r.discouragedTerms),
11505
+ preferredTerms: asStringArray(r.preferredTerms)
11506
+ },
11507
+ targetAudience: painPoints.length || interests.length ? { psychographics: { painPoints, interests } } : void 0
11508
+ },
11509
+ r
11510
+ );
11511
+ }
11512
+ const stub = { name };
11513
+ if (vocabularyRules)
11514
+ stub.vocabularyRules = vocabularyRules;
11515
+ return mergeVoiceVocabulary(stub, r);
11516
+ }
11517
+ var FABRICATION_PATTERNS = [
11518
+ { regex: /\b\d+(?:[,.]\d+)?\s*(?:%|percent\b)/gi, label: "unverified percentage" },
11519
+ { regex: /\b(award[- ]?winning|best[- ]selling|#\s*1)\b/gi, label: "unverified ranking" },
11092
11520
  {
11093
11521
  regex: /\b(guaranteed|proven to|studies show|scientifically proven)\b/gi,
11094
11522
  label: "unverified claim"
@@ -11303,24 +11731,25 @@ function computeBrandConsistency(content, profile, threshold = 60) {
11303
11731
  fabricationWarnings: []
11304
11732
  };
11305
11733
  }
11734
+ const resolved = resolveBrandProfile(profile);
11306
11735
  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)
11736
+ toneAlignment: scoreTone(content, resolved),
11737
+ vocabularyAdherence: scoreVocab(content, resolved),
11738
+ avoidCompliance: scoreAvoid(content, resolved),
11739
+ audienceRelevance: scoreAudience(content, resolved),
11740
+ brandMentions: scoreBrand(content, resolved),
11741
+ structuralPatterns: scoreStructure(content, resolved)
11313
11742
  };
11314
11743
  const overall = Math.round(
11315
11744
  Object.values(dimensions).reduce((sum, d) => sum + d.score * d.weight, 0)
11316
11745
  );
11317
11746
  const preferred = [
11318
- ...profile.voiceProfile?.languagePatterns || [],
11319
- ...profile.vocabularyRules?.preferredTerms || []
11747
+ ...resolved.voiceProfile?.languagePatterns || [],
11748
+ ...resolved.vocabularyRules?.preferredTerms || []
11320
11749
  ];
11321
11750
  const banned = [
11322
- ...profile.voiceProfile?.avoidPatterns || [],
11323
- ...profile.vocabularyRules?.bannedTerms || []
11751
+ ...resolved.voiceProfile?.avoidPatterns || [],
11752
+ ...resolved.vocabularyRules?.bannedTerms || []
11324
11753
  ];
11325
11754
  const fabrications = detectFabricationPatterns(content);
11326
11755
  return {
@@ -11351,11 +11780,11 @@ function hexToLab(hex) {
11351
11780
  const lb = srgbToLinear(b);
11352
11781
  const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
11353
11782
  const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
11354
- const z36 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
11783
+ const z39 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
11355
11784
  const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
11356
11785
  const fx = f(x / 0.95047);
11357
11786
  const fy = f(y / 1);
11358
- const fz = f(z36 / 1.08883);
11787
+ const fz = f(z39 / 1.08883);
11359
11788
  return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
11360
11789
  }
11361
11790
  function deltaE2000(lab1, lab2) {
@@ -11507,7 +11936,7 @@ function exportFigma(palette, typography) {
11507
11936
  }
11508
11937
 
11509
11938
  // src/tools/brandRuntime.ts
11510
- function asEnvelope22(data) {
11939
+ function asEnvelope23(data) {
11511
11940
  return {
11512
11941
  _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
11513
11942
  data
@@ -11588,8 +12017,9 @@ function registerBrandRuntimeTools(server) {
11588
12017
  pagesScraped: meta.pagesScraped || 0
11589
12018
  }
11590
12019
  };
11591
- const envelope = asEnvelope22(runtime);
12020
+ const envelope = asEnvelope23(runtime);
11592
12021
  return {
12022
+ structuredContent: envelope,
11593
12023
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
11594
12024
  };
11595
12025
  }
@@ -11730,7 +12160,7 @@ function registerBrandRuntimeTools(server) {
11730
12160
  }
11731
12161
  const profile = row.profile_data;
11732
12162
  const checkResult = computeBrandConsistency(content, profile);
11733
- const envelope = asEnvelope22(checkResult);
12163
+ const envelope = asEnvelope23(checkResult);
11734
12164
  return {
11735
12165
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
11736
12166
  };
@@ -11764,7 +12194,7 @@ function registerBrandRuntimeTools(server) {
11764
12194
  content_colors,
11765
12195
  threshold ?? 10
11766
12196
  );
11767
- const envelope = asEnvelope22(auditResult);
12197
+ const envelope = asEnvelope23(auditResult);
11768
12198
  return {
11769
12199
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
11770
12200
  };
@@ -11799,7 +12229,7 @@ function registerBrandRuntimeTools(server) {
11799
12229
  row.profile_data.typography,
11800
12230
  format
11801
12231
  );
11802
- const envelope = asEnvelope22({ format, tokens: output });
12232
+ const envelope = asEnvelope23({ format, tokens: output });
11803
12233
  return {
11804
12234
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
11805
12235
  };
@@ -11810,57 +12240,6 @@ function registerBrandRuntimeTools(server) {
11810
12240
  // src/tools/carousel.ts
11811
12241
  import { z as z28 } from "zod";
11812
12242
  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
12243
  var IMAGE_CREDIT_ESTIMATES2 = {
11865
12244
  midjourney: 20,
11866
12245
  "nano-banana": 15,
@@ -11869,7 +12248,7 @@ var IMAGE_CREDIT_ESTIMATES2 = {
11869
12248
  "flux-max": 50,
11870
12249
  "gpt4o-image": 40,
11871
12250
  imagen4: 35,
11872
- "imagen4-fast": 25,
12251
+ "imagen4-fast": 35,
11873
12252
  seedream: 20
11874
12253
  };
11875
12254
  async function fetchBrandVisualContext(projectId) {
@@ -11993,14 +12372,14 @@ function registerCarouselTools(server) {
11993
12372
  const carouselTextCost = 10 + slideCount * 2;
11994
12373
  const perImageCost = IMAGE_CREDIT_ESTIMATES2[image_model] ?? 30;
11995
12374
  const totalEstimatedCost = carouselTextCost + slideCount * perImageCost;
11996
- const budgetCheck = checkCreditBudget2(totalEstimatedCost);
12375
+ const budgetCheck = checkCreditBudget(totalEstimatedCost);
11997
12376
  if (!budgetCheck.ok) {
11998
12377
  return {
11999
12378
  content: [{ type: "text", text: budgetCheck.message }],
12000
12379
  isError: true
12001
12380
  };
12002
12381
  }
12003
- const assetBudget = checkAssetBudget2();
12382
+ const assetBudget = checkAssetBudget(slideCount);
12004
12383
  if (!assetBudget.ok) {
12005
12384
  return {
12006
12385
  content: [{ type: "text", text: assetBudget.message }],
@@ -12047,8 +12426,15 @@ function registerCarouselTools(server) {
12047
12426
  };
12048
12427
  }
12049
12428
  const carousel = carouselData.carousel;
12429
+ const imageAssetBudget = checkAssetBudget(carousel.slides.length);
12430
+ if (!imageAssetBudget.ok) {
12431
+ return {
12432
+ content: [{ type: "text", text: imageAssetBudget.message }],
12433
+ isError: true
12434
+ };
12435
+ }
12050
12436
  const textCredits = carousel.credits?.used ?? carouselTextCost;
12051
- addCreditsUsed2(textCredits);
12437
+ addCreditsUsed(textCredits);
12052
12438
  const imageJobs = await Promise.all(
12053
12439
  carousel.slides.map(async (slide) => {
12054
12440
  const promptParts = [];
@@ -12078,8 +12464,8 @@ function registerCarouselTools(server) {
12078
12464
  }
12079
12465
  const jobId = data.asyncJobId ?? data.taskId ?? null;
12080
12466
  if (jobId) {
12081
- addCreditsUsed2(perImageCost);
12082
- addAssetsGenerated2(1);
12467
+ addCreditsUsed(perImageCost);
12468
+ addAssetsGenerated(1);
12083
12469
  }
12084
12470
  return {
12085
12471
  slideNumber: slide.slideNumber,
@@ -12185,7 +12571,7 @@ function registerCarouselTools(server) {
12185
12571
 
12186
12572
  // src/tools/niche-research.ts
12187
12573
  import { z as z29 } from "zod";
12188
- function asEnvelope23(data) {
12574
+ function asEnvelope24(data) {
12189
12575
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
12190
12576
  }
12191
12577
  function registerNicheResearchTools(server) {
@@ -12219,7 +12605,7 @@ function registerNicheResearchTools(server) {
12219
12605
  {
12220
12606
  type: "text",
12221
12607
  text: JSON.stringify(
12222
- asEnvelope23({
12608
+ asEnvelope24({
12223
12609
  winners,
12224
12610
  count: winners.length,
12225
12611
  filters: result?.filters ?? {}
@@ -12516,6 +12902,16 @@ import { z as z31 } from "zod";
12516
12902
  import fs from "node:fs/promises";
12517
12903
  import path from "node:path";
12518
12904
  var CALENDAR_URI = "ui://content-calendar/mcp-app.html";
12905
+ var RecentPostOutputSchema = z31.object({
12906
+ id: z31.string(),
12907
+ platform: z31.string(),
12908
+ status: z31.string(),
12909
+ title: z31.string().nullable(),
12910
+ external_post_id: z31.string().nullable(),
12911
+ published_at: z31.string().nullable(),
12912
+ scheduled_at: z31.string().nullable(),
12913
+ created_at: z31.string()
12914
+ });
12519
12915
  function startOfCurrentWeekMonday() {
12520
12916
  const now = /* @__PURE__ */ new Date();
12521
12917
  const day = now.getDay();
@@ -12535,6 +12931,11 @@ function registerContentCalendarApp(server) {
12535
12931
  "ISO date for the week start (YYYY-MM-DD); defaults to the current week's Monday."
12536
12932
  )
12537
12933
  },
12934
+ outputSchema: {
12935
+ start_date: z31.string(),
12936
+ posts: z31.array(RecentPostOutputSchema),
12937
+ scopes: z31.array(z31.string())
12938
+ },
12538
12939
  _meta: {
12539
12940
  ui: {
12540
12941
  resourceUri: CALENDAR_URI,
@@ -12573,11 +12974,17 @@ function registerContentCalendarApp(server) {
12573
12974
  if (!ts) return false;
12574
12975
  return ts.split("T")[0] >= fromDate;
12575
12976
  });
12977
+ const structuredContent = {
12978
+ start_date: fromDate,
12979
+ posts,
12980
+ scopes: userScopes
12981
+ };
12576
12982
  return {
12983
+ structuredContent,
12577
12984
  content: [
12578
12985
  {
12579
12986
  type: "text",
12580
- text: JSON.stringify({ posts, scopes: userScopes })
12987
+ text: `Loaded ${posts.length} calendar post${posts.length === 1 ? "" : "s"} from ${fromDate}.`
12581
12988
  }
12582
12989
  ]
12583
12990
  };
@@ -12952,7 +13359,7 @@ function registerHarnessTools(server, _ctx) {
12952
13359
 
12953
13360
  // src/tools/hermes.ts
12954
13361
  import { z as z34 } from "zod";
12955
- function asEnvelope24(data) {
13362
+ function asEnvelope25(data) {
12956
13363
  return {
12957
13364
  _meta: {
12958
13365
  version: MCP_VERSION,
@@ -13003,7 +13410,7 @@ function registerHermesTools(server) {
13003
13410
  const payload = { content_id: data?.content_id ?? null };
13004
13411
  if (format === "json") {
13005
13412
  return {
13006
- content: [{ type: "text", text: JSON.stringify(asEnvelope24(payload), null, 2) }]
13413
+ content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
13007
13414
  };
13008
13415
  }
13009
13416
  return {
@@ -13047,7 +13454,7 @@ function registerHermesTools(server) {
13047
13454
  };
13048
13455
  if (response_format === "json") {
13049
13456
  return {
13050
- content: [{ type: "text", text: JSON.stringify(asEnvelope24(payload), null, 2) }]
13457
+ content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
13051
13458
  };
13052
13459
  }
13053
13460
  return {
@@ -13077,7 +13484,7 @@ function registerHermesTools(server) {
13077
13484
  const payload = { observation_id: data?.observation_id ?? null };
13078
13485
  if (response_format === "json") {
13079
13486
  return {
13080
- content: [{ type: "text", text: JSON.stringify(asEnvelope24(payload), null, 2) }]
13487
+ content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
13081
13488
  };
13082
13489
  }
13083
13490
  return {
@@ -13121,7 +13528,7 @@ function registerHermesTools(server) {
13121
13528
  };
13122
13529
  if (response_format === "json") {
13123
13530
  return {
13124
- content: [{ type: "text", text: JSON.stringify(asEnvelope24(payload), null, 2) }]
13531
+ content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
13125
13532
  };
13126
13533
  }
13127
13534
  return {
@@ -13165,7 +13572,7 @@ function registerHermesTools(server) {
13165
13572
  };
13166
13573
  if (response_format === "json") {
13167
13574
  return {
13168
- content: [{ type: "text", text: JSON.stringify(asEnvelope24(payload), null, 2) }]
13575
+ content: [{ type: "text", text: JSON.stringify(asEnvelope25(payload), null, 2) }]
13169
13576
  };
13170
13577
  }
13171
13578
  return {
@@ -13195,7 +13602,7 @@ function registerHermesTools(server) {
13195
13602
  content: [
13196
13603
  {
13197
13604
  type: "text",
13198
- text: JSON.stringify(asEnvelope24({ campaigns }), null, 2)
13605
+ text: JSON.stringify(asEnvelope25({ campaigns }), null, 2)
13199
13606
  }
13200
13607
  ]
13201
13608
  };
@@ -13226,6 +13633,340 @@ ${"=".repeat(40)}
13226
13633
  );
13227
13634
  }
13228
13635
 
13636
+ // src/tools/skills.ts
13637
+ import { z as z35 } from "zod";
13638
+
13639
+ // src/lib/skills-manifest.ts
13640
+ var SKILLS_MANIFEST = [
13641
+ {
13642
+ id: "skill-brand-locked-viral-hook-reel",
13643
+ name: "Brand-locked viral hook reel",
13644
+ studio: "video",
13645
+ category: "hook",
13646
+ 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.",
13647
+ hookFormula: "Pattern interrupt in first 0.5s + escalating stakes every 2s + brand-locked voice + cloned-from-winning-past-post structure.",
13648
+ estimatedCredits: 580,
13649
+ estimatedSeconds: 158,
13650
+ featured: true,
13651
+ stepCount: 9,
13652
+ inspiredBy: ["MrBeast", "Alex Hormozi"]
13653
+ }
13654
+ ];
13655
+ function getSkill(id) {
13656
+ return SKILLS_MANIFEST.find((s) => s.id === id);
13657
+ }
13658
+ function listSkills(opts) {
13659
+ let entries = SKILLS_MANIFEST;
13660
+ if (opts?.studio) entries = entries.filter((s) => s.studio === opts.studio);
13661
+ if (opts?.featuredOnly) entries = entries.filter((s) => s.featured);
13662
+ return entries;
13663
+ }
13664
+
13665
+ // src/tools/skills.ts
13666
+ function asEnvelope26(data) {
13667
+ return {
13668
+ _meta: {
13669
+ version: MCP_VERSION,
13670
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
13671
+ },
13672
+ data
13673
+ };
13674
+ }
13675
+ var STUDIO_VALUES = ["video", "avatar", "carousel", "voice", "caption", "edit"];
13676
+ function renderSkillSummary(skill) {
13677
+ const lines = [];
13678
+ lines.push(`${skill.name} (${skill.id})`);
13679
+ lines.push(` Studio: ${skill.studio} \xB7 Category: ${skill.category}`);
13680
+ if (skill.featured) lines.push(" \u2B50 Featured");
13681
+ lines.push(` ${skill.shortDescription}`);
13682
+ lines.push(` Hook: ${skill.hookFormula}`);
13683
+ lines.push(
13684
+ ` Cost: ~${skill.estimatedCredits} credits \xB7 ~${skill.estimatedSeconds}s \xB7 ${skill.stepCount} steps`
13685
+ );
13686
+ lines.push(` Inspired by: ${skill.inspiredBy.join(", ")}`);
13687
+ return lines.join("\n");
13688
+ }
13689
+ function registerSkillsTools(server) {
13690
+ server.tool(
13691
+ "list_skills",
13692
+ '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.',
13693
+ {
13694
+ studio: z35.enum(STUDIO_VALUES).optional().describe("Filter to one studio (video, avatar, carousel, voice, caption, edit)."),
13695
+ featured_only: z35.boolean().optional().describe("Return only featured (recommended) skills. Defaults to false."),
13696
+ response_format: z35.enum(["text", "json"]).optional().describe("Response format. Defaults to text \u2014 human-readable summary.")
13697
+ },
13698
+ async ({ studio, featured_only, response_format }) => {
13699
+ const skills = listSkills({ studio, featuredOnly: featured_only });
13700
+ const format = response_format ?? "text";
13701
+ if (format === "json") {
13702
+ return {
13703
+ content: [
13704
+ {
13705
+ type: "text",
13706
+ text: JSON.stringify(asEnvelope26({ count: skills.length, skills }), null, 2)
13707
+ }
13708
+ ]
13709
+ };
13710
+ }
13711
+ if (skills.length === 0) {
13712
+ return {
13713
+ content: [
13714
+ {
13715
+ type: "text",
13716
+ 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(", ")
13717
+ }
13718
+ ]
13719
+ };
13720
+ }
13721
+ const blocks = skills.map(renderSkillSummary).join("\n\n");
13722
+ const header = `${skills.length} skill${skills.length === 1 ? "" : "s"} available
13723
+ ${"=".repeat(40)}`;
13724
+ return {
13725
+ content: [{ type: "text", text: `${header}
13726
+
13727
+ ${blocks}` }]
13728
+ };
13729
+ }
13730
+ );
13731
+ server.tool(
13732
+ "run_skill",
13733
+ "Run a Social Neuron workflow skill end-to-end (brand-locked content production). PR #4.4 v1: returns a structured run preview with the exact step plan, credit cost, and a deep-link to launch the run in the SN dashboard. PR #4.5 (next 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.",
13734
+ {
13735
+ skill_id: z35.string().describe(
13736
+ 'The id of the skill to run (e.g. "skill-brand-locked-viral-hook-reel"). Use list_skills to discover available ids.'
13737
+ ),
13738
+ topic: z35.string().min(1).describe('What the content is about (e.g. "why we built Social Neuron").'),
13739
+ audience: z35.string().optional().describe(
13740
+ 'Who the content is for (e.g. "first-time founders"). Defaults to brand persona.'
13741
+ ),
13742
+ hook: z35.string().optional().describe(
13743
+ "Optional explicit hook. If omitted, the skill drafts and scores 3-5 candidates."
13744
+ ),
13745
+ cta: z35.string().optional().describe("Optional call-to-action override. Defaults to brand standard CTA."),
13746
+ response_format: z35.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
13747
+ },
13748
+ async ({ skill_id, topic, audience, hook, cta, response_format }) => {
13749
+ const skill = getSkill(skill_id);
13750
+ const format = response_format ?? "text";
13751
+ if (!skill) {
13752
+ const knownIds = SKILLS_MANIFEST.map((s) => s.id).join(", ");
13753
+ return {
13754
+ content: [
13755
+ {
13756
+ type: "text",
13757
+ text: `Unknown skill_id "${skill_id}". Available skills: ${knownIds}. Call list_skills for descriptions.`
13758
+ }
13759
+ ],
13760
+ isError: true
13761
+ };
13762
+ }
13763
+ const runUrl = "https://socialneuron.com/dashboard/creation?skill=" + encodeURIComponent(skill.id);
13764
+ const inputs = { topic, audience, hook, cta };
13765
+ const previewedAt = (/* @__PURE__ */ new Date()).toISOString();
13766
+ if (format === "json") {
13767
+ return {
13768
+ content: [
13769
+ {
13770
+ type: "text",
13771
+ text: JSON.stringify(
13772
+ asEnvelope26({
13773
+ status: "preview",
13774
+ skill,
13775
+ inputs,
13776
+ runUrl,
13777
+ previewedAt,
13778
+ note: "PR #4.4 v1 returns a preview. PR #4.5 will execute in-process via the run-skill EF."
13779
+ }),
13780
+ null,
13781
+ 2
13782
+ )
13783
+ }
13784
+ ]
13785
+ };
13786
+ }
13787
+ const lines = [
13788
+ `Ready to run: ${skill.name}`,
13789
+ "=".repeat(40),
13790
+ "",
13791
+ skill.shortDescription,
13792
+ "",
13793
+ `Hook formula: ${skill.hookFormula}`,
13794
+ `Inspired by: ${skill.inspiredBy.join(", ")}`,
13795
+ "",
13796
+ "Inputs",
13797
+ ` topic : ${topic}`,
13798
+ ` audience : ${audience ?? "(brand persona)"}`,
13799
+ ` hook : ${hook ?? "(auto-drafted from 3-5 candidates, scored)"}`,
13800
+ ` cta : ${cta ?? "(brand default)"}`,
13801
+ "",
13802
+ "Plan",
13803
+ ` ${skill.stepCount} steps \xB7 ~${skill.estimatedCredits} credits \xB7 ~${skill.estimatedSeconds}s wall time`,
13804
+ "",
13805
+ `Launch in dashboard: ${runUrl}`,
13806
+ "",
13807
+ "\u26A0\uFE0F PR #4.4 v1: preview only. End-to-end MCP execution arrives in the next release."
13808
+ ];
13809
+ return {
13810
+ content: [{ type: "text", text: lines.join("\n") }]
13811
+ };
13812
+ }
13813
+ );
13814
+ }
13815
+
13816
+ // src/tools/loopPulse.ts
13817
+ import { z as z36 } from "zod";
13818
+ function asEnvelope27(data) {
13819
+ return {
13820
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
13821
+ data
13822
+ };
13823
+ }
13824
+ function registerLoopPulseTools(server) {
13825
+ server.tool(
13826
+ "get_loop_pulse",
13827
+ '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, bandit-update application rate, per-platform bandit 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.',
13828
+ {
13829
+ response_format: z36.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
13830
+ },
13831
+ async ({ response_format }) => {
13832
+ const format = response_format ?? "text";
13833
+ const { data, error } = await callEdgeFunction("mc-loop-pulse", {});
13834
+ if (error || !data) {
13835
+ return {
13836
+ content: [
13837
+ {
13838
+ type: "text",
13839
+ text: `Failed to read loop pulse: ${error || "no data"}`
13840
+ }
13841
+ ],
13842
+ isError: true
13843
+ };
13844
+ }
13845
+ if (format === "json") {
13846
+ return {
13847
+ content: [{ type: "text", text: JSON.stringify(asEnvelope27(data), null, 2) }]
13848
+ };
13849
+ }
13850
+ const rank = { bad: 0, warn: 1, unknown: 2, ok: 3 };
13851
+ const sorted = [...data.kpis].sort((a, b) => rank[a.status] - rank[b.status]);
13852
+ const lines = [
13853
+ `LOOP PULSE \u2014 overall: ${data.overall.toUpperCase()}`,
13854
+ "",
13855
+ ...sorted.map((k) => {
13856
+ const unit = k.unit ?? "";
13857
+ const val = k.value == null ? "\u2014" : `${k.value}${unit}`;
13858
+ return `[${k.status.toUpperCase().padEnd(4)}] ${k.label}: ${val}
13859
+ ${k.why}`;
13860
+ }),
13861
+ "",
13862
+ `Generated ${data.generated_at}`
13863
+ ];
13864
+ return {
13865
+ content: [{ type: "text", text: lines.join("\n") }]
13866
+ };
13867
+ }
13868
+ );
13869
+ }
13870
+
13871
+ // src/tools/banditState.ts
13872
+ import { z as z37 } from "zod";
13873
+ init_supabase();
13874
+ function asEnvelope28(data) {
13875
+ return {
13876
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
13877
+ data
13878
+ };
13879
+ }
13880
+ function registerBanditStateTools(server) {
13881
+ server.tool(
13882
+ "get_bandit_state",
13883
+ "Read the current Thompson Sampling bandit posteriors for a project. Returns top-K arms per (arm_type, platform) with Beta(alpha, beta) posterior mean and uncertainty. Use this to reason about which hook family / format / timing slot the bandit currently prefers on each platform before recommending next moves. SN real arm types: hook_family (6 fallback families), 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). Legacy/dead-taxonomy types also present: hook_type, format, timing_slot, topic_cluster, caption_style, platform, story_type, emoji_type.",
13884
+ {
13885
+ project_id: z37.string().uuid().optional().describe("Project UUID. Defaults to the authenticated user default project."),
13886
+ platform: z37.enum([
13887
+ "instagram",
13888
+ "tiktok",
13889
+ "youtube",
13890
+ "linkedin",
13891
+ "twitter",
13892
+ "facebook",
13893
+ "threads",
13894
+ "bluesky"
13895
+ ]).optional().describe(
13896
+ "Lowercase platform name. Omit to return both platform-scoped and legacy platform-agnostic arms."
13897
+ ),
13898
+ arm_type: z37.enum([
13899
+ "hook_family",
13900
+ "hook_type",
13901
+ "format",
13902
+ "timing_slot",
13903
+ "topic_cluster",
13904
+ "caption_style",
13905
+ "platform",
13906
+ "story_type",
13907
+ "emoji_type",
13908
+ "length_bucket",
13909
+ "posting_time_bucket",
13910
+ "content_format"
13911
+ ]).optional().describe("Arm dimension. Omit to return all types grouped."),
13912
+ top_k: z37.number().min(1).max(50).optional().describe("Max arms per (arm_type) group. Default 5."),
13913
+ response_format: z37.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
13914
+ },
13915
+ async ({ project_id, platform: platform2, arm_type, top_k, response_format }) => {
13916
+ const format = response_format ?? "text";
13917
+ const projectId = project_id ?? await getDefaultProjectId();
13918
+ if (!projectId) {
13919
+ return {
13920
+ content: [
13921
+ { type: "text", text: "No project_id provided and no default project available." }
13922
+ ],
13923
+ isError: true
13924
+ };
13925
+ }
13926
+ const { data, error } = await callEdgeFunction("mc-bandit-state", {
13927
+ project_id: projectId,
13928
+ platform: platform2,
13929
+ arm_type,
13930
+ top_k
13931
+ });
13932
+ if (error || !data) {
13933
+ return {
13934
+ content: [
13935
+ { type: "text", text: `Failed to read bandit state: ${error || "no data"}` }
13936
+ ],
13937
+ isError: true
13938
+ };
13939
+ }
13940
+ if (format === "json") {
13941
+ return { content: [{ type: "text", text: JSON.stringify(asEnvelope28(data), null, 2) }] };
13942
+ }
13943
+ const lines = [
13944
+ `BANDIT STATE \u2014 project ${data.project_id.slice(0, 8)}${data.platform_filter ? ` \xB7 ${data.platform_filter}` : ""}`,
13945
+ `${data.total_arms} arms across ${data.groups.length} arm types`,
13946
+ ""
13947
+ ];
13948
+ for (const group of data.groups) {
13949
+ lines.push(`[${group.arm_type}] ${group.summary}`);
13950
+ const renderArm = (a) => {
13951
+ const conf = a.posterior_stdev < 0.05 ? "high" : a.posterior_stdev < 0.15 ? "med" : "low";
13952
+ 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)`;
13953
+ };
13954
+ if (group.platform_scoped.length > 0) {
13955
+ lines.push(` Per-platform top arms:`);
13956
+ group.platform_scoped.forEach((a) => lines.push(renderArm(a)));
13957
+ }
13958
+ if (group.platform_agnostic.length > 0) {
13959
+ lines.push(` Platform-agnostic (legacy) top arms:`);
13960
+ group.platform_agnostic.forEach((a) => lines.push(renderArm(a)));
13961
+ }
13962
+ lines.push("");
13963
+ }
13964
+ lines.push(`Generated ${data.generated_at}`);
13965
+ return { content: [{ type: "text", text: lines.join("\n") }] };
13966
+ }
13967
+ );
13968
+ }
13969
+
13229
13970
  // src/lib/register-tools.ts
13230
13971
  function wrapToolWithScanner(toolName, handler) {
13231
13972
  return async function scannerWrappedHandler(...handlerArgs) {
@@ -13278,7 +14019,8 @@ function wrapToolWithScanner(toolName, handler) {
13278
14019
  }
13279
14020
  function applyScopeEnforcement(server, scopeResolver) {
13280
14021
  const originalTool = server.tool.bind(server);
13281
- server.tool = function wrappedTool(...args) {
14022
+ const originalRegisterTool = server.registerTool?.bind(server);
14023
+ const wrapRegistration = (...args) => {
13282
14024
  const name = args[0];
13283
14025
  const requiredScope = TOOL_SCOPES[name];
13284
14026
  const handlerIndex = args.findIndex(
@@ -13289,27 +14031,11 @@ function applyScopeEnforcement(server, scopeResolver) {
13289
14031
  const scannerWrappedHandler = wrapToolWithScanner(name, originalHandler);
13290
14032
  args[handlerIndex] = async function scopeEnforcedHandler(...handlerArgs) {
13291
14033
  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
- };
14034
+ return scopeDeniedResult(name, void 0, scopeResolver());
13301
14035
  }
13302
14036
  const userScopes = scopeResolver();
13303
14037
  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
- };
14038
+ return scopeDeniedResult(name, requiredScope, userScopes);
13313
14039
  }
13314
14040
  const startedAt = Date.now();
13315
14041
  try {
@@ -13336,8 +14062,58 @@ function applyScopeEnforcement(server, scopeResolver) {
13336
14062
  }
13337
14063
  };
13338
14064
  }
13339
- return originalTool(...args);
14065
+ return args;
13340
14066
  };
14067
+ server.tool = function wrappedTool(...args) {
14068
+ return originalTool(...wrapRegistration(...args));
14069
+ };
14070
+ if (originalRegisterTool) {
14071
+ server.registerTool = function wrappedRegisterTool(...args) {
14072
+ return originalRegisterTool(...wrapRegistration(...args));
14073
+ };
14074
+ }
14075
+ }
14076
+ function scopeDeniedResult(name, requiredScope, userScopes) {
14077
+ const error = requiredScope ? {
14078
+ error: "permission_denied",
14079
+ tool: name,
14080
+ required_scope: requiredScope,
14081
+ available_scopes: userScopes,
14082
+ recover_with: [
14083
+ "Call search_tools with available_only=true to find tools this key can use.",
14084
+ "Use a read-only alternative if one is available for the task.",
14085
+ "Regenerate the API key with the required scope or upgrade the plan tier."
14086
+ ],
14087
+ developer_url: "https://socialneuron.com/settings/developer"
14088
+ } : {
14089
+ error: "tool_scope_missing",
14090
+ tool: name,
14091
+ available_scopes: userScopes,
14092
+ recover_with: ["Contact support; this tool is not mapped to a required scope."]
14093
+ };
14094
+ const challenge = requiredScope ? buildWwwAuthenticateHeader({
14095
+ issuerUrl: getChallengeIssuerUrl(),
14096
+ error: "insufficient_scope",
14097
+ errorDescription: `Tool ${name} requires scope ${requiredScope}.`,
14098
+ scope: requiredScope
14099
+ }) : void 0;
14100
+ return {
14101
+ content: [{ type: "text", text: JSON.stringify(error, null, 2) }],
14102
+ ...challenge ? { _meta: { "mcp/www_authenticate": [challenge] } } : {},
14103
+ isError: true
14104
+ };
14105
+ }
14106
+ function getChallengeIssuerUrl() {
14107
+ if (process.env.OAUTH_ISSUER_URL) return process.env.OAUTH_ISSUER_URL;
14108
+ const mcpServerUrl = process.env.MCP_SERVER_URL;
14109
+ if (mcpServerUrl) {
14110
+ try {
14111
+ const parsed = new URL(mcpServerUrl);
14112
+ return `${parsed.protocol}//${parsed.host}`;
14113
+ } catch {
14114
+ }
14115
+ }
14116
+ return "https://mcp.socialneuron.com";
13341
14117
  }
13342
14118
  var RESPONSE_CHAR_LIMIT = 1e5;
13343
14119
  function truncateResponse(result) {
@@ -13408,6 +14184,9 @@ function registerAllTools(server, options) {
13408
14184
  registerConnectionTools(server);
13409
14185
  registerHarnessTools(server, void 0);
13410
14186
  registerHermesTools(server);
14187
+ registerSkillsTools(server);
14188
+ registerLoopPulseTools(server);
14189
+ registerBanditStateTools(server);
13411
14190
  if (!options?.skipApps) {
13412
14191
  registerContentCalendarApp(server);
13413
14192
  }
@@ -13415,17 +14194,17 @@ function registerAllTools(server, options) {
13415
14194
  }
13416
14195
 
13417
14196
  // src/prompts.ts
13418
- import { z as z35 } from "zod";
14197
+ import { z as z38 } from "zod";
13419
14198
  function registerPrompts(server) {
13420
14199
  server.prompt(
13421
14200
  "create_weekly_content_plan",
13422
14201
  "Generate a full week of social media content (7 days, multiple platforms). Returns a structured plan with topics, formats, and posting times.",
13423
14202
  {
13424
- niche: z35.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
13425
- platforms: z35.string().optional().describe(
14203
+ niche: z38.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
14204
+ platforms: z38.string().optional().describe(
13426
14205
  'Comma-separated platforms to target (default: "YouTube, Instagram, TikTok, LinkedIn")'
13427
14206
  ),
13428
- tone: z35.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
14207
+ tone: z38.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
13429
14208
  },
13430
14209
  ({ niche, platforms, tone }) => {
13431
14210
  const targetPlatforms = platforms || "YouTube, Instagram, TikTok, LinkedIn";
@@ -13467,8 +14246,8 @@ After building the plan, use \`save_content_plan\` to save it.`
13467
14246
  "analyze_top_content",
13468
14247
  "Analyze your best-performing posts to identify patterns and replicate success. Returns insights on hooks, formats, timing, and topics that resonate.",
13469
14248
  {
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")')
14249
+ timeframe: z38.string().optional().describe('Analysis period (default: "30 days"). E.g., "7 days", "90 days"'),
14250
+ platform: z38.string().optional().describe('Filter to a specific platform (e.g., "youtube", "instagram")')
13472
14251
  },
13473
14252
  ({ timeframe, platform: platform2 }) => {
13474
14253
  const period = timeframe || "30 days";
@@ -13505,10 +14284,10 @@ Format as a clear, actionable performance report.`
13505
14284
  "repurpose_content",
13506
14285
  "Take one piece of content and transform it into 8-10 pieces across multiple platforms and formats.",
13507
14286
  {
13508
- source: z35.string().describe(
14287
+ source: z38.string().describe(
13509
14288
  "The source content to repurpose \u2014 a URL, transcript, or the content text itself"
13510
14289
  ),
13511
- target_platforms: z35.string().optional().describe(
14290
+ target_platforms: z38.string().optional().describe(
13512
14291
  'Comma-separated target platforms (default: "Twitter, LinkedIn, Instagram, TikTok, YouTube")'
13513
14292
  )
13514
14293
  },
@@ -13550,9 +14329,9 @@ For each piece, include the platform, format, character count, and suggested pos
13550
14329
  "setup_brand_voice",
13551
14330
  "Define or refine your brand voice profile so all generated content stays on-brand. Walks through tone, audience, values, and style.",
13552
14331
  {
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")
14332
+ brand_name: z38.string().describe("Your brand or business name"),
14333
+ industry: z38.string().optional().describe('Your industry or niche (e.g., "B2B SaaS", "fitness coaching")'),
14334
+ website: z38.string().optional().describe("Your website URL for context")
13556
14335
  },
13557
14336
  ({ brand_name, industry, website }) => {
13558
14337
  const industryContext = industry ? ` in the ${industry} space` : "";
@@ -13793,13 +14572,13 @@ function registerResources(server) {
13793
14572
  pro: {
13794
14573
  price: "$49/mo",
13795
14574
  credits: 1500,
13796
- mcp_access: "Read + Analytics",
14575
+ mcp_access: "Read + Analytics + Write + Distribute",
13797
14576
  features: [
13798
14577
  "All Starter features",
13799
14578
  "Closed-loop optimization",
13800
- "5 autopilot runs / month",
14579
+ "Full agent engine via MCP (generate, gate, publish)",
13801
14580
  "All 10 platforms",
13802
- "MCP read + analytics"
14581
+ "MCP read + analytics + write + distribute"
13803
14582
  ]
13804
14583
  },
13805
14584
  team: {
@@ -13904,6 +14683,7 @@ function registerResources(server) {
13904
14683
  init_request_context();
13905
14684
 
13906
14685
  // src/lib/token-verifier.ts
14686
+ import { createHash as createHash3 } from "node:crypto";
13907
14687
  import * as jose from "jose";
13908
14688
  var jwks = null;
13909
14689
  function getJWKS(supabaseUrl) {
@@ -13913,39 +14693,89 @@ function getJWKS(supabaseUrl) {
13913
14693
  }
13914
14694
  return jwks;
13915
14695
  }
13916
- var apiKeyCache = /* @__PURE__ */ new Map();
13917
- var API_KEY_CACHE_TTL_MS = 1e4;
14696
+ var tokenValidationCache = /* @__PURE__ */ new Map();
14697
+ var CONNECTOR_TOKEN_VALIDATION_CACHE_TTL_MS = 3e5;
14698
+ var DEFAULT_MCP_RESOURCE = "https://mcp.socialneuron.com";
14699
+ function cacheKey(token) {
14700
+ return createHash3("sha256").update(token).digest("hex");
14701
+ }
13918
14702
  function evictFromCache(token) {
13919
- apiKeyCache.delete(token);
14703
+ tokenValidationCache.delete(cacheKey(token));
14704
+ }
14705
+ async function verifyCachedOpaqueToken(token, validate, ttlMs) {
14706
+ const key = cacheKey(token);
14707
+ const cached2 = tokenValidationCache.get(key);
14708
+ const now = Date.now();
14709
+ if (cached2 && cached2.expiresAt > now) {
14710
+ const tokenExpiresAtMs2 = cached2.authInfo.expiresAt ? cached2.authInfo.expiresAt * 1e3 : void 0;
14711
+ if (!tokenExpiresAtMs2 || tokenExpiresAtMs2 > now) {
14712
+ return cached2.authInfo;
14713
+ }
14714
+ }
14715
+ if (cached2) tokenValidationCache.delete(key);
14716
+ const authInfo = await validate();
14717
+ const tokenExpiresAtMs = authInfo.expiresAt ? authInfo.expiresAt * 1e3 : void 0;
14718
+ const cacheExpiresAt = Math.min(Date.now() + ttlMs, tokenExpiresAtMs ?? Number.POSITIVE_INFINITY);
14719
+ if (cacheExpiresAt > Date.now()) {
14720
+ tokenValidationCache.set(key, {
14721
+ authInfo,
14722
+ expiresAt: cacheExpiresAt
14723
+ });
14724
+ }
14725
+ if (tokenValidationCache.size > 100) {
14726
+ const now2 = Date.now();
14727
+ for (const [k, v] of tokenValidationCache) {
14728
+ if (v.expiresAt <= now2) tokenValidationCache.delete(k);
14729
+ }
14730
+ }
14731
+ return authInfo;
13920
14732
  }
13921
14733
  function createTokenVerifier(options) {
13922
14734
  const { supabaseUrl, supabaseAnonKey } = options;
14735
+ const expectedResource = normalizeResource(
14736
+ options.resource ?? process.env.MCP_RESOURCE_URL ?? process.env.MCP_SERVER_URL
14737
+ ) ?? DEFAULT_MCP_RESOURCE;
13923
14738
  return {
13924
14739
  async verifyAccessToken(token) {
13925
14740
  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;
14741
+ return verifyApiKey(token, supabaseUrl, supabaseAnonKey);
14742
+ }
14743
+ if (token.startsWith("sno_")) {
14744
+ return verifyCachedOpaqueToken(
14745
+ token,
14746
+ () => verifyConnectorToken(token, supabaseUrl, supabaseAnonKey, expectedResource),
14747
+ CONNECTOR_TOKEN_VALIDATION_CACHE_TTL_MS
14748
+ );
13940
14749
  }
13941
14750
  return verifySupabaseJwt(token, supabaseUrl);
13942
14751
  }
13943
14752
  };
13944
14753
  }
14754
+ function normalizeResource(value) {
14755
+ if (!value) return void 0;
14756
+ try {
14757
+ const parsed = new URL(value);
14758
+ return `${parsed.protocol}//${parsed.host}`;
14759
+ } catch {
14760
+ return value.replace(/\/$/, "");
14761
+ }
14762
+ }
14763
+ function audienceIncludesExpected(audience, expectedResource) {
14764
+ if (typeof audience === "string") {
14765
+ return normalizeResource(audience) === expectedResource;
14766
+ }
14767
+ if (Array.isArray(audience)) {
14768
+ return audience.some((item) => audienceIncludesExpected(item, expectedResource));
14769
+ }
14770
+ return false;
14771
+ }
13945
14772
  async function verifySupabaseJwt(token, supabaseUrl) {
13946
14773
  const jwksKeySet = getJWKS(supabaseUrl);
13947
14774
  const { payload } = await jose.jwtVerify(token, jwksKeySet, {
13948
- issuer: `${supabaseUrl}/auth/v1`
14775
+ issuer: `${supabaseUrl}/auth/v1`,
14776
+ audience: "authenticated",
14777
+ algorithms: ["RS256", "ES256"],
14778
+ clockTolerance: "30s"
13949
14779
  });
13950
14780
  const userId = payload.sub;
13951
14781
  if (!userId) {
@@ -14004,9 +14834,59 @@ async function verifyApiKey(apiKey, supabaseUrl, supabaseAnonKey) {
14004
14834
  throw err;
14005
14835
  }
14006
14836
  }
14837
+ async function verifyConnectorToken(accessToken, supabaseUrl, supabaseAnonKey, expectedResource) {
14838
+ const controller = new AbortController();
14839
+ const timer = setTimeout(() => controller.abort(), 1e4);
14840
+ try {
14841
+ const response = await fetch(
14842
+ `${supabaseUrl}/functions/v1/mcp-auth?action=validate-connector-token`,
14843
+ {
14844
+ method: "POST",
14845
+ headers: {
14846
+ Authorization: `Bearer ${supabaseAnonKey}`,
14847
+ "Content-Type": "application/json"
14848
+ },
14849
+ body: JSON.stringify({ access_token: accessToken, resource: expectedResource }),
14850
+ signal: controller.signal
14851
+ }
14852
+ );
14853
+ if (!response.ok) {
14854
+ throw new Error(`Connector token validation failed: HTTP ${response.status}`);
14855
+ }
14856
+ const data = await response.json();
14857
+ if (!data.valid || !data.userId) {
14858
+ throw new Error(data.error ?? "Invalid connector token");
14859
+ }
14860
+ const expiresAt = data.expiresAt ? Math.floor(new Date(data.expiresAt).getTime() / 1e3) : void 0;
14861
+ if (expiresAt && expiresAt < Math.floor(Date.now() / 1e3)) {
14862
+ throw new Error("Connector token expired");
14863
+ }
14864
+ const tokenResource = data.resource ?? data.audience ?? data.aud;
14865
+ if (!audienceIncludesExpected(tokenResource, expectedResource)) {
14866
+ throw new Error("Connector token audience/resource mismatch");
14867
+ }
14868
+ return {
14869
+ token: accessToken,
14870
+ clientId: data.clientId ?? "connector-oauth",
14871
+ scopes: data.scopes ?? ["mcp:read"],
14872
+ expiresAt,
14873
+ extra: { userId: data.userId, email: data.email, resource: expectedResource }
14874
+ };
14875
+ } catch (err) {
14876
+ if (err instanceof Error && err.name === "AbortError") {
14877
+ throw new Error("Connector token validation timed out");
14878
+ }
14879
+ throw err;
14880
+ } finally {
14881
+ clearTimeout(timer);
14882
+ }
14883
+ }
14007
14884
 
14008
14885
  // src/lib/oauth-provider.ts
14009
- import { InvalidClientMetadataError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
14886
+ import {
14887
+ InvalidClientMetadataError,
14888
+ TooManyRequestsError
14889
+ } from "@modelcontextprotocol/sdk/server/auth/errors.js";
14010
14890
  init_supabase();
14011
14891
  var ALLOWED_REDIRECT_URIS = /* @__PURE__ */ new Set([
14012
14892
  // Claude Code local callback
@@ -14024,11 +14904,18 @@ var ALLOWED_REDIRECT_URIS = /* @__PURE__ */ new Set([
14024
14904
  "https://glama.ai/callback",
14025
14905
  "https://mcp.so/callback"
14026
14906
  ]);
14907
+ var ALLOWED_LOOPBACK_CALLBACK_PATHS = /* @__PURE__ */ new Set([
14908
+ "/oauth/callback",
14909
+ "/oauth/callback/debug",
14910
+ // Codex uses an ephemeral loopback server with this callback path.
14911
+ "/callback"
14912
+ ]);
14913
+ var CODEX_LOOPBACK_CALLBACK_PATH_RE = /^\/callback\/[A-Za-z0-9_-]+$/;
14027
14914
  function isAllowedRedirectUri(uri) {
14028
14915
  if (ALLOWED_REDIRECT_URIS.has(uri)) return true;
14029
14916
  try {
14030
14917
  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:") {
14918
+ 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
14919
  return true;
14033
14920
  }
14034
14921
  if (parsed.protocol === "https:") {
@@ -14038,6 +14925,45 @@ function isAllowedRedirectUri(uri) {
14038
14925
  }
14039
14926
  return false;
14040
14927
  }
14928
+ var MAX_CLIENT_NAME_BYTES = 512;
14929
+ var MAX_REDIRECT_URIS = 10;
14930
+ var MAX_REDIRECT_URI_BYTES = 2048;
14931
+ var MAX_METADATA_BYTES = 8192;
14932
+ var MAX_CACHED_CLIENTS = 1e3;
14933
+ var MAX_PERSISTED_CLIENTS = 5e3;
14934
+ var OAUTH_CLIENT_RETENTION_DAYS = 90;
14935
+ function byteLength(value) {
14936
+ return Buffer.byteLength(value, "utf8");
14937
+ }
14938
+ function assertMaxBytes(label, value, maxBytes) {
14939
+ if (value !== void 0 && byteLength(value) > maxBytes) {
14940
+ throw new InvalidClientMetadataError(`${label} exceeds ${maxBytes} bytes`);
14941
+ }
14942
+ }
14943
+ function assertRegistrationWithinBounds(client3) {
14944
+ assertMaxBytes("client_name", client3.client_name, MAX_CLIENT_NAME_BYTES);
14945
+ if ((client3.redirect_uris?.length ?? 0) > MAX_REDIRECT_URIS) {
14946
+ throw new InvalidClientMetadataError(`redirect_uris exceeds ${MAX_REDIRECT_URIS} entries`);
14947
+ }
14948
+ for (const uri of client3.redirect_uris ?? []) {
14949
+ assertMaxBytes("redirect_uri", uri, MAX_REDIRECT_URI_BYTES);
14950
+ }
14951
+ const metadataBytes = byteLength(JSON.stringify(clientToRow(client3).metadata));
14952
+ if (metadataBytes > MAX_METADATA_BYTES) {
14953
+ throw new InvalidClientMetadataError(`client metadata exceeds ${MAX_METADATA_BYTES} bytes`);
14954
+ }
14955
+ }
14956
+ function cacheClient(cache, clientId, client3) {
14957
+ if (cache.has(clientId)) {
14958
+ cache.delete(clientId);
14959
+ }
14960
+ cache.set(clientId, client3);
14961
+ while (cache.size > MAX_CACHED_CLIENTS) {
14962
+ const oldestClientId = cache.keys().next().value;
14963
+ if (!oldestClientId) break;
14964
+ cache.delete(oldestClientId);
14965
+ }
14966
+ }
14041
14967
  function rowToClient(row) {
14042
14968
  return {
14043
14969
  client_id: row.client_id,
@@ -14082,8 +15008,8 @@ function createClientsStore() {
14082
15008
  }
14083
15009
  return {
14084
15010
  async getClient(clientId) {
14085
- const cached = cache.get(clientId);
14086
- if (cached) return cached;
15011
+ const cached2 = cache.get(clientId);
15012
+ if (cached2) return cached2;
14087
15013
  if (!supabaseAvailable) return void 0;
14088
15014
  try {
14089
15015
  const supabase = getSupabaseClient();
@@ -14094,7 +15020,7 @@ function createClientsStore() {
14094
15020
  }
14095
15021
  if (!data || Array.isArray(data) && data.length === 0) return void 0;
14096
15022
  const client3 = rowToClient(data);
14097
- cache.set(clientId, client3);
15023
+ cacheClient(cache, clientId, client3);
14098
15024
  void supabase.from("mcp_oauth_clients").update({ last_used_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("client_id", clientId);
14099
15025
  return client3;
14100
15026
  } catch (err) {
@@ -14110,18 +15036,38 @@ function createClientsStore() {
14110
15036
  }
14111
15037
  }
14112
15038
  }
14113
- cache.set(client3.client_id, client3);
15039
+ assertRegistrationWithinBounds(client3);
14114
15040
  if (supabaseAvailable) {
14115
15041
  try {
14116
15042
  const supabase = getSupabaseClient();
14117
- const { error } = await supabase.from("mcp_oauth_clients").insert(clientToRow(client3));
14118
- if (error) {
14119
- markUnavailable(error.message);
15043
+ const retentionCutoff = new Date(
15044
+ Date.now() - OAUTH_CLIENT_RETENTION_DAYS * 24 * 60 * 60 * 1e3
15045
+ ).toISOString();
15046
+ const { error: pruneError } = await supabase.from("mcp_oauth_clients").delete().lt("last_used_at", retentionCutoff);
15047
+ if (pruneError) {
15048
+ markUnavailable(pruneError.message);
15049
+ } else {
15050
+ const { count, error: countError } = await supabase.from("mcp_oauth_clients").select("client_id", { count: "exact", head: true });
15051
+ if (countError) {
15052
+ markUnavailable(countError.message);
15053
+ } else {
15054
+ if ((count ?? 0) >= MAX_PERSISTED_CLIENTS) {
15055
+ throw new TooManyRequestsError(
15056
+ "OAuth client registration capacity reached; retry later"
15057
+ );
15058
+ }
15059
+ const { error } = await supabase.from("mcp_oauth_clients").insert(clientToRow(client3));
15060
+ if (error) {
15061
+ markUnavailable(error.message);
15062
+ }
15063
+ }
14120
15064
  }
14121
15065
  } catch (err) {
15066
+ if (err instanceof TooManyRequestsError) throw err;
14122
15067
  markUnavailable(err instanceof Error ? err.message : "unknown error");
14123
15068
  }
14124
15069
  }
15070
+ cacheClient(cache, client3.client_id, client3);
14125
15071
  return client3;
14126
15072
  }
14127
15073
  };
@@ -14154,6 +15100,10 @@ function createOAuthProvider(options) {
14154
15100
  if (params.redirectUri) {
14155
15101
  consentUrl.searchParams.set("redirect_uri", params.redirectUri);
14156
15102
  }
15103
+ const resource = params.resource;
15104
+ if (resource) {
15105
+ consentUrl.searchParams.set("resource", resource.href);
15106
+ }
14157
15107
  res.redirect(consentUrl.toString());
14158
15108
  },
14159
15109
  async challengeForAuthorizationCode(_client, _authorizationCode) {
@@ -14161,7 +15111,7 @@ function createOAuthProvider(options) {
14161
15111
  "Local PKCE validation is disabled \u2014 challengeForAuthorizationCode should not be called. PKCE is verified server-side by the mcp-auth Edge Function."
14162
15112
  );
14163
15113
  },
14164
- async exchangeAuthorizationCode(_client, authorizationCode, codeVerifier, redirectUri) {
15114
+ async exchangeAuthorizationCode(client3, authorizationCode, codeVerifier, redirectUri, resource) {
14165
15115
  if (!codeVerifier) {
14166
15116
  throw new Error("code_verifier is required for PKCE exchange");
14167
15117
  }
@@ -14179,8 +15129,14 @@ function createOAuthProvider(options) {
14179
15129
  code_verifier: codeVerifier,
14180
15130
  authorization_code: authorizationCode,
14181
15131
  return_token: true,
15132
+ // Forward client_id so the Edge Function can match it against
15133
+ // the client that the code was originally issued to.
15134
+ client_id: client3.client_id,
14182
15135
  // Pass redirect_uri for server-side verification (OAuth spec)
14183
- ...redirectUri && { redirect_uri: redirectUri }
15136
+ ...redirectUri && { redirect_uri: redirectUri },
15137
+ // ChatGPT sends resource; backend should bind issued connector
15138
+ // tokens to that MCP resource/audience.
15139
+ ...resource && { resource: resource.href }
14184
15140
  }),
14185
15141
  signal: controller.signal
14186
15142
  });
@@ -14203,13 +15159,55 @@ function createOAuthProvider(options) {
14203
15159
  return {
14204
15160
  access_token: data.access_token,
14205
15161
  token_type: "bearer",
14206
- expires_in: data.expires_in ?? 7776e3,
14207
- // 90 days default
14208
- scope: data.scopes?.join(" ")
15162
+ expires_in: data.expires_in ?? (data.access_token.startsWith("snk_") ? 7776e3 : 3600),
15163
+ scope: data.scopes?.join(" "),
15164
+ ...data.refresh_token ? { refresh_token: data.refresh_token } : {}
14209
15165
  };
14210
15166
  },
14211
- async exchangeRefreshToken(_client, _refreshToken, _scopes) {
14212
- throw new Error("Refresh tokens are not supported. Generate a new API key.");
15167
+ async exchangeRefreshToken(client3, refreshToken, scopes, resource) {
15168
+ const controller = new AbortController();
15169
+ const timer = setTimeout(() => controller.abort(), 1e4);
15170
+ try {
15171
+ const response = await fetch(
15172
+ `${supabaseUrl}/functions/v1/mcp-auth?action=refresh-connector-token`,
15173
+ {
15174
+ method: "POST",
15175
+ headers: {
15176
+ Authorization: `Bearer ${supabaseAnonKey}`,
15177
+ "Content-Type": "application/json"
15178
+ },
15179
+ body: JSON.stringify({
15180
+ client_id: client3.client_id,
15181
+ refresh_token: refreshToken,
15182
+ ...scopes && scopes.length > 0 ? { scopes } : {},
15183
+ ...resource ? { resource: resource.href } : {}
15184
+ }),
15185
+ signal: controller.signal
15186
+ }
15187
+ );
15188
+ if (!response.ok) {
15189
+ const err = await response.json().catch(() => ({ error: "Refresh failed" }));
15190
+ throw new Error(err.error ?? `HTTP ${response.status}`);
15191
+ }
15192
+ const data = await response.json();
15193
+ if (!data.access_token) {
15194
+ throw new Error(data.error ?? "No access token returned from refresh");
15195
+ }
15196
+ return {
15197
+ access_token: data.access_token,
15198
+ token_type: "bearer",
15199
+ expires_in: data.expires_in ?? 3600,
15200
+ scope: data.scopes?.join(" "),
15201
+ ...data.refresh_token ? { refresh_token: data.refresh_token } : {}
15202
+ };
15203
+ } catch (err) {
15204
+ if (err instanceof Error && err.name === "AbortError") {
15205
+ throw new Error("Refresh token exchange timed out");
15206
+ }
15207
+ throw err;
15208
+ } finally {
15209
+ clearTimeout(timer);
15210
+ }
14213
15211
  },
14214
15212
  async verifyAccessToken(token) {
14215
15213
  return tokenVerifier2.verifyAccessToken(token);
@@ -14219,15 +15217,23 @@ function createOAuthProvider(options) {
14219
15217
  const controller = new AbortController();
14220
15218
  const timer = setTimeout(() => controller.abort(), 1e4);
14221
15219
  try {
14222
- await fetch(`${supabaseUrl}/functions/v1/mcp-auth?action=revoke-by-token`, {
15220
+ const action = request.token.startsWith("sno_") || request.token_type_hint === "refresh_token" ? "revoke-connector-token" : "revoke-by-token";
15221
+ const response = await fetch(`${supabaseUrl}/functions/v1/mcp-auth?action=${action}`, {
14223
15222
  method: "POST",
14224
15223
  headers: {
14225
15224
  Authorization: `Bearer ${supabaseAnonKey}`,
14226
15225
  "Content-Type": "application/json"
14227
15226
  },
14228
- body: JSON.stringify({ token: request.token }),
15227
+ body: JSON.stringify({
15228
+ token: request.token,
15229
+ token_type_hint: request.token_type_hint,
15230
+ client_id: _client.client_id
15231
+ }),
14229
15232
  signal: controller.signal
14230
15233
  });
15234
+ if (!response.ok) {
15235
+ throw new Error(`Token revocation failed: HTTP ${response.status}`);
15236
+ }
14231
15237
  } catch (err) {
14232
15238
  const msg = err instanceof Error ? err.message : "unknown";
14233
15239
  console.error(`[oauth] Token revocation call failed: ${msg}`);
@@ -14241,27 +15247,120 @@ function createOAuthProvider(options) {
14241
15247
  // src/http.ts
14242
15248
  init_posthog();
14243
15249
 
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, "_")}"`);
15250
+ // src/lib/discovery-catalog.ts
15251
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15252
+ var cached = null;
15253
+ function buildDiscoveryCatalog() {
15254
+ if (!cached) cached = computeDiscoveryCatalog();
15255
+ return cached;
15256
+ }
15257
+ async function computeDiscoveryCatalog() {
15258
+ const schemaByName = /* @__PURE__ */ new Map();
15259
+ try {
15260
+ const probe = new McpServer({ name: "discovery-probe", version: MCP_VERSION });
15261
+ registerAllTools(probe, { skipScreenshots: true });
15262
+ const handlers = probe.server._requestHandlers;
15263
+ const listHandler = handlers.get("tools/list");
15264
+ if (listHandler) {
15265
+ const out = await listHandler({ method: "tools/list", params: {} }, {});
15266
+ for (const t of out.tools) {
15267
+ const props = t.inputSchema?.properties;
15268
+ if (props && Object.keys(props).length > 0) {
15269
+ schemaByName.set(t.name, {
15270
+ type: "object",
15271
+ properties: props,
15272
+ ...t.inputSchema?.required ? { required: t.inputSchema.required } : {}
15273
+ });
15274
+ }
15275
+ }
15276
+ }
15277
+ } catch (err) {
15278
+ console.error(
15279
+ "[mcp] discovery schema build failed; serving names-only catalog:",
15280
+ err?.message
15281
+ );
14249
15282
  }
14250
- if (opts.errorDescription) {
14251
- params.push(`error_description="${opts.errorDescription.replace(/[\\"]/g, "_")}"`);
15283
+ return TOOL_CATALOG.filter((t) => !t.localOnly).map((t) => ({
15284
+ name: t.name,
15285
+ description: t.description,
15286
+ inputSchema: schemaByName.get(t.name) ?? { type: "object", properties: {} }
15287
+ }));
15288
+ }
15289
+
15290
+ // src/lib/origin-policy.ts
15291
+ var PRODUCTION_FALLBACK_ORIGINS = [
15292
+ "https://socialneuron.com",
15293
+ "https://www.socialneuron.com",
15294
+ "https://app.socialneuron.com"
15295
+ ];
15296
+ var DEVELOPMENT_FALLBACK_ORIGINS = [
15297
+ "http://localhost:3000",
15298
+ "http://localhost:3001",
15299
+ "http://localhost:5173",
15300
+ "http://localhost:8080",
15301
+ "http://127.0.0.1:3000",
15302
+ "http://127.0.0.1:3001",
15303
+ "http://127.0.0.1:5173",
15304
+ "http://127.0.0.1:8080"
15305
+ ];
15306
+ function normalizeOrigin(value) {
15307
+ const trimmed = value.trim();
15308
+ if (!trimmed || trimmed === "*" || trimmed.toLowerCase() === "null") return null;
15309
+ try {
15310
+ const url = new URL(trimmed);
15311
+ if (url.protocol !== "https:" && url.protocol !== "http:") return null;
15312
+ if (!url.hostname) return null;
15313
+ return url.origin;
15314
+ } catch {
15315
+ return null;
14252
15316
  }
14253
- const issuer = opts.issuerUrl.replace(/\/+$/, "");
14254
- params.push(`resource_metadata="${issuer}/.well-known/oauth-protected-resource"`);
14255
- return `Bearer ${params.join(", ")}`;
15317
+ }
15318
+ function parseAllowedOrigins(raw) {
15319
+ const origins = /* @__PURE__ */ new Set();
15320
+ for (const entry of (raw ?? "").split(",")) {
15321
+ const normalized = normalizeOrigin(entry);
15322
+ if (normalized) origins.add(normalized);
15323
+ }
15324
+ return origins;
15325
+ }
15326
+ function buildOriginPolicy(input) {
15327
+ const envOrigins = parseAllowedOrigins(input.allowedOriginsEnv);
15328
+ if (envOrigins.size > 0) {
15329
+ return { allowedOrigins: envOrigins, source: "env" };
15330
+ }
15331
+ const fallback = new Set(PRODUCTION_FALLBACK_ORIGINS);
15332
+ for (const configuredUrl of input.configuredUrls ?? []) {
15333
+ const normalized = normalizeOrigin(configuredUrl);
15334
+ if (normalized) fallback.add(normalized);
15335
+ }
15336
+ if (input.nodeEnv !== "production") {
15337
+ for (const origin of DEVELOPMENT_FALLBACK_ORIGINS) fallback.add(origin);
15338
+ }
15339
+ return { allowedOrigins: fallback, source: "fallback" };
15340
+ }
15341
+ function validateBrowserOrigin(originHeader, policy) {
15342
+ if (originHeader === void 0) return { allowed: true, origin: null };
15343
+ if (Array.isArray(originHeader)) return { allowed: false, reason: "invalid_origin" };
15344
+ const normalized = normalizeOrigin(originHeader);
15345
+ if (!normalized || !policy.allowedOrigins.has(normalized)) {
15346
+ return { allowed: false, reason: "invalid_origin" };
15347
+ }
15348
+ return { allowed: true, origin: normalized };
14256
15349
  }
14257
15350
 
14258
15351
  // src/http.ts
15352
+ process.env.MCP_TRANSPORT = "http";
14259
15353
  var PORT = parseInt(process.env.PORT ?? "8080", 10);
14260
15354
  var SUPABASE_URL2 = process.env.SUPABASE_URL ?? "";
14261
15355
  var SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY ?? "";
14262
15356
  var MCP_SERVER_URL = process.env.MCP_SERVER_URL ?? `http://localhost:${PORT}/mcp`;
14263
15357
  var APP_BASE_URL = process.env.APP_BASE_URL ?? "https://www.socialneuron.com";
14264
15358
  var NODE_ENV = process.env.NODE_ENV ?? "development";
15359
+ var ORIGIN_POLICY = buildOriginPolicy({
15360
+ allowedOriginsEnv: process.env.ALLOWED_ORIGINS,
15361
+ configuredUrls: [APP_BASE_URL, MCP_SERVER_URL],
15362
+ nodeEnv: NODE_ENV
15363
+ });
14265
15364
  function deriveOAuthIssuerUrl() {
14266
15365
  if (process.env.OAUTH_ISSUER_URL) {
14267
15366
  return process.env.OAUTH_ISSUER_URL;
@@ -14335,7 +15434,9 @@ var cleanupInterval = setInterval(
14335
15434
  );
14336
15435
  var app = express();
14337
15436
  app.disable("x-powered-by");
14338
- app.use(express.json());
15437
+ var defaultJsonParser = express.json({ limit: "100kb" });
15438
+ var authenticatedMcpJsonParser = express.json({ limit: "16mb" });
15439
+ var unauthenticatedMcpJsonParser = express.json({ limit: "100kb" });
14339
15440
  app.set("trust proxy", 1);
14340
15441
  var ipBuckets = /* @__PURE__ */ new Map();
14341
15442
  var IP_RATE_MAX = 60;
@@ -14378,20 +15479,35 @@ app.use((_req, res, next) => {
14378
15479
  res.setHeader("X-Content-Type-Options", "nosniff");
14379
15480
  next();
14380
15481
  });
14381
- app.use((_req, res, next) => {
14382
- res.setHeader("Access-Control-Allow-Origin", "*");
15482
+ app.use((req, res, next) => {
15483
+ const originCheck = validateBrowserOrigin(req.headers.origin, ORIGIN_POLICY);
15484
+ if (!originCheck.allowed) {
15485
+ res.status(403).json({
15486
+ error: "invalid_origin",
15487
+ error_description: "Request Origin is not allowed."
15488
+ });
15489
+ return;
15490
+ }
15491
+ if (originCheck.origin) {
15492
+ res.setHeader("Access-Control-Allow-Origin", originCheck.origin);
15493
+ res.setHeader("Vary", "Origin");
15494
+ }
14383
15495
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
14384
15496
  res.setHeader(
14385
15497
  "Access-Control-Allow-Headers",
14386
15498
  "Content-Type, Authorization, Mcp-Session-Id, MCP-Protocol-Version"
14387
15499
  );
14388
15500
  res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id, WWW-Authenticate");
14389
- if (_req.method === "OPTIONS") {
15501
+ if (req.method === "OPTIONS") {
14390
15502
  res.status(204).end();
14391
15503
  return;
14392
15504
  }
14393
15505
  next();
14394
15506
  });
15507
+ app.use((req, res, next) => {
15508
+ if (req.path === "/mcp") return next();
15509
+ defaultJsonParser(req, res, next);
15510
+ });
14395
15511
  var oauthProvider = createOAuthProvider({
14396
15512
  supabaseUrl: SUPABASE_URL2,
14397
15513
  supabaseAnonKey: SUPABASE_ANON_KEY,
@@ -14429,6 +15545,26 @@ var authRouter = mcpAuthRouter({
14429
15545
  serviceDocumentationUrl: new URL("https://socialneuron.com/for-developers"),
14430
15546
  scopesSupported: SCOPES_SUPPORTED
14431
15547
  });
15548
+ function normalizeOAuthResourceParam(value) {
15549
+ const values = Array.isArray(value) ? value : [value];
15550
+ const strings = values.filter(
15551
+ (item) => typeof item === "string" && item.length > 0
15552
+ );
15553
+ const issuer = OAUTH_ISSUER_URL.replace(/\/$/, "");
15554
+ return strings.find((item) => item.replace(/\/$/, "") === issuer) ?? strings[0];
15555
+ }
15556
+ app.use((req, _res, next) => {
15557
+ if ((req.path === "/authorize" || req.path === "/token") && Array.isArray(req.query.resource)) {
15558
+ const normalized = normalizeOAuthResourceParam(req.query.resource);
15559
+ if (normalized) {
15560
+ const normalizedUrl = new URL(req.originalUrl, OAUTH_ISSUER_URL);
15561
+ normalizedUrl.searchParams.delete("resource");
15562
+ normalizedUrl.searchParams.set("resource", normalized);
15563
+ req.url = `${normalizedUrl.pathname}${normalizedUrl.search}`;
15564
+ }
15565
+ }
15566
+ next();
15567
+ });
14432
15568
  app.use((req, res, next) => {
14433
15569
  authRouter(req, res, (err) => {
14434
15570
  if (err) {
@@ -14477,6 +15613,16 @@ async function authenticateRequest(req, res, next) {
14477
15613
  });
14478
15614
  }
14479
15615
  }
15616
+ app.use("/mcp", (req, res, next) => {
15617
+ if (req.method !== "POST") return next();
15618
+ if (!req.headers.authorization?.startsWith("Bearer ")) return next();
15619
+ authenticateRequest(req, res, next);
15620
+ });
15621
+ app.use("/mcp", (req, res, next) => {
15622
+ if (req.method !== "POST") return next();
15623
+ const parser = req.auth ? authenticatedMcpJsonParser : unauthenticatedMcpJsonParser;
15624
+ parser(req, res, next);
15625
+ });
14480
15626
  app.get("/.well-known/mcp/server-card.json", (_req, res) => {
14481
15627
  res.json({
14482
15628
  serverInfo: {
@@ -14487,8 +15633,8 @@ app.get("/.well-known/mcp/server-card.json", (_req, res) => {
14487
15633
  required: true,
14488
15634
  schemes: ["oauth2"]
14489
15635
  },
14490
- toolCount: TOOL_CATALOG.length,
14491
- tools: TOOL_CATALOG.map((t) => ({
15636
+ toolCount: TOOL_CATALOG.filter((t) => !t.localOnly).length,
15637
+ tools: TOOL_CATALOG.filter((t) => !t.localOnly).map((t) => ({
14492
15638
  name: t.name,
14493
15639
  description: t.description,
14494
15640
  module: t.module,
@@ -14566,19 +15712,32 @@ app.post("/mcp", (req, res, next) => {
14566
15712
  const body = req.body;
14567
15713
  if (body?.jsonrpc !== "2.0") return next();
14568
15714
  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
- }
15715
+ const hasBearer = req.headers.authorization?.startsWith("Bearer ");
15716
+ if (hasBearer) {
15717
+ next();
15718
+ return;
15719
+ }
15720
+ buildDiscoveryCatalog().then((tools) => {
15721
+ res.json({ jsonrpc: "2.0", id: body.id ?? null, result: { tools } });
15722
+ }).catch(() => {
15723
+ res.json({
15724
+ jsonrpc: "2.0",
15725
+ id: body.id ?? null,
15726
+ result: {
15727
+ tools: TOOL_CATALOG.map((t) => ({
15728
+ name: t.name,
15729
+ description: t.description,
15730
+ inputSchema: { type: "object", properties: {} }
15731
+ }))
15732
+ }
15733
+ });
14579
15734
  });
14580
15735
  return;
14581
15736
  }
15737
+ if (req.auth) {
15738
+ next();
15739
+ return;
15740
+ }
14582
15741
  authenticateRequest(req, res, next);
14583
15742
  });
14584
15743
  app.post("/mcp", async (req, res) => {
@@ -14631,7 +15790,7 @@ app.post("/mcp", async (req, res) => {
14631
15790
  });
14632
15791
  return;
14633
15792
  }
14634
- const server = new McpServer({
15793
+ const server = new McpServer2({
14635
15794
  name: "socialneuron",
14636
15795
  version: MCP_VERSION
14637
15796
  });
@@ -14717,9 +15876,17 @@ app.delete("/mcp", authenticateRequest, async (req, res) => {
14717
15876
  });
14718
15877
  app.use((err, _req, res, _next) => {
14719
15878
  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) });
15879
+ if (res.headersSent) return;
15880
+ const e = err;
15881
+ const status = e.status ?? e.statusCode;
15882
+ if (status === 413 || e.type === "entity.too.large") {
15883
+ res.status(413).json({
15884
+ error: "payload_too_large",
15885
+ error_description: "Request body exceeds the allowed JSON limit."
15886
+ });
15887
+ return;
14722
15888
  }
15889
+ res.status(500).json({ error: "internal_error", error_description: sanitizeError(err) });
14723
15890
  });
14724
15891
  var httpServer = app.listen(PORT, "0.0.0.0", () => {
14725
15892
  console.log(`[MCP HTTP] Social Neuron MCP Server listening on 0.0.0.0:${PORT}`);