@frontmcp/sdk 1.8.6 → 1.8.7

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.
Files changed (75) hide show
  1. package/agent/flows/call-agent.flow.d.ts.map +1 -1
  2. package/auth/instances/instance.local-primary-auth.d.ts.map +1 -1
  3. package/common/dynamic/dynamic.adapter.d.ts +1 -1
  4. package/common/dynamic/dynamic.adapter.d.ts.map +1 -1
  5. package/common/dynamic/dynamic.plugin.d.ts +7 -1
  6. package/common/dynamic/dynamic.plugin.d.ts.map +1 -1
  7. package/common/dynamic/dynamic.utils.d.ts +10 -1
  8. package/common/dynamic/dynamic.utils.d.ts.map +1 -1
  9. package/common/interfaces/execution-context.interface.d.ts +14 -0
  10. package/common/interfaces/execution-context.interface.d.ts.map +1 -1
  11. package/common/metadata/front-mcp.metadata.d.ts +127 -19
  12. package/common/metadata/front-mcp.metadata.d.ts.map +1 -1
  13. package/common/types/options/http/index.d.ts +1 -1
  14. package/common/types/options/http/index.d.ts.map +1 -1
  15. package/common/types/options/http/interfaces.d.ts +36 -1
  16. package/common/types/options/http/interfaces.d.ts.map +1 -1
  17. package/common/types/options/http/schema.d.ts +12 -0
  18. package/common/types/options/http/schema.d.ts.map +1 -1
  19. package/common/types/options/transport/schema.d.ts +2 -2
  20. package/context/frontmcp-context-storage.d.ts.map +1 -1
  21. package/context/frontmcp-context.d.ts +11 -0
  22. package/context/frontmcp-context.d.ts.map +1 -1
  23. package/elicitation/store/elicitation-store.factory.d.ts.map +1 -1
  24. package/errors/misconfiguration.d.ts +30 -0
  25. package/errors/misconfiguration.d.ts.map +1 -0
  26. package/esm/index.mjs +912 -198
  27. package/flows/flow.instance.d.ts.map +1 -1
  28. package/front-mcp/front-mcp.d.ts.map +1 -1
  29. package/ha/ha-headers.d.ts +16 -0
  30. package/ha/ha-headers.d.ts.map +1 -0
  31. package/health/health.probes.d.ts +1 -1
  32. package/health/health.probes.d.ts.map +1 -1
  33. package/index.js +1016 -298
  34. package/job/store/job-definition-store.factory.d.ts.map +1 -1
  35. package/job/store/job-state-store.factory.d.ts.map +1 -1
  36. package/notification/notification.service.d.ts +4 -1
  37. package/notification/notification.service.d.ts.map +1 -1
  38. package/package.json +10 -10
  39. package/remote-mcp/mcp-stateless-client.adapter.d.ts +9 -1
  40. package/remote-mcp/mcp-stateless-client.adapter.d.ts.map +1 -1
  41. package/remote-mcp/resilience/health-check.d.ts.map +1 -1
  42. package/scope/scope.instance.d.ts +8 -0
  43. package/scope/scope.instance.d.ts.map +1 -1
  44. package/server/adapters/express.host.adapter.d.ts +6 -1
  45. package/server/adapters/express.host.adapter.d.ts.map +1 -1
  46. package/server/middleware/csp.middleware.d.ts +12 -1
  47. package/server/middleware/csp.middleware.d.ts.map +1 -1
  48. package/server/security/security-audit.d.ts.map +1 -1
  49. package/server/server.instance.d.ts.map +1 -1
  50. package/skill/cache/skill-http-cache.factory.d.ts +0 -5
  51. package/skill/cache/skill-http-cache.factory.d.ts.map +1 -1
  52. package/task/store/index.d.ts +1 -1
  53. package/task/store/index.d.ts.map +1 -1
  54. package/task/store/task-store.factory.d.ts +6 -0
  55. package/task/store/task-store.factory.d.ts.map +1 -1
  56. package/tool/flows/call-tool.flow.d.ts.map +1 -1
  57. package/tool/ui/ui-option-warnings.d.ts +3 -0
  58. package/tool/ui/ui-option-warnings.d.ts.map +1 -0
  59. package/tool/ui/ui-resource.handler.d.ts.map +1 -1
  60. package/tool/ui/ui-shared.d.ts +14 -0
  61. package/tool/ui/ui-shared.d.ts.map +1 -1
  62. package/transport/event-stores/redis.event-store.d.ts.map +1 -1
  63. package/transport/flows/handle.mcp-20260728.flow.d.ts.map +1 -1
  64. package/transport/flows/handle.sse.flow.d.ts.map +1 -1
  65. package/transport/flows/handle.stateless-http.flow.d.ts +2 -1
  66. package/transport/flows/handle.stateless-http.flow.d.ts.map +1 -1
  67. package/transport/flows/handle.streamable-http.flow.d.ts +2 -1
  68. package/transport/flows/handle.streamable-http.flow.d.ts.map +1 -1
  69. package/transport/ipc-server.transport.d.ts +26 -0
  70. package/transport/ipc-server.transport.d.ts.map +1 -0
  71. package/transport/mcp-20260728/client/mcp-stateless.client.d.ts +2 -0
  72. package/transport/mcp-20260728/client/mcp-stateless.client.d.ts.map +1 -1
  73. package/transport/transport.registry.d.ts +34 -0
  74. package/transport/transport.registry.d.ts.map +1 -1
  75. package/transport/web-fetch-handler.d.ts.map +1 -1
package/esm/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
- import { createRequire as __frontmcpCreateRequire } from 'module';
2
- const require = __frontmcpCreateRequire(import.meta.url || 'file:///');
1
+ const __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
3
4
  var __defProp = Object.defineProperty;
4
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -1861,7 +1862,7 @@ var init_session = __esm({
1861
1862
 
1862
1863
  // libs/sdk/src/common/types/options/http/schema.ts
1863
1864
  import { z as z8 } from "@frontmcp/lazy-zod";
1864
- var HTTP_METHODS, httpRouteSchema, bodyParserLimitSchema, corsOptionsSchema, httpOptionsSchema;
1865
+ var HTTP_METHODS, httpRouteSchema, bodyParserLimitSchema, corsOptionsSchema, securityHeaderValueSchema, securityHeadersSchema, httpOptionsSchema;
1865
1866
  var init_schema3 = __esm({
1866
1867
  "libs/sdk/src/common/types/options/http/schema.ts"() {
1867
1868
  "use strict";
@@ -1898,6 +1899,19 @@ var init_schema3 = __esm({
1898
1899
  credentials: z8.boolean().optional(),
1899
1900
  maxAge: z8.number().optional()
1900
1901
  });
1902
+ securityHeaderValueSchema = z8.union([z8.string(), z8.literal(false)]);
1903
+ securityHeadersSchema = z8.object({
1904
+ hsts: securityHeaderValueSchema.optional(),
1905
+ contentTypeOptions: securityHeaderValueSchema.optional(),
1906
+ frameOptions: securityHeaderValueSchema.optional(),
1907
+ custom: z8.record(z8.string(), z8.string()).optional(),
1908
+ csp: z8.object({
1909
+ enabled: z8.boolean().optional(),
1910
+ directives: z8.record(z8.string(), z8.union([z8.string(), z8.array(z8.string())])).optional(),
1911
+ reportUri: z8.string().optional(),
1912
+ reportOnly: z8.boolean().optional()
1913
+ }).optional()
1914
+ });
1901
1915
  httpOptionsSchema = z8.object({
1902
1916
  port: z8.number().optional().default(Number(process.env["PORT"]) || 3e3),
1903
1917
  // `FRONTMCP_HTTP_ENTRY_PATH` lets `frontmcp dev` propagate the configured
@@ -1956,7 +1970,9 @@ var init_schema3 = __esm({
1956
1970
  * app as the MCP endpoint; public by default, `auth: true` opts into the MCP
1957
1971
  * `session:verify` flow. Reserved-path collisions are rejected at startup.
1958
1972
  */
1959
- routes: z8.array(httpRouteSchema).optional()
1973
+ routes: z8.array(httpRouteSchema).optional(),
1974
+ /** Response security headers; see `SecurityHeadersOptions`. */
1975
+ securityHeaders: securityHeadersSchema.optional()
1960
1976
  });
1961
1977
  }
1962
1978
  });
@@ -2394,7 +2410,7 @@ var init_schema7 = __esm({
2394
2410
  persistenceConfigSchema = z12.object({
2395
2411
  redis: redisOptionsSchema.optional(),
2396
2412
  sqlite: sqliteOptionsSchema.optional(),
2397
- defaultTtlMs: z12.number().int().positive().default(36e5)
2413
+ defaultTtlMs: z12.number().int().positive().optional()
2398
2414
  }).refine((cfg) => !(cfg.redis && cfg.sqlite), {
2399
2415
  message: "Invalid persistence config: choose either redis or sqlite, not both",
2400
2416
  path: ["sqlite"]
@@ -6510,6 +6526,7 @@ var require_dist = __commonJS({
6510
6526
  var __toCommonJS2 = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod);
6511
6527
  var index_exports = {};
6512
6528
  __export2(index_exports, {
6529
+ SqliteDecryptionError: () => SqliteDecryptionError,
6513
6530
  SqliteElicitationStore: () => SqliteElicitationStore,
6514
6531
  SqliteEventStore: () => SqliteEventStore,
6515
6532
  SqliteKvStore: () => SqliteKvStore,
@@ -6521,34 +6538,47 @@ var require_dist = __commonJS({
6521
6538
  encryptValue: () => encryptValue,
6522
6539
  loadBetterSqlite3: () => loadBetterSqlite3,
6523
6540
  openDatabase: () => openDatabase,
6524
- sqliteStorageOptionsSchema: () => sqliteStorageOptionsSchema
6541
+ sqliteStorageOptionsSchema: () => sqliteStorageOptionsSchema,
6542
+ withBusyRetry: () => withBusyRetry
6525
6543
  });
6526
6544
  module.exports = __toCommonJS2(index_exports);
6527
- var import_utils114 = __require("@frontmcp/utils");
6545
+ var import_utils119 = __require("@frontmcp/utils");
6546
+ var SqliteDecryptionError = class extends Error {
6547
+ code = "SQLITE_DECRYPTION_FAILED";
6548
+ constructor() {
6549
+ super(
6550
+ "SqliteKvStore: failed to decrypt a stored value. The encryption secret differs from the one the database was written with (or the value is corrupted). Restore the original secret, or delete the database file to start empty."
6551
+ );
6552
+ this.name = "SqliteDecryptionError";
6553
+ }
6554
+ };
6528
6555
  var HKDF_SALT = new TextEncoder().encode("frontmcp-sqlite-storage-v1");
6529
6556
  var HKDF_INFO = new TextEncoder().encode("aes-256-gcm-value-encryption");
6530
6557
  var KEY_LENGTH = 32;
6531
6558
  function deriveEncryptionKey(secret) {
6532
6559
  const ikm = new TextEncoder().encode(secret);
6533
- return (0, import_utils114.hkdfSha256)(ikm, HKDF_SALT, HKDF_INFO, KEY_LENGTH);
6560
+ return (0, import_utils119.hkdfSha256)(ikm, HKDF_SALT, HKDF_INFO, KEY_LENGTH);
6534
6561
  }
6535
6562
  var SEPARATOR = ":";
6536
6563
  function encryptValue(key, plaintext) {
6537
- const iv = (0, import_utils114.randomBytes)(12);
6564
+ const iv = (0, import_utils119.randomBytes)(12);
6538
6565
  const plaintextBytes = new TextEncoder().encode(plaintext);
6539
- const { ciphertext, tag } = (0, import_utils114.encryptAesGcm)(key, plaintextBytes, iv);
6540
- return [(0, import_utils114.base64urlEncode)(iv), (0, import_utils114.base64urlEncode)(tag), (0, import_utils114.base64urlEncode)(ciphertext)].join(SEPARATOR);
6566
+ const { ciphertext, tag } = (0, import_utils119.encryptAesGcm)(key, plaintextBytes, iv);
6567
+ return [(0, import_utils119.base64urlEncode)(iv), (0, import_utils119.base64urlEncode)(tag), (0, import_utils119.base64urlEncode)(ciphertext)].join(SEPARATOR);
6541
6568
  }
6542
6569
  function decryptValue(key, encrypted) {
6543
6570
  const parts = encrypted.split(SEPARATOR);
6544
6571
  if (parts.length !== 3) {
6545
6572
  throw new Error("Invalid encrypted value format");
6546
6573
  }
6547
- const iv = (0, import_utils114.base64urlDecode)(parts[0]);
6548
- const tag = (0, import_utils114.base64urlDecode)(parts[1]);
6549
- const ciphertext = (0, import_utils114.base64urlDecode)(parts[2]);
6550
- const plaintext = (0, import_utils114.decryptAesGcm)(key, ciphertext, iv, tag);
6551
- return new TextDecoder().decode(plaintext);
6574
+ const iv = (0, import_utils119.base64urlDecode)(parts[0]);
6575
+ const tag = (0, import_utils119.base64urlDecode)(parts[1]);
6576
+ const ciphertext = (0, import_utils119.base64urlDecode)(parts[2]);
6577
+ try {
6578
+ return new TextDecoder().decode((0, import_utils119.decryptAesGcm)(key, ciphertext, iv, tag));
6579
+ } catch {
6580
+ throw new SqliteDecryptionError();
6581
+ }
6552
6582
  }
6553
6583
  var import_utils210 = __require("@frontmcp/utils");
6554
6584
  var cached = null;
@@ -6561,7 +6591,27 @@ var require_dist = __commonJS({
6561
6591
  return mod;
6562
6592
  }
6563
6593
  var MEMORY_PATH = ":memory:";
6564
- function openDatabase(path3, storeName) {
6594
+ var DEFAULT_BUSY_TIMEOUT_MS = 5e3;
6595
+ function isBusyError(err) {
6596
+ const code = err?.code;
6597
+ return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_RECOVERY" || code === "SQLITE_BUSY_SNAPSHOT";
6598
+ }
6599
+ function sleepSync(ms) {
6600
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
6601
+ }
6602
+ function withBusyRetry(operation, options = {}) {
6603
+ const attempts = options.attempts ?? 5;
6604
+ const delayMs = options.delayMs ?? 50;
6605
+ for (let attempt = 1; ; attempt++) {
6606
+ try {
6607
+ return operation();
6608
+ } catch (err) {
6609
+ if (!isBusyError(err) || attempt >= attempts) throw err;
6610
+ sleepSync(delayMs * attempt);
6611
+ }
6612
+ }
6613
+ }
6614
+ function openDatabase(path3, storeName, options = {}) {
6565
6615
  const BetterSqlite3 = loadBetterSqlite3();
6566
6616
  if (path3 !== MEMORY_PATH && path3 !== "") {
6567
6617
  const dir = (0, import_utils210.dirname)(path3);
@@ -6570,16 +6620,28 @@ var require_dist = __commonJS({
6570
6620
  (0, import_utils210.ensureDirSync)(dir);
6571
6621
  } catch (err) {
6572
6622
  const message = err instanceof Error ? err.message : String(err);
6573
- throw new Error(`${storeName}: failed to create directory "${dir}" for database "${path3}": ${message}`);
6623
+ throw new Error(`${storeName}: failed to create directory "${dir}" for database "${path3}": ${message}`, {
6624
+ cause: err
6625
+ });
6574
6626
  }
6575
6627
  }
6576
6628
  }
6629
+ let db;
6630
+ try {
6631
+ db = new BetterSqlite3(path3);
6632
+ } catch (err) {
6633
+ const message = err instanceof Error ? err.message : String(err);
6634
+ throw new Error(`${storeName}: failed to open database at "${path3}": ${message}`, { cause: err });
6635
+ }
6577
6636
  try {
6578
- return new BetterSqlite3(path3);
6637
+ db.pragma(`busy_timeout = ${Math.max(0, Math.floor(options.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS))}`);
6638
+ if (options.walMode) withBusyRetry(() => db.pragma("journal_mode = WAL"));
6579
6639
  } catch (err) {
6640
+ db.close();
6580
6641
  const message = err instanceof Error ? err.message : String(err);
6581
- throw new Error(`${storeName}: failed to open database at "${path3}": ${message}`);
6642
+ throw new Error(`${storeName}: failed to configure database at "${path3}": ${message}`, { cause: err });
6582
6643
  }
6644
+ return db;
6583
6645
  }
6584
6646
  var SqliteKvStore = class {
6585
6647
  db;
@@ -6587,14 +6649,14 @@ var require_dist = __commonJS({
6587
6649
  cleanupTimer = null;
6588
6650
  stmts = null;
6589
6651
  constructor(options) {
6590
- this.db = openDatabase(options.path, "SqliteKvStore");
6591
- if (options.walMode !== false) {
6592
- this.db.pragma("journal_mode = WAL");
6593
- }
6652
+ this.db = openDatabase(options.path, "SqliteKvStore", {
6653
+ busyTimeoutMs: options.busyTimeoutMs,
6654
+ walMode: options.walMode !== false
6655
+ });
6594
6656
  if (options.encryption?.secret) {
6595
6657
  this.encryptionKey = deriveEncryptionKey(options.encryption.secret);
6596
6658
  }
6597
- this.initSchema();
6659
+ withBusyRetry(() => this.initSchema());
6598
6660
  this.prepareStatements();
6599
6661
  const cleanupInterval = options.ttlCleanupIntervalMs ?? 6e4;
6600
6662
  if (cleanupInterval > 0) {
@@ -7053,17 +7115,17 @@ var require_dist = __commonJS({
7053
7115
  encryptionKey = null;
7054
7116
  stmts = null;
7055
7117
  constructor(options) {
7056
- this.db = openDatabase(options.path, "SqliteTaskStore");
7057
- if (options.walMode !== false) {
7058
- this.db.pragma("journal_mode = WAL");
7059
- }
7118
+ this.db = openDatabase(options.path, "SqliteTaskStore", {
7119
+ busyTimeoutMs: options.busyTimeoutMs,
7120
+ walMode: options.walMode !== false
7121
+ });
7060
7122
  this.logger = options.logger;
7061
7123
  this.liveness = options.livenessProbe ?? defaultLivenessProbe;
7062
7124
  if (options.encryption?.secret) {
7063
7125
  this.encryptionKey = deriveEncryptionKey(options.encryption.secret);
7064
7126
  }
7065
7127
  this.emitter.setMaxListeners(200);
7066
- this.initSchema();
7128
+ withBusyRetry(() => this.initSchema());
7067
7129
  this.prepareStatements();
7068
7130
  const cleanupInterval = options.ttlCleanupIntervalMs ?? 6e4;
7069
7131
  if (cleanupInterval > 0) {
@@ -7300,13 +7362,13 @@ var require_dist = __commonJS({
7300
7362
  cleanupTimer = null;
7301
7363
  stmts = null;
7302
7364
  constructor(options) {
7303
- this.db = openDatabase(options.path, "SqliteEventStore");
7304
- if (options.walMode !== false) {
7305
- this.db.pragma("journal_mode = WAL");
7306
- }
7365
+ this.db = openDatabase(options.path, "SqliteEventStore", {
7366
+ busyTimeoutMs: options.busyTimeoutMs,
7367
+ walMode: options.walMode !== false
7368
+ });
7307
7369
  this.maxEvents = options.maxEvents ?? 1e4;
7308
7370
  this.ttlMs = options.ttlMs ?? 3e5;
7309
- this.initSchema();
7371
+ withBusyRetry(() => this.initSchema());
7310
7372
  this.prepareStatements();
7311
7373
  const cleanupInterval = options.ttlCleanupIntervalMs ?? 6e4;
7312
7374
  if (cleanupInterval > 0) {
@@ -7441,7 +7503,8 @@ var require_dist = __commonJS({
7441
7503
  secret: import_lazy_zod113.z.string().min(1)
7442
7504
  }).optional(),
7443
7505
  ttlCleanupIntervalMs: import_lazy_zod113.z.number().int().nonnegative().optional().default(6e4),
7444
- walMode: import_lazy_zod113.z.boolean().optional().default(true)
7506
+ walMode: import_lazy_zod113.z.boolean().optional().default(true),
7507
+ busyTimeoutMs: import_lazy_zod113.z.number().int().nonnegative().optional().default(5e3)
7445
7508
  });
7446
7509
  }
7447
7510
  });
@@ -7460,6 +7523,14 @@ function detectStorageType(storage, configuredType) {
7460
7523
  if (getEnv("REDIS_URL") || getEnv("REDIS_HOST")) return "redis";
7461
7524
  return "memory";
7462
7525
  }
7526
+ function isExplicitStorage(storage) {
7527
+ return Boolean(storage?.type && storage.type !== "auto");
7528
+ }
7529
+ function resolvesToVercelKvTaskBackend(redis, storage) {
7530
+ if (isExplicitStorage(storage)) return storage.type === "vercel-kv";
7531
+ if (redis) return "provider" in redis && redis.provider === "vercel-kv";
7532
+ return Boolean(getEnv("KV_REST_API_URL"));
7533
+ }
7463
7534
  async function createTaskStore(options = {}) {
7464
7535
  const { storage: storageConfig, redis, keyPrefix = "mcp:task:", logger, isEdgeRuntime: isEdgeRuntime3 = false, sqlite } = options;
7465
7536
  if (isEdgeRuntime3 && sqlite) {
@@ -7484,7 +7555,7 @@ async function createTaskStore(options = {}) {
7484
7555
  return { store: store2, type: "sqlite" };
7485
7556
  }
7486
7557
  let finalStorageConfig = storageConfig;
7487
- if (redis && !storageConfig) {
7558
+ if (redis && !isExplicitStorage(storageConfig)) {
7488
7559
  if ("provider" in redis && redis.provider === "vercel-kv") {
7489
7560
  throw new TaskStoreNotSupportedError(
7490
7561
  "Vercel KV is not supported for task stores. Task result blocking and cancel signalling require pub/sub. Use Redis or Upstash instead."
@@ -7512,7 +7583,8 @@ async function createTaskStore(options = {}) {
7512
7583
  };
7513
7584
  }
7514
7585
  }
7515
- if (finalStorageConfig?.type === "vercel-kv" || getEnv("KV_REST_API_URL")) {
7586
+ const explicitBackend = isExplicitStorage(storageConfig) || Boolean(redis);
7587
+ if (finalStorageConfig?.type === "vercel-kv" || !explicitBackend && getEnv("KV_REST_API_URL")) {
7516
7588
  throw new TaskStoreNotSupportedError(
7517
7589
  "Vercel KV is not supported for task stores (pub/sub required). Use Redis or Upstash."
7518
7590
  );
@@ -8753,11 +8825,8 @@ import {
8753
8825
  } from "@frontmcp/protocol";
8754
8826
  import { awaitOutsideRequest } from "@frontmcp/utils";
8755
8827
  function hasMcpAppsExtension(capabilities) {
8756
- if (!capabilities?.experimental) {
8757
- return false;
8758
- }
8759
- const uiExtension = capabilities.experimental[MCP_APPS_EXTENSION_KEY];
8760
- return uiExtension !== void 0 && uiExtension !== null;
8828
+ const declared = capabilities?.experimental?.[MCP_APPS_EXTENSION_KEY] ?? capabilities?.extensions?.[MCP_APPS_EXTENSION_KEY];
8829
+ return declared !== void 0 && declared !== null;
8761
8830
  }
8762
8831
  function detectPlatformFromCapabilities(capabilities) {
8763
8832
  if (hasMcpAppsExtension(capabilities)) {
@@ -11364,6 +11433,8 @@ var init_ui_shared = __esm({
11364
11433
  widgets = /* @__PURE__ */ new Map();
11365
11434
  manifests = /* @__PURE__ */ new Map();
11366
11435
  resourceMeta = /* @__PURE__ */ new Map();
11436
+ uiTools = /* @__PURE__ */ new Set();
11437
+ customUris = /* @__PURE__ */ new Map();
11367
11438
  resolver;
11368
11439
  options;
11369
11440
  constructor(resolver, options = {}) {
@@ -11378,6 +11449,28 @@ var init_ui_shared = __esm({
11378
11449
  logger: this.options.logger
11379
11450
  };
11380
11451
  }
11452
+ /**
11453
+ * Record that `toolName` has a UI, and the custom `ui.resourceUri` it advertises (if any).
11454
+ * `tools/list` advertises that URI verbatim, so `resources/read` must resolve it back to the
11455
+ * tool; and only registered tools may be served a placeholder widget.
11456
+ */
11457
+ registerTool(toolName, resourceUri) {
11458
+ this.uiTools.add(toolName);
11459
+ if (resourceUri) this.customUris.set(resourceUri, toolName);
11460
+ }
11461
+ /** The tool that advertised `uri` as its custom `ui.resourceUri`. */
11462
+ resolveCustomUri(uri) {
11463
+ return this.customUris.get(uri);
11464
+ }
11465
+ /**
11466
+ * Whether `name` (or its app-qualified form `app:name`) is a tool with a UI.
11467
+ */
11468
+ hasTool(name44) {
11469
+ const known = (n) => this.uiTools.has(n) || this.widgets.has(n) || this.resourceMeta.has(n);
11470
+ if (known(name44)) return true;
11471
+ const sep = name44.indexOf(":");
11472
+ return sep > 0 && known(name44.slice(sep + 1));
11473
+ }
11381
11474
  getStaticWidget(name44) {
11382
11475
  return this.widgets.get(name44);
11383
11476
  }
@@ -11406,8 +11499,9 @@ var init_ui_shared = __esm({
11406
11499
  const output = options["output"] ?? {};
11407
11500
  const uiConfig = options["uiConfig"];
11408
11501
  const resourceMode = uiConfig?.["resourceMode"] ?? options["resourceMode"];
11409
- if (!toolName || !template) return;
11502
+ if (!toolName) return;
11410
11503
  this.updateResourceMetaFromConfig(toolName, uiConfig);
11504
+ if (!template) return;
11411
11505
  const result = renderToolTemplate({
11412
11506
  toolName,
11413
11507
  input,
@@ -11416,6 +11510,7 @@ var init_ui_shared = __esm({
11416
11510
  resolver: this.resolver,
11417
11511
  resourceMode,
11418
11512
  sizing: extractSizing(uiConfig),
11513
+ csp: uiConfig?.["csp"],
11419
11514
  ...this.stringResultOptions(uiConfig)
11420
11515
  });
11421
11516
  this.widgets.set(toolName, result.html);
@@ -11494,6 +11589,7 @@ var init_ui_shared = __esm({
11494
11589
  resolver: this.resolver,
11495
11590
  resourceMode,
11496
11591
  sizing: extractSizing(uiConfig),
11592
+ csp: uiConfig?.["csp"],
11497
11593
  ...this.stringResultOptions(uiConfig)
11498
11594
  });
11499
11595
  return { meta: result.meta };
@@ -11546,26 +11642,14 @@ function handleUIResourceRead(uri, registry, platformType) {
11546
11642
  return { handled: false };
11547
11643
  }
11548
11644
  const mimeType = getUIResourceMimeType(platformType);
11549
- const widgetParsed = parseWidgetUri(uri);
11645
+ const customTool = registry.resolveCustomUri(uri);
11646
+ const widgetParsed = customTool ? { toolName: customTool } : parseWidgetUri(uri);
11550
11647
  if (widgetParsed) {
11551
- const resourceMeta = buildResourceMetaForWidget(registry, widgetParsed.toolName);
11552
- const cachedWidget = registry.getStaticWidget(widgetParsed.toolName);
11553
- if (cachedWidget) {
11554
- return {
11555
- handled: true,
11556
- result: {
11557
- contents: [
11558
- {
11559
- uri,
11560
- mimeType,
11561
- text: cachedWidget,
11562
- ...resourceMeta ? { _meta: resourceMeta } : {}
11563
- }
11564
- ]
11565
- }
11566
- };
11648
+ if (!registry.hasTool(widgetParsed.toolName)) {
11649
+ return { handled: true, error: `No UI widget is registered for ${uri}` };
11567
11650
  }
11568
- const html = generatePlaceholderWidget(widgetParsed.toolName);
11651
+ const resourceMeta = buildResourceMetaForWidget(registry, widgetParsed.toolName);
11652
+ const html = registry.getStaticWidget(widgetParsed.toolName) ?? generatePlaceholderWidget(widgetParsed.toolName);
11569
11653
  return {
11570
11654
  handled: true,
11571
11655
  result: {
@@ -12299,19 +12383,34 @@ var init_direct_client = __esm({
12299
12383
  // Resource Operations (raw format)
12300
12384
  // ─────────────────────────────────────────────────────────────────────────────
12301
12385
  async listResources() {
12302
- return this.mcpClient.listResources();
12386
+ const resources = await listAllPages("resources/list", async (cursor) => {
12387
+ const result = await this.mcpClient.listResources(cursor ? { cursor } : void 0);
12388
+ return { items: result.resources, nextCursor: result.nextCursor };
12389
+ });
12390
+ return { resources };
12303
12391
  }
12304
12392
  async readResource(uri) {
12305
12393
  return this.mcpClient.readResource({ uri });
12306
12394
  }
12307
12395
  async listResourceTemplates() {
12308
- return this.mcpClient.listResourceTemplates();
12396
+ const resourceTemplates = await listAllPages(
12397
+ "resources/templates/list",
12398
+ async (cursor) => {
12399
+ const result = await this.mcpClient.listResourceTemplates(cursor ? { cursor } : void 0);
12400
+ return { items: result.resourceTemplates, nextCursor: result.nextCursor };
12401
+ }
12402
+ );
12403
+ return { resourceTemplates };
12309
12404
  }
12310
12405
  // ─────────────────────────────────────────────────────────────────────────────
12311
12406
  // Prompt Operations (raw format)
12312
12407
  // ─────────────────────────────────────────────────────────────────────────────
12313
12408
  async listPrompts() {
12314
- return this.mcpClient.listPrompts();
12409
+ const prompts = await listAllPages("prompts/list", async (cursor) => {
12410
+ const result = await this.mcpClient.listPrompts(cursor ? { cursor } : void 0);
12411
+ return { items: result.prompts, nextCursor: result.nextCursor };
12412
+ });
12413
+ return { prompts };
12315
12414
  }
12316
12415
  async getPrompt(name44, args) {
12317
12416
  return this.mcpClient.getPrompt({
@@ -13039,8 +13138,8 @@ function createRemoteAppProbe(appId, provider) {
13039
13138
  name: `remote:${appId}`,
13040
13139
  async check() {
13041
13140
  const result = provider.getHealthStatus(appId);
13042
- if (!result) {
13043
- return { status: "unhealthy", error: "No health check result available" };
13141
+ if (!result || result.status === "unknown" && result.consecutiveFailures === 0) {
13142
+ return { status: "degraded", details: { state: "unknown" } };
13044
13143
  }
13045
13144
  return {
13046
13145
  status: result.status === "healthy" ? "healthy" : result.status === "degraded" ? "degraded" : "unhealthy",
@@ -14153,6 +14252,12 @@ var init_frontmcp_context = __esm({
14153
14252
  // =====================
14154
14253
  /** Request metadata (headers, user-agent, etc.) */
14155
14254
  metadata;
14255
+ /**
14256
+ * The hosting platform's bindings object for this request: a Cloudflare Worker's `env`
14257
+ * (KV namespaces, D1 databases, R2 buckets, Durable Objects, `[vars]`, secrets).
14258
+ * `undefined` on Node, where bindings do not exist.
14259
+ */
14260
+ platformEnv;
14156
14261
  // =====================
14157
14262
  // Auth (mutable, progressively populated)
14158
14263
  // =====================
@@ -14188,6 +14293,7 @@ var init_frontmcp_context = __esm({
14188
14293
  this.traceContext = args.traceContext ?? generateTraceContext();
14189
14294
  this.timestamp = args.timestamp ?? Date.now();
14190
14295
  this._authInfo = args.authInfo ?? {};
14296
+ this.platformEnv = args.platformEnv;
14191
14297
  this.config = {
14192
14298
  forwardCallerTokenTo: toOrigins(args.config?.forwardCallerTokenTo),
14193
14299
  forwardCustomHeadersTo: toOrigins(args.config?.forwardCustomHeadersTo),
@@ -14889,6 +14995,7 @@ var init_frontmcp_context_storage = __esm({
14889
14995
  "use strict";
14890
14996
  init_decorators();
14891
14997
  init_metadata();
14998
+ init_tokens();
14892
14999
  init_mcp_error();
14893
15000
  init_frontmcp_context();
14894
15001
  init_metadata_utils();
@@ -14956,7 +15063,12 @@ var init_frontmcp_context_storage = __esm({
14956
15063
  const headers = request.headers ?? {};
14957
15064
  const headerSessionId = typeof headers["mcp-session-id"] === "string" ? headers["mcp-session-id"].trim() : "";
14958
15065
  const sessionId = headerSessionId.length > 0 ? headerSessionId : `anon:${randomUUID9()}`;
14959
- return this.runFromHeaders(headers, { sessionId, scopeId, peerAddress: request.socket?.remoteAddress }, fn);
15066
+ const platformEnv = request[ServerRequestTokens.webEnv];
15067
+ return this.runFromHeaders(
15068
+ headers,
15069
+ { sessionId, scopeId, peerAddress: request.socket?.remoteAddress, platformEnv },
15070
+ fn
15071
+ );
14960
15072
  }
14961
15073
  withServerConfig(args) {
14962
15074
  return { ...args, config: { ...this.contextConfig, ...args.config } };
@@ -22018,6 +22130,14 @@ import {
22018
22130
  isDevelopment as isDevelopment2,
22019
22131
  randomUUID as randomUUID11
22020
22132
  } from "@frontmcp/utils";
22133
+ function declaredOutput(tool, output) {
22134
+ if (!tool?.outputSchema || typeof output !== "object" || output === null || Array.isArray(output)) return output;
22135
+ if (Array.isArray(output["content"])) return output;
22136
+ const parsed = tool.safeParseOutput(output);
22137
+ if (!parsed.success) return output;
22138
+ const structured = parsed.data.structuredContent;
22139
+ return typeof structured === "object" && structured !== null && !Array.isArray(structured) ? structured : output;
22140
+ }
22021
22141
  function toolNameCandidates(name44) {
22022
22142
  if (!/[-_]/.test(name44)) return [name44];
22023
22143
  return [name44, name44.includes("_") ? name44.replace(/_/g, "-") : name44.replace(/-/g, "_")];
@@ -22745,7 +22865,7 @@ ${JSON.stringify(error.schema, null, 2)}
22745
22865
  this.logger.verbose("execute:done (elicitation fire-and-forget fallback)");
22746
22866
  return;
22747
22867
  }
22748
- if (isClientFacingError(error)) throw error;
22868
+ if (isClientFacingError(error) || error instanceof InvalidOutputError) throw error;
22749
22869
  throw new ToolExecutionError(
22750
22870
  this.state.tool?.metadata.name || "unknown",
22751
22871
  error instanceof Error ? error : void 0
@@ -22788,7 +22908,8 @@ ${JSON.stringify(error.schema, null, 2)}
22788
22908
  }
22789
22909
  async applyUI() {
22790
22910
  this.logger.verbose("applyUI:start");
22791
- const { tool, rawOutput, input } = this.state;
22911
+ const { tool, rawOutput: executeOutput, input } = this.state;
22912
+ const rawOutput = declaredOutput(tool, executeOutput);
22792
22913
  const ctx = this.input.ctx;
22793
22914
  if (ctx?._skipUI) {
22794
22915
  this.logger.verbose("applyUI:skip (agent context - structured data only)");
@@ -24413,6 +24534,38 @@ var init_context = __esm({
24413
24534
  }
24414
24535
  });
24415
24536
 
24537
+ // libs/sdk/src/errors/misconfiguration.ts
24538
+ function findMisconfiguration(error) {
24539
+ let current = error;
24540
+ for (let depth = 0; current instanceof Error && depth < 5; depth++) {
24541
+ const code = current.code;
24542
+ if (typeof code === "string" && Object.prototype.hasOwnProperty.call(MISCONFIGURATION_REMEDIES, code)) {
24543
+ return { code, remedy: MISCONFIGURATION_REMEDIES[code] };
24544
+ }
24545
+ if (current.name === "ZodError") return { code: "CONFIG_INVALID", remedy: CONFIG_INVALID_REMEDY };
24546
+ current = current.cause;
24547
+ }
24548
+ return void 0;
24549
+ }
24550
+ function misconfigurationBody(m) {
24551
+ return { error: "server_misconfigured", code: m.code, message: m.remedy };
24552
+ }
24553
+ var MISCONFIGURATION_REMEDIES, CONFIG_INVALID_REMEDY;
24554
+ var init_misconfiguration = __esm({
24555
+ "libs/sdk/src/errors/misconfiguration.ts"() {
24556
+ "use strict";
24557
+ MISCONFIGURATION_REMEDIES = {
24558
+ SESSION_SECRET_REQUIRED: "Set MCP_SESSION_SECRET in the deployment environment (e.g. `wrangler secret put MCP_SESSION_SECRET` on Cloudflare, or the platform environment settings on Node, Vercel and Lambda). Session IDs are encrypted with it, and production refuses the development machine-id fallback.",
24559
+ JWT_SECRET_INVALID: "JWT_SECRET is present but too weak: HS256 requires at least 32 bytes (RFC 7518). Replace it with `openssl rand -hex 32`.",
24560
+ JWT_SECRET_REQUIRED: "Set JWT_SECRET in the deployment environment (e.g. `wrangler secret put JWT_SECRET`). Tokens are signed with it; production refuses the random per-process fallback because tokens would not survive a restart or verify across instances.",
24561
+ // The startup checks: a server whose entries ask for protection nothing gives them does not start.
24562
+ UNENFORCED_METADATA: "An entry declares a field only a plugin enforces (approval, featureFlag, ...) and no installed plugin that enforces it reaches the entry, so the server refuses to start. Install the plugin or remove the field; the server log names the entries.",
24563
+ AUTH_CONFIGURATION_ERROR: "The auth or authorities configuration is invalid (for example, entries declare authorities and the server has no authorities option, or a rule checks nothing), so the server refuses to start. The server log names the entries."
24564
+ };
24565
+ CONFIG_INVALID_REMEDY = "The FrontMCP configuration failed validation, so the server refuses to start. The server log names the invalid fields.";
24566
+ }
24567
+ });
24568
+
24416
24569
  // libs/sdk/src/server/server.validation.ts
24417
24570
  import { base64Decode } from "@frontmcp/utils";
24418
24571
  function applyHeaders(res, headers) {
@@ -24724,6 +24877,7 @@ var init_flow_instance = __esm({
24724
24877
  init_common();
24725
24878
  init_context();
24726
24879
  init_errors();
24880
+ init_misconfiguration();
24727
24881
  init_server_validation();
24728
24882
  init_flow_http_path();
24729
24883
  init_flow_stages();
@@ -24783,6 +24937,15 @@ var init_flow_instance = __esm({
24783
24937
  return writeHttpResponse(response, e.output);
24784
24938
  }
24785
24939
  }
24940
+ const misconfiguration = findMisconfiguration(e);
24941
+ if (misconfiguration) {
24942
+ this.logger.error("Server misconfigured", { flow: this.name, code: misconfiguration.code });
24943
+ return writeHttpResponse(response, {
24944
+ kind: "json",
24945
+ status: 500,
24946
+ body: misconfigurationBody(misconfiguration)
24947
+ });
24948
+ }
24786
24949
  if (e instanceof PublicMcpError) {
24787
24950
  const challenge = e.wwwAuthenticate;
24788
24951
  this.logger.warn("Public error in flow", {
@@ -25494,14 +25657,39 @@ function collectDynamicProviders(klass, options) {
25494
25657
  }
25495
25658
  return out;
25496
25659
  }
25660
+ function collectDynamicTools(klass, options) {
25661
+ const chain = [];
25662
+ for (let k = klass; k && k !== Function.prototype; k = Object.getPrototypeOf(k)) {
25663
+ chain.push(k);
25664
+ }
25665
+ const out = [];
25666
+ for (let i = chain.length - 1; i >= 0; i--) {
25667
+ const k = chain[i];
25668
+ if (Object.prototype.hasOwnProperty.call(k, "dynamicTools") && typeof k.dynamicTools === "function") {
25669
+ out.push(...k.dynamicTools(options) ?? []);
25670
+ }
25671
+ }
25672
+ return out;
25673
+ }
25674
+ function pluginMetadataFromOptions(options, dynamicTools) {
25675
+ const out = { ...options };
25676
+ for (const key of LIST_METADATA_KEYS) {
25677
+ if (key in out && !Array.isArray(out[key])) delete out[key];
25678
+ }
25679
+ const tools = [...out["tools"] ?? [], ...dynamicTools];
25680
+ if (tools.length > 0) out["tools"] = tools;
25681
+ return out;
25682
+ }
25497
25683
  function dedupePluginProviders(providers) {
25498
25684
  const map = /* @__PURE__ */ new Map();
25499
25685
  for (const p of providers) map.set(p["provide"] ?? p, p);
25500
25686
  return [...map.values()];
25501
25687
  }
25688
+ var LIST_METADATA_KEYS;
25502
25689
  var init_dynamic_utils = __esm({
25503
25690
  "libs/sdk/src/common/dynamic/dynamic.utils.ts"() {
25504
25691
  "use strict";
25692
+ LIST_METADATA_KEYS = ["exports", "plugins", "adapters", "tools", "resources", "prompts", "skills"];
25505
25693
  }
25506
25694
  });
25507
25695
 
@@ -25533,7 +25721,7 @@ var init_dynamic_plugin = __esm({
25533
25721
  const typedOptions = options;
25534
25722
  if ("useFactory" in options) {
25535
25723
  return {
25536
- ...typedOptions,
25724
+ ...pluginMetadataFromOptions(typedOptions, []),
25537
25725
  provide: this,
25538
25726
  inject: options.inject,
25539
25727
  useFactory: options.useFactory,
@@ -25542,10 +25730,11 @@ var init_dynamic_plugin = __esm({
25542
25730
  }
25543
25731
  const dyn = collectDynamicProviders(this, typedOptions);
25544
25732
  const mergedProviders = dedupePluginProviders([...dyn ?? [], ...extraProviders ?? []]);
25733
+ const dynamicTools = collectDynamicTools(this, typedOptions);
25545
25734
  const instance = new this(options);
25546
25735
  initOptionsByInstance.set(instance, options);
25547
25736
  return {
25548
- ...typedOptions,
25737
+ ...pluginMetadataFromOptions(typedOptions, dynamicTools),
25549
25738
  provide: this,
25550
25739
  useValue: instance,
25551
25740
  providers: mergedProviders
@@ -25807,9 +25996,18 @@ var init_execution_context_interface = __esm({
25807
25996
  if (this._authContext) return this._authContext;
25808
25997
  const rawAuth = __require("@frontmcp/auth");
25809
25998
  const auth = rawAuth.default ?? rawAuth;
25810
- this._authContext = auth.buildAuthContext(this._authInfo, this.scope.metadata.authorities?.claimsMapping);
25999
+ this._authContext = auth.buildAuthContext(this.resolveAuthSource(), this.scope.metadata.authorities?.claimsMapping);
25811
26000
  return this._authContext;
25812
26001
  }
26002
+ resolveAuthSource() {
26003
+ const fromContext = this.tryGetContext()?.authInfo;
26004
+ if (!fromContext) return this._authInfo;
26005
+ const merged = { ...this._authInfo };
26006
+ for (const [key, value] of Object.entries(fromContext)) {
26007
+ if (value !== void 0) merged[key] = value;
26008
+ }
26009
+ return merged;
26010
+ }
25813
26011
  /**
25814
26012
  * Try to get the context, returning undefined if not available.
25815
26013
  *
@@ -25995,6 +26193,23 @@ var init_execution_context_interface = __esm({
25995
26193
  get config() {
25996
26194
  return this.providers.get(ConfigService);
25997
26195
  }
26196
+ // ---- Platform bindings ----
26197
+ /**
26198
+ * The hosting platform's bindings for the current request — on a Cloudflare Worker, the `env`
26199
+ * object holding KV namespaces, D1 databases, R2 buckets, Durable Object namespaces, `[vars]`
26200
+ * and secrets. `undefined` on Node/Express, where no such object exists.
26201
+ *
26202
+ * Only string bindings are mirrored into `process.env`; use this for the rest.
26203
+ *
26204
+ * @example
26205
+ * ```typescript
26206
+ * const kv = this.workerEnv?.['MY_KV'] as KVNamespace | undefined;
26207
+ * ```
26208
+ */
26209
+ get workerEnv() {
26210
+ const env = this.tryGetContext()?.platformEnv;
26211
+ return env !== null && typeof env === "object" ? env : void 0;
26212
+ }
25998
26213
  // ---- Runtime context helpers ----
25999
26214
  /**
26000
26215
  * Get the current runtime context (platform, runtime, deployment, env).
@@ -30801,6 +31016,9 @@ import { AuthorityDeniedError as AuthorityDeniedError7, resolveRequiredScopes as
30801
31016
  import { ExecutionTimeoutError as ExecutionTimeoutError2, withTimeout as withTimeout2 } from "@frontmcp/guard";
30802
31017
  import { z as z60 } from "@frontmcp/lazy-zod";
30803
31018
  import { CallToolRequestSchema as CallToolRequestSchema3, CallToolResultSchema as CallToolResultSchema3 } from "@frontmcp/protocol";
31019
+ function invalidOutputFrom(outputError) {
31020
+ return outputError instanceof InvalidOutputError ? outputError : new InvalidOutputError();
31021
+ }
30804
31022
  var inputSchema15, outputSchema14, stateSchema14, plan14, name14, Stage14, CallAgentFlow;
30805
31023
  var init_call_agent_flow = __esm({
30806
31024
  "libs/sdk/src/agent/flows/call-agent.flow.ts"() {
@@ -31183,7 +31401,7 @@ var init_call_agent_flow = __esm({
31183
31401
  const { agent, agentContext, executionStartedAt, executionError, outputError, authInfo } = this.state;
31184
31402
  if (!agent || executionStartedAt === void 0) return;
31185
31403
  if (executionError instanceof ElicitationFallbackRequired || executionError instanceof FlowControl) return;
31186
- const failure = executionError ?? (outputError !== void 0 ? new InvalidOutputError() : void 0);
31404
+ const failure = executionError ?? (outputError !== void 0 ? invalidOutputFrom(outputError) : void 0);
31187
31405
  const failed = failure !== void 0;
31188
31406
  const requestId = this.tryGetContext()?.requestId;
31189
31407
  const sessionId = typeof authInfo?.sessionId === "string" && authInfo.sessionId ? authInfo.sessionId : void 0;
@@ -31213,7 +31431,7 @@ var init_call_agent_flow = __esm({
31213
31431
  agent: agent.metadata.name,
31214
31432
  errors: outputError
31215
31433
  });
31216
- throw new InvalidOutputError();
31434
+ throw invalidOutputFrom(outputError);
31217
31435
  }
31218
31436
  const result = parsedOutput;
31219
31437
  if (executionMeta) {
@@ -32850,6 +33068,10 @@ var init_mcp_stateless_client = __esm({
32850
33068
  async listResources(cursor) {
32851
33069
  return this.request("resources/list", cursor ? { cursor } : {});
32852
33070
  }
33071
+ /** One page of `resources/templates/list`; pass the previous page's `nextCursor` for the next. */
33072
+ async listResourceTemplates(cursor) {
33073
+ return this.request("resources/templates/list", cursor ? { cursor } : {});
33074
+ }
32853
33075
  /** One page of `prompts/list`; pass the previous page's `nextCursor` for the next. */
32854
33076
  async listPrompts(cursor) {
32855
33077
  return this.request("prompts/list", cursor ? { cursor } : {});
@@ -33191,6 +33413,13 @@ var init_mcp_stateless_client_adapter = __esm({
33191
33413
  ...nextCursorOf(result)
33192
33414
  };
33193
33415
  }
33416
+ async listResourceTemplates(params) {
33417
+ const result = await this.client.listResourceTemplates(params?.cursor);
33418
+ return {
33419
+ resourceTemplates: Array.isArray(result["resourceTemplates"]) ? result["resourceTemplates"] : [],
33420
+ ...nextCursorOf(result)
33421
+ };
33422
+ }
33194
33423
  async readResource(params) {
33195
33424
  return this.client.readResource(params.uri);
33196
33425
  }
@@ -33558,7 +33787,7 @@ var init_health_check = __esm({
33558
33787
  this.consecutiveSuccesses++;
33559
33788
  this.consecutiveFailures = 0;
33560
33789
  const previousStatus = this.status;
33561
- if (this.consecutiveSuccesses >= this.options.healthyThreshold) {
33790
+ if (previousStatus === "unknown" || this.consecutiveSuccesses >= this.options.healthyThreshold) {
33562
33791
  this.status = latencyMs > this.options.degradedLatencyMs ? "degraded" : "healthy";
33563
33792
  }
33564
33793
  this.lastResult = {
@@ -42904,9 +43133,11 @@ var init_instance_local_primary_auth = __esm({
42904
43133
  } else if (isProduction6() && isOrchestratedMode(options)) {
42905
43134
  throw new JwtSecretRequiredError(options.mode);
42906
43135
  } else {
42907
- this.logger.warn(
42908
- "JWT_SECRET is not set; signing with a random per-process secret. Tokens will not survive a restart and will not verify across instances. Set JWT_SECRET for any deployment that mints tokens."
42909
- );
43136
+ if (!isPublicMode(options)) {
43137
+ this.logger.warn(
43138
+ "JWT_SECRET is not set; signing with a random per-process secret. Tokens will not survive a restart and will not verify across instances. Set JWT_SECRET for any deployment that mints tokens."
43139
+ );
43140
+ }
42910
43141
  this.secret = getDefaultNoAuthSecret();
42911
43142
  }
42912
43143
  this.tokenStorage = this.readTokenStorage(options);
@@ -46883,7 +47114,14 @@ var init_encrypted_elicitation_store = __esm({
46883
47114
 
46884
47115
  // libs/sdk/src/elicitation/store/elicitation-store.factory.ts
46885
47116
  import { createMemoryStorage as createMemoryStorage2, createStorage as createStorage2, getEnv as getEnv7 } from "@frontmcp/utils";
46886
- function detectStorageType2(storage) {
47117
+ function detectStorageType2(storage, configuredType) {
47118
+ if (configuredType === "redis" || configuredType === "upstash" || configuredType === "memory") {
47119
+ return configuredType;
47120
+ }
47121
+ const backend = storage.type ?? storage.backendName;
47122
+ if (backend === "redis" || backend === "upstash" || backend === "memory") {
47123
+ return backend;
47124
+ }
46887
47125
  if (getEnv7("UPSTASH_REDIS_REST_URL")) {
46888
47126
  return "upstash";
46889
47127
  }
@@ -46973,7 +47211,8 @@ async function createElicitationStore(options = {}) {
46973
47211
  });
46974
47212
  }
46975
47213
  }
46976
- if (finalStorageConfig?.type === "vercel-kv" || getEnv7("KV_REST_API_URL")) {
47214
+ const explicitBackend = Boolean(storageConfig || redis);
47215
+ if (finalStorageConfig?.type === "vercel-kv" || !explicitBackend && getEnv7("KV_REST_API_URL")) {
46977
47216
  throw new ElicitationNotSupportedError(
46978
47217
  "Vercel KV is not supported for elicitation stores. Elicitation requires pub/sub for cross-node result routing, which Vercel KV does not support. Use Redis or Upstash instead."
46979
47218
  );
@@ -46994,7 +47233,7 @@ async function createElicitationStore(options = {}) {
46994
47233
  );
46995
47234
  }
46996
47235
  let store = new StorageElicitationStore(storage, logger);
46997
- const type = detectStorageType2(storage);
47236
+ const type = detectStorageType2(storage, finalStorageConfig?.type);
46998
47237
  const encryptionEnabled = encryption?.enabled ?? "auto";
46999
47238
  const encryptionSecret = encryption?.secret;
47000
47239
  const hasSecret = encryptionSecret || isElicitationEncryptionAvailable();
@@ -49806,6 +50045,7 @@ var init_redis_job_definition_store = __esm({
49806
50045
  });
49807
50046
 
49808
50047
  // libs/sdk/src/job/store/job-definition-store.factory.ts
50048
+ import { attachRedisErrorListener } from "@frontmcp/utils";
49809
50049
  function createJobDefinitionStore(options, logger) {
49810
50050
  const keyPrefix = options?.keyPrefix ?? "mcp:jobs:def:";
49811
50051
  const effectiveLogger = logger ?? noopLogger2;
@@ -49817,6 +50057,7 @@ function createJobDefinitionStore(options, logger) {
49817
50057
  host: options.redis.host ?? "localhost",
49818
50058
  port: options.redis.port ?? 6379
49819
50059
  });
50060
+ attachRedisErrorListener(client, { label: "JobDefinitionStore", logger: effectiveLogger });
49820
50061
  return {
49821
50062
  // Factory-created client lifetime is bound to the store, so the store
49822
50063
  // must close it on dispose() — pass ownsClient: true.
@@ -50017,6 +50258,7 @@ var init_redis_job_state_store = __esm({
50017
50258
  });
50018
50259
 
50019
50260
  // libs/sdk/src/job/store/job-state-store.factory.ts
50261
+ import { attachRedisErrorListener as attachRedisErrorListener2 } from "@frontmcp/utils";
50020
50262
  function createJobStateStore(options, logger) {
50021
50263
  const keyPrefix = options?.keyPrefix ?? "mcp:jobs:";
50022
50264
  const ttlSeconds = options?.ttlSeconds ?? 86400;
@@ -50028,6 +50270,7 @@ function createJobStateStore(options, logger) {
50028
50270
  host: options.redis.host ?? "localhost",
50029
50271
  port: options.redis.port ?? 6379
50030
50272
  });
50273
+ attachRedisErrorListener2(client, { label: "JobStateStore", logger });
50031
50274
  return {
50032
50275
  store: new RedisJobStateStore2(client, logger, keyPrefix, ttlSeconds),
50033
50276
  type: "redis"
@@ -52443,6 +52686,7 @@ var init_skill_http_cache = __esm({
52443
52686
  });
52444
52687
 
52445
52688
  // libs/sdk/src/skill/cache/skill-http-cache.factory.ts
52689
+ import { attachRedisErrorListener as attachRedisErrorListener3 } from "@frontmcp/utils";
52446
52690
  function hasRedisProvider(redis) {
52447
52691
  if (!redis?.provider) return false;
52448
52692
  const provider = redis.provider;
@@ -52487,6 +52731,7 @@ async function createRedisCache(redis, keyPrefix, ttlMs, logger) {
52487
52731
  enableReadyCheck: false,
52488
52732
  maxRetriesPerRequest: 3
52489
52733
  });
52734
+ attachRedisErrorListener3(client, { label: "SkillHttpCache" });
52490
52735
  await client.connect();
52491
52736
  return new RedisSkillHttpCache({
52492
52737
  getClient: async () => client,
@@ -56302,6 +56547,51 @@ var init_skill_scope_helper = __esm({
56302
56547
  }
56303
56548
  });
56304
56549
 
56550
+ // libs/sdk/src/tool/ui/ui-option-warnings.ts
56551
+ function describeIgnoredUiOptions(toolName, ui) {
56552
+ if (!ui) return [];
56553
+ const messages = [];
56554
+ const ignored = IGNORED_UI_OPTIONS.filter((key) => ui[key] !== void 0);
56555
+ if (ignored.length > 0) {
56556
+ messages.push(
56557
+ `Tool "${toolName}": \`ui.${ignored.join("`, `ui.")}\` ${ignored.length === 1 ? "is" : "are"} accepted but not used yet, so ${ignored.length === 1 ? "it has" : "they have"} no effect.`
56558
+ );
56559
+ }
56560
+ const mode = ui["servingMode"];
56561
+ if (UNSUPPORTED_SERVING_MODES.includes(mode)) {
56562
+ messages.push(
56563
+ `Tool "${toolName}": \`ui.servingMode: '${String(mode)}'\` is not implemented; the widget is served inline, as with \`servingMode: 'inline'\`.`
56564
+ );
56565
+ }
56566
+ if (mode === "hybrid") {
56567
+ messages.push(
56568
+ `Tool "${toolName}": \`ui.servingMode: 'hybrid'\` sends only a reference in \`_meta['ui/component']\` ({ type, hash, toolName }), not the component code; the widget is rendered from its \`ui://\` resource.`
56569
+ );
56570
+ }
56571
+ return messages;
56572
+ }
56573
+ var IGNORED_UI_OPTIONS, UNSUPPORTED_SERVING_MODES;
56574
+ var init_ui_option_warnings = __esm({
56575
+ "libs/sdk/src/tool/ui/ui-option-warnings.ts"() {
56576
+ "use strict";
56577
+ IGNORED_UI_OPTIONS = [
56578
+ "widgetDescription",
56579
+ "widgetAccessible",
56580
+ "displayMode",
56581
+ "prefersBorder",
56582
+ "sandboxDomain",
56583
+ "contentSecurity",
56584
+ "hydrate",
56585
+ "runtimeOptions",
56586
+ "mdxComponents",
56587
+ "bundlingMode",
56588
+ "uiType",
56589
+ "htmlResponsePrefix"
56590
+ ];
56591
+ UNSUPPORTED_SERVING_MODES = ["direct-url", "custom-url"];
56592
+ }
56593
+ });
56594
+
56305
56595
  // libs/sdk/src/transport/bus/redis-transport-bus.ts
56306
56596
  var DEFAULT_BUS_PREFIX, DEFAULT_BUS_TTL_SECONDS, REVOKE_LUA, RedisTransportBus;
56307
56597
  var init_redis_transport_bus = __esm({
@@ -56431,6 +56721,7 @@ var redis_event_store_exports = {};
56431
56721
  __export(redis_event_store_exports, {
56432
56722
  RedisEventStore: () => RedisEventStore
56433
56723
  });
56724
+ import { attachRedisErrorListener as attachRedisErrorListener4 } from "@frontmcp/utils";
56434
56725
  var RedisEventStore;
56435
56726
  var init_redis_event_store = __esm({
56436
56727
  "libs/sdk/src/transport/event-stores/redis.event-store.ts"() {
@@ -56478,6 +56769,9 @@ var init_redis_event_store = __esm({
56478
56769
  enableReadyCheck: false,
56479
56770
  maxRetriesPerRequest: 3
56480
56771
  });
56772
+ attachRedisErrorListener4(this.client, {
56773
+ label: "RedisEventStore"
56774
+ });
56481
56775
  await this.client.connect();
56482
56776
  }
56483
56777
  return this.client;
@@ -57130,7 +57424,7 @@ var init_handle_mcp_20260728_flow = __esm({
57130
57424
  const sink = new RequestNotificationSink(logLevel, progressToken);
57131
57425
  const clientInfo = toClientInfo(meta[MCP_20260728_META5.clientInfo]);
57132
57426
  if (clientInfo) {
57133
- const platformType = detectAIPlatform(clientInfo, this.scope.metadata.transport?.platformDetection);
57427
+ const platformType = detectPlatformFromCapabilities(clientCapabilities) ?? detectAIPlatform(clientInfo, this.scope.metadata.transport?.platformDetection);
57134
57428
  this.tryGetContext()?.setClientInfo(clientInfo, platformType);
57135
57429
  }
57136
57430
  const dispatchOptions = {
@@ -57228,6 +57522,28 @@ var init_handle_mcp_20260728_flow = __esm({
57228
57522
  }
57229
57523
  });
57230
57524
 
57525
+ // libs/sdk/src/ha/ha-headers.ts
57526
+ import { buildSetCookie, getMachineId as getMachineId3, getRuntimeContext as getRuntimeContext13 } from "@frontmcp/utils";
57527
+ function applyMachineIdHeader(response) {
57528
+ if (getRuntimeContext13().deployment !== "distributed") return false;
57529
+ response.setHeader(DEFAULT_FRONTMCP_MACHINE_ID_HEADER, getMachineId3());
57530
+ return true;
57531
+ }
57532
+ function applyNodeAffinity(response, request) {
57533
+ if (!applyMachineIdHeader(response)) return;
57534
+ const cookie = buildSetCookie({ name: DEFAULT_FRONTMCP_NODE_COOKIE, value: getMachineId3() }, request);
57535
+ if (!cookie) return;
57536
+ const existing = response.getHeader("Set-Cookie");
57537
+ const existingArr = Array.isArray(existing) ? existing : existing ? [String(existing)] : [];
57538
+ response.setHeader("Set-Cookie", [...existingArr, cookie]);
57539
+ }
57540
+ var init_ha_headers = __esm({
57541
+ "libs/sdk/src/ha/ha-headers.ts"() {
57542
+ "use strict";
57543
+ init_ha_constants();
57544
+ }
57545
+ });
57546
+
57231
57547
  // libs/sdk/src/skill/skill-mode.utils.ts
57232
57548
  function detectSkillsOnlyMode(query) {
57233
57549
  if (!query) return false;
@@ -57248,7 +57564,7 @@ var init_skill_mode_utils = __esm({
57248
57564
 
57249
57565
  // libs/sdk/src/transport/flows/handle.sse.flow.ts
57250
57566
  import { z as z103 } from "@frontmcp/lazy-zod";
57251
- import { buildSetCookie, getMachineId as getMachineId3, getRuntimeContext as getRuntimeContext13 } from "@frontmcp/utils";
57567
+ import { getMachineId as getMachineId4, getRuntimeContext as getRuntimeContext14 } from "@frontmcp/utils";
57252
57568
  function getQuerySessionId2(urlPath) {
57253
57569
  if (!urlPath) return void 0;
57254
57570
  try {
@@ -57289,7 +57605,7 @@ var init_handle_sse_flow = __esm({
57289
57605
  init_session_id_utils();
57290
57606
  init_common();
57291
57607
  init_errors();
57292
- init_ha_constants();
57608
+ init_ha_headers();
57293
57609
  init_skill_mode_utils();
57294
57610
  plan38 = {
57295
57611
  pre: ["parseInput", "router"],
@@ -57363,16 +57679,7 @@ var init_handle_sse_flow = __esm({
57363
57679
  const { request, response } = this.rawInput;
57364
57680
  const { token, session } = this.state.required;
57365
57681
  const transport = await transportService.createTransporter("sse", token, session.id, response);
57366
- if (getRuntimeContext13().deployment === "distributed") {
57367
- const nodeId = getMachineId3();
57368
- response.setHeader(DEFAULT_FRONTMCP_MACHINE_ID_HEADER, nodeId);
57369
- const cookie = buildSetCookie({ name: DEFAULT_FRONTMCP_NODE_COOKIE, value: nodeId }, request);
57370
- if (cookie) {
57371
- const existing = response.getHeader("Set-Cookie");
57372
- const existingArr = Array.isArray(existing) ? existing : existing ? [String(existing)] : [];
57373
- response.setHeader("Set-Cookie", [...existingArr, cookie]);
57374
- }
57375
- }
57682
+ applyNodeAffinity(response, request);
57376
57683
  await transport.initialize(request, response);
57377
57684
  this.handled();
57378
57685
  }
@@ -57388,12 +57695,12 @@ var init_handle_sse_flow = __esm({
57388
57695
  const { request, response } = this.rawInput;
57389
57696
  const { token, session } = this.state.required;
57390
57697
  const transport = await transportService.getTransporter("sse", token, session.id);
57391
- if (!transport && getRuntimeContext13().deployment === "distributed") {
57698
+ if (!transport && getRuntimeContext14().deployment === "distributed") {
57392
57699
  const storedSession = await transportService.getStoredSession("sse", token, session.id);
57393
57700
  if (storedSession) {
57394
57701
  const haManager = this.scope.haManager;
57395
57702
  const relay = haManager?.getRelay();
57396
- if (relay && storedSession.session.nodeId && storedSession.session.nodeId !== getMachineId3()) {
57703
+ if (relay && storedSession.session.nodeId && storedSession.session.nodeId !== getMachineId4()) {
57397
57704
  const isAlive2 = await haManager.isNodeAlive(storedSession.session.nodeId);
57398
57705
  if (isAlive2) {
57399
57706
  try {
@@ -57488,9 +57795,10 @@ var init_handle_stateless_http_flow = __esm({
57488
57795
  "use strict";
57489
57796
  init_common();
57490
57797
  init_errors();
57798
+ init_ha_headers();
57491
57799
  init_transport_types();
57492
57800
  plan39 = {
57493
- pre: ["parseInput", "router"],
57801
+ pre: ["applyNodeHeaders", "parseInput", "router"],
57494
57802
  execute: ["handleRequest"],
57495
57803
  post: [],
57496
57804
  finalize: ["cleanup"]
@@ -57504,6 +57812,9 @@ var init_handle_stateless_http_flow = __esm({
57504
57812
  ({ Stage: Stage40 } = FlowHooksOf(name40));
57505
57813
  HandleStatelessHttpFlow = class extends FlowBase {
57506
57814
  name = name40;
57815
+ async applyNodeHeaders() {
57816
+ applyMachineIdHeader(this.rawInput.response);
57817
+ }
57507
57818
  async parseInput() {
57508
57819
  const { request } = this.rawInput;
57509
57820
  const logger = this.scope.logger.child("HandleStatelessHttpFlow");
@@ -57565,6 +57876,9 @@ var init_handle_stateless_http_flow = __esm({
57565
57876
  this.handled();
57566
57877
  }
57567
57878
  };
57879
+ __decorateClass([
57880
+ Stage40("applyNodeHeaders")
57881
+ ], HandleStatelessHttpFlow.prototype, "applyNodeHeaders", 1);
57568
57882
  __decorateClass([
57569
57883
  Stage40("parseInput")
57570
57884
  ], HandleStatelessHttpFlow.prototype, "parseInput", 1);
@@ -57944,7 +58258,6 @@ var init_ext_apps2 = __esm({
57944
58258
  // libs/sdk/src/transport/flows/handle.streamable-http.flow.ts
57945
58259
  import { z as z105 } from "@frontmcp/lazy-zod";
57946
58260
  import { CallToolResultSchema as CallToolResultSchema4, ElicitResultSchema, RequestSchema as RequestSchema2 } from "@frontmcp/protocol";
57947
- import { buildSetCookie as buildSetCookie2, getMachineId as getMachineId4, getRuntimeContext as getRuntimeContext14 } from "@frontmcp/utils";
57948
58261
  function resolveStreamableHttpSession(params) {
57949
58262
  const { rawHeader, authorizationSession, createSession } = params;
57950
58263
  const rawMcpSessionHeader = typeof rawHeader === "string" ? rawHeader : void 0;
@@ -58040,11 +58353,11 @@ var init_handle_streamable_http_flow = __esm({
58040
58353
  init_common();
58041
58354
  init_errors();
58042
58355
  init_ext_apps2();
58043
- init_ha_constants();
58356
+ init_ha_headers();
58044
58357
  init_skill_mode_utils();
58045
58358
  init_mcp_surface();
58046
58359
  plan40 = {
58047
- pre: ["parseInput", "router"],
58360
+ pre: ["applyNodeHeaders", "parseInput", "router"],
58048
58361
  execute: ["onInitialize", "onMessage", "onElicitResult", "onSseListener", "onExtApps"],
58049
58362
  post: [],
58050
58363
  finalize: ["cleanup"]
@@ -58070,6 +58383,9 @@ var init_handle_streamable_http_flow = __esm({
58070
58383
  ({ Stage: Stage41 } = FlowHooksOf(name41));
58071
58384
  HandleStreamableHttpFlow = class extends FlowBase {
58072
58385
  name = name41;
58386
+ async applyNodeHeaders() {
58387
+ applyMachineIdHeader(this.rawInput.response);
58388
+ }
58073
58389
  async parseInput() {
58074
58390
  const { request } = this.rawInput;
58075
58391
  const authorization = request[ServerRequestTokens.auth];
@@ -58168,16 +58484,7 @@ var init_handle_streamable_http_flow = __esm({
58168
58484
  return;
58169
58485
  }
58170
58486
  }
58171
- if (getRuntimeContext14().deployment === "distributed") {
58172
- const nodeId = getMachineId4();
58173
- response.setHeader(DEFAULT_FRONTMCP_MACHINE_ID_HEADER, nodeId);
58174
- const cookie = buildSetCookie2({ name: DEFAULT_FRONTMCP_NODE_COOKIE, value: nodeId }, request);
58175
- if (cookie) {
58176
- const existing = response.getHeader("Set-Cookie");
58177
- const existingArr = Array.isArray(existing) ? existing : existing ? [String(existing)] : [];
58178
- response.setHeader("Set-Cookie", [...existingArr, cookie]);
58179
- }
58180
- }
58487
+ applyNodeAffinity(response, request);
58181
58488
  logger.info("onInitialize: transport created, calling initialize");
58182
58489
  await transport.initialize(request, response);
58183
58490
  logger.info("onInitialize: completed successfully");
@@ -58360,6 +58667,17 @@ var init_handle_streamable_http_flow = __esm({
58360
58667
  const logger = this.scopeLogger.child("handle:streamable-http:onExtApps");
58361
58668
  const { request, response } = this.rawInput;
58362
58669
  const { token, session } = this.state.required;
58670
+ if (this.scope.metadata.extApps?.enabled === false) {
58671
+ const rpc = request.body;
58672
+ logger.verbose("onExtApps: ext-apps disabled by config", { method: rpc?.method });
58673
+ response.status(200).json({
58674
+ jsonrpc: "2.0",
58675
+ id: rpc?.id ?? null,
58676
+ error: { code: -32601, message: `Method not found: ${rpc?.method}` }
58677
+ });
58678
+ this.handled();
58679
+ return;
58680
+ }
58363
58681
  logger.info("onExtApps: starting", {
58364
58682
  sessionId: session.id?.slice(0, 20),
58365
58683
  method: request.body?.method
@@ -58411,8 +58729,20 @@ var init_handle_streamable_http_flow = __esm({
58411
58729
  const hostCapabilities = {
58412
58730
  serverToolProxy: configuredCapabilities?.serverToolProxy ?? true,
58413
58731
  logging: configuredCapabilities?.logging ?? true,
58414
- ...configuredCapabilities
58732
+ ...configuredCapabilities,
58733
+ // Only advertise what this host can serve: the context below provides callTool and nothing else
58734
+ openLink: false,
58735
+ modelContextUpdate: false,
58736
+ widgetTools: false
58415
58737
  };
58738
+ const unsupported = ["openLink", "modelContextUpdate", "widgetTools"].filter(
58739
+ (key) => configuredCapabilities?.[key] === true
58740
+ );
58741
+ if (unsupported.length > 0) {
58742
+ logger.warn(
58743
+ `onExtApps: extApps.hostCapabilities ${unsupported.join(", ")} are not supported yet and are not advertised`
58744
+ );
58745
+ }
58416
58746
  const handler = createExtAppsMessageHandler({
58417
58747
  context: {
58418
58748
  sessionId: session.id,
@@ -58446,6 +58776,9 @@ var init_handle_streamable_http_flow = __esm({
58446
58776
  this.handled();
58447
58777
  }
58448
58778
  };
58779
+ __decorateClass([
58780
+ Stage41("applyNodeHeaders")
58781
+ ], HandleStreamableHttpFlow.prototype, "applyNodeHeaders", 1);
58449
58782
  __decorateClass([
58450
58783
  Stage41("parseInput")
58451
58784
  ], HandleStreamableHttpFlow.prototype, "parseInput", 1);
@@ -60038,7 +60371,7 @@ var init_transport_remote = __esm({
60038
60371
 
60039
60372
  // libs/sdk/src/transport/transport.registry.ts
60040
60373
  import { getMachineId as getMachineId5, sha256Hex as sha256Hex12 } from "@frontmcp/utils";
60041
- var TransportService;
60374
+ var SESSION_STORE_RETRY_BASE_MS, SESSION_STORE_RETRY_MAX_MS, TransportService;
60042
60375
  var init_transport_registry = __esm({
60043
60376
  "libs/sdk/src/transport/transport.registry.ts"() {
60044
60377
  "use strict";
@@ -60051,6 +60384,8 @@ var init_transport_registry = __esm({
60051
60384
  init_transport_local();
60052
60385
  init_transport_remote();
60053
60386
  init_transport_types();
60387
+ SESSION_STORE_RETRY_BASE_MS = 1e3;
60388
+ SESSION_STORE_RETRY_MAX_MS = 3e4;
60054
60389
  TransportService = class {
60055
60390
  ready;
60056
60391
  byType = /* @__PURE__ */ new Map();
@@ -60107,8 +60442,28 @@ var init_transport_registry = __esm({
60107
60442
  * Returns undefined if persistence is disabled or not configured.
60108
60443
  */
60109
60444
  getDefaultTtlMs() {
60445
+ if (this.resolvedTtlMs !== void 0) return this.resolvedTtlMs;
60110
60446
  return typeof this.persistenceConfig === "object" ? this.persistenceConfig?.defaultTtlMs : void 0;
60111
60447
  }
60448
+ /**
60449
+ * TTL resolved from `persistence.defaultTtlMs`, then `persistence.redis.defaultTtlMs`, then 1 hour.
60450
+ * Set only when a persistence backend is configured.
60451
+ */
60452
+ resolvedTtlMs;
60453
+ /** Store config kept after a successful create so a failed ping can be retried. */
60454
+ storeRetryConfig;
60455
+ storeRetryTimer;
60456
+ storeRetryAttempt = 0;
60457
+ destroyed = false;
60458
+ /** Last time a request refreshed the stored session's TTL, by session id. */
60459
+ lastTtlRefreshAt = /* @__PURE__ */ new Map();
60460
+ /**
60461
+ * Redis key prefix under which session records are stored.
60462
+ * The session store appends `session:` to the transport key prefix.
60463
+ */
60464
+ getSessionKeyPrefix() {
60465
+ return `${this.transportKeyPrefix}session:`;
60466
+ }
60112
60467
  constructor(scope, persistenceConfig, bus) {
60113
60468
  this.scope = scope;
60114
60469
  this.persistenceConfig = persistenceConfig;
@@ -60119,10 +60474,11 @@ var init_transport_registry = __esm({
60119
60474
  this.backendKind = "sqlite";
60120
60475
  const sqliteConfig = persistenceConfig.sqlite;
60121
60476
  this.transportKeyPrefix = "mcp:transport:";
60477
+ this.resolvedTtlMs = persistenceConfig.defaultTtlMs ?? 36e5;
60122
60478
  this.pendingStoreConfig = {
60123
60479
  sqlite: sqliteConfig,
60124
60480
  keyPrefix: this.transportKeyPrefix,
60125
- defaultTtlMs: persistenceConfig.defaultTtlMs ?? 36e5
60481
+ defaultTtlMs: this.resolvedTtlMs
60126
60482
  };
60127
60483
  this.scope.logger.info("[TransportService] sqlite session store will be initialized for transport persistence");
60128
60484
  } else if (persistenceConfig !== false && persistenceConfig?.redis) {
@@ -60131,43 +60487,23 @@ var init_transport_registry = __esm({
60131
60487
  const providerType = "provider" in redisConfig ? redisConfig.provider : "redis";
60132
60488
  this.backendKind = providerType === "vercel-kv" ? "vercel-kv" : "redis";
60133
60489
  this.transportKeyPrefix = redisConfig.keyPrefix ?? "mcp:transport:";
60490
+ this.resolvedTtlMs = persistenceConfig.defaultTtlMs ?? redisConfig.defaultTtlMs ?? 36e5;
60134
60491
  this.pendingStoreConfig = {
60135
60492
  ...redisConfig,
60136
60493
  keyPrefix: this.transportKeyPrefix,
60137
- defaultTtlMs: persistenceConfig.defaultTtlMs ?? 36e5
60138
- // 1 hour default
60494
+ defaultTtlMs: this.resolvedTtlMs
60139
60495
  };
60140
60496
  this.scope.logger.info(
60141
60497
  `[TransportService] ${providerType} session store will be initialized for transport persistence`
60142
60498
  );
60143
60499
  }
60500
+ this.storeRetryConfig = this.pendingStoreConfig;
60144
60501
  this.ready = this.initialize();
60145
60502
  }
60146
60503
  async initialize() {
60147
60504
  if (this.pendingStoreConfig) {
60148
- try {
60149
- const store = await createSessionStore(this.pendingStoreConfig, this.scope.logger.child("SessionStore"));
60150
- this.sessionStore = store;
60151
- this.pendingStoreConfig = void 0;
60152
- } catch (error) {
60153
- const err = error;
60154
- this.scope.logger.error("[TransportService] Failed to create session store - session persistence disabled", {
60155
- error: err.message,
60156
- cause: err.cause?.message
60157
- });
60158
- }
60159
- }
60160
- if (this.sessionStore) {
60161
- const isConnected = this.sessionStore.ping ? await this.sessionStore.ping() : true;
60162
- if (!isConnected) {
60163
- const providerType = this.backendKind ?? "redis";
60164
- this.scope.logger.error(
60165
- `[TransportService] Failed to connect to ${providerType} - session persistence disabled`
60166
- );
60167
- await this.teardownSessionStore().catch(() => void 0);
60168
- } else {
60169
- this.scope.logger.info("[TransportService] Session store connection validated successfully");
60170
- }
60505
+ const connected = await this.connectSessionStore();
60506
+ if (!connected && this.backendKind !== "sqlite") this.scheduleSessionStoreRetry();
60171
60507
  }
60172
60508
  await this.scope.registryFlows(
60173
60509
  HandleStreamableHttpFlow,
@@ -60176,7 +60512,70 @@ var init_transport_registry = __esm({
60176
60512
  HandleMcp20260728Flow
60177
60513
  );
60178
60514
  }
60515
+ /**
60516
+ * Create the session store and validate the connection.
60517
+ * Returns true when the store is usable; on failure the store is left undefined.
60518
+ */
60519
+ async connectSessionStore() {
60520
+ const storeConfig = this.pendingStoreConfig ?? this.storeRetryConfig;
60521
+ if (!storeConfig) return false;
60522
+ try {
60523
+ const store = await createSessionStore(storeConfig, this.scope.logger.child("SessionStore"));
60524
+ this.sessionStore = store;
60525
+ this.pendingStoreConfig = void 0;
60526
+ } catch (error) {
60527
+ const err = error;
60528
+ this.scope.logger.error("[TransportService] Failed to create session store - session persistence disabled", {
60529
+ error: err.message,
60530
+ cause: err.cause?.message
60531
+ });
60532
+ return false;
60533
+ }
60534
+ const isConnected = this.sessionStore?.ping ? await this.sessionStore.ping() : true;
60535
+ if (!isConnected) {
60536
+ const providerType = this.backendKind ?? "redis";
60537
+ this.scope.logger.error(`[TransportService] Failed to connect to ${providerType} - session persistence disabled`);
60538
+ await this.teardownSessionStore().catch(() => void 0);
60539
+ return false;
60540
+ }
60541
+ this.scope.logger.info("[TransportService] Session store connection validated successfully");
60542
+ return true;
60543
+ }
60544
+ /**
60545
+ * Retry the session store with exponential backoff (1s doubling to 30s) so a Redis that
60546
+ * was down at startup is picked up when it comes back, and `/readyz` turns healthy.
60547
+ */
60548
+ scheduleSessionStoreRetry() {
60549
+ if (this.destroyed || this.storeRetryTimer) return;
60550
+ const delay = Math.min(SESSION_STORE_RETRY_MAX_MS, SESSION_STORE_RETRY_BASE_MS * 2 ** this.storeRetryAttempt);
60551
+ this.storeRetryAttempt++;
60552
+ this.storeRetryTimer = setTimeout(() => {
60553
+ this.storeRetryTimer = void 0;
60554
+ void this.retrySessionStore();
60555
+ }, delay);
60556
+ this.storeRetryTimer.unref?.();
60557
+ }
60558
+ async retrySessionStore() {
60559
+ if (this.destroyed || this.sessionStore) return;
60560
+ const connected = await this.connectSessionStore().catch(() => false);
60561
+ if (this.destroyed) {
60562
+ await this.teardownSessionStore().catch(() => void 0);
60563
+ return;
60564
+ }
60565
+ if (connected) {
60566
+ this.storeRetryAttempt = 0;
60567
+ this.scope.logger.info("[TransportService] Session store recovered after startup failure");
60568
+ return;
60569
+ }
60570
+ this.scheduleSessionStoreRetry();
60571
+ }
60179
60572
  async destroy() {
60573
+ this.destroyed = true;
60574
+ if (this.storeRetryTimer) {
60575
+ clearTimeout(this.storeRetryTimer);
60576
+ this.storeRetryTimer = void 0;
60577
+ }
60578
+ this.lastTtlRefreshAt.clear();
60180
60579
  await this.teardownSessionStore();
60181
60580
  }
60182
60581
  /**
@@ -60237,10 +60636,33 @@ var init_transport_registry = __esm({
60237
60636
  getBackendKind() {
60238
60637
  return this.backendKind;
60239
60638
  }
60639
+ /**
60640
+ * Slide the stored session's TTL while this instance is serving it. Redis and Vercel KV
60641
+ * session stores extend the TTL on read, so a plain `get` is enough. Throttled per session
60642
+ * to a quarter of the TTL, fire-and-forget.
60643
+ */
60644
+ refreshStoredSessionTtl(sessionId) {
60645
+ const store = this.sessionStore;
60646
+ const ttlMs = this.getDefaultTtlMs();
60647
+ if (!store || !ttlMs || this.backendKind !== "redis" && this.backendKind !== "vercel-kv") return;
60648
+ const now = Date.now();
60649
+ const last = this.lastTtlRefreshAt.get(sessionId);
60650
+ if (last !== void 0 && now - last < Math.max(1e3, Math.floor(ttlMs / 4))) return;
60651
+ this.lastTtlRefreshAt.set(sessionId, now);
60652
+ store.get(sessionId).catch((err) => {
60653
+ this.scope.logger.warn("[TransportService] Failed to refresh session TTL", {
60654
+ sessionId: sessionId.slice(0, 20),
60655
+ error: err instanceof Error ? err.message : String(err)
60656
+ });
60657
+ });
60658
+ }
60240
60659
  async getTransporter(type, token, sessionId) {
60241
60660
  const key = this.keyOf(type, token, sessionId);
60242
60661
  const local = this.lookupLocal(key);
60243
- if (local) return local;
60662
+ if (local) {
60663
+ this.refreshStoredSessionTtl(sessionId);
60664
+ return local;
60665
+ }
60244
60666
  if (this.distributed && this.bus) {
60245
60667
  const location = await this.bus.lookup(key);
60246
60668
  if (location) {
@@ -60335,7 +60757,7 @@ var init_transport_registry = __esm({
60335
60757
  });
60336
60758
  const currentNodeId = getMachineId5();
60337
60759
  if (this.scope.haManager && storedSession.session.nodeId && storedSession.session.nodeId !== currentNodeId) {
60338
- const sessionKey = `${this.transportKeyPrefix}${sessionId}`;
60760
+ const sessionKey = `${this.getSessionKeyPrefix()}${sessionId}`;
60339
60761
  const result = await this.scope.haManager.attemptTakeover(sessionKey, storedSession.session.nodeId);
60340
60762
  if (!result.claimed) {
60341
60763
  this.scope.logger.debug("[HA] Session already claimed by another pod", { sessionId: sessionId.slice(0, 20) });
@@ -60668,6 +61090,7 @@ var init_transport_registry = __esm({
60668
61090
  }
60669
61091
  }
60670
61092
  evictLocal(key) {
61093
+ this.lastTtlRefreshAt.delete(key.sessionId);
60671
61094
  const typeBucket = this.byType.get(key.type);
60672
61095
  if (!typeBucket) return;
60673
61096
  const tokenBucket = typeBucket.get(key.tokenHash);
@@ -61624,7 +62047,7 @@ var init_http_request_flow = __esm({
61624
62047
  // libs/sdk/src/scope/scope.instance.ts
61625
62048
  import "reflect-metadata";
61626
62049
  import { createGuardManager } from "@frontmcp/guard";
61627
- import { getEnvFlag, getMachineId as getMachineId6, getRuntimeContext as getRuntimeContext15, isEdgeRuntime } from "@frontmcp/utils";
62050
+ import { createRedisClient, getEnvFlag, getMachineId as getMachineId6, getRuntimeContext as getRuntimeContext15, isEdgeRuntime } from "@frontmcp/utils";
61628
62051
  var SCOPE_NON_DISPATCHABLE_FLOWS, Scope;
61629
62052
  var init_scope_instance = __esm({
61630
62053
  "libs/sdk/src/scope/scope.instance.ts"() {
@@ -61669,6 +62092,7 @@ var init_scope_instance = __esm({
61669
62092
  init_tool_registry();
61670
62093
  init_tool_utils();
61671
62094
  init_ui();
62095
+ init_ui_option_warnings();
61672
62096
  init_bus();
61673
62097
  init_event_stores();
61674
62098
  init_request_state();
@@ -61695,6 +62119,8 @@ var init_scope_instance = __esm({
61695
62119
  transportService;
61696
62120
  // TODO: migrate transport service to transport.registry
61697
62121
  notificationService;
62122
+ /** ioredis client owned by the HA manager and transport bus; closed in shutdown(). */
62123
+ haRedisClient;
61698
62124
  toolUIRegistry;
61699
62125
  authUiRegistry;
61700
62126
  entryPath;
@@ -61775,7 +62201,7 @@ var init_scope_instance = __esm({
61775
62201
  const isDistributed = getRuntimeContext15().deployment === "distributed";
61776
62202
  if (isDistributed && this.metadata.redis && !this.cliMode) {
61777
62203
  try {
61778
- const haRedis = this.metadata.redis;
62204
+ const haRedis = this.createHaRedisClient();
61779
62205
  this.haManager = HaManager.create({
61780
62206
  redis: haRedis,
61781
62207
  nodeId: getMachineId6(),
@@ -61785,11 +62211,13 @@ var init_scope_instance = __esm({
61785
62211
  this.logger.info("[HA] Manager started for distributed deployment");
61786
62212
  } catch (err) {
61787
62213
  this.logger.warn("[HA] Failed to start HA manager \u2014 running without HA", { error: err });
62214
+ this.haManager = void 0;
62215
+ await this.closeHaRedisClient();
61788
62216
  }
61789
62217
  }
61790
62218
  let transportBus;
61791
- if (this.haManager && isDistributed && this.metadata.redis) {
61792
- transportBus = new RedisTransportBus(this.metadata.redis, getMachineId6(), {
62219
+ if (this.haManager && this.haRedisClient && isDistributed) {
62220
+ transportBus = new RedisTransportBus(this.haRedisClient, getMachineId6(), {
61793
62221
  logger: this.logger
61794
62222
  });
61795
62223
  this.logger.info("[HA] Transport bus created for distributed session routing");
@@ -61801,11 +62229,9 @@ var init_scope_instance = __esm({
61801
62229
  }
61802
62230
  this.transportService = new TransportService(this, effectivePersistence, transportBus);
61803
62231
  warnIfRequestStateKeyNotShared({ logger: this.logger, metadata: this.metadata });
61804
- if (this.haManager && isDistributed) {
61805
- const persistCfg = transportConfig?.persistence;
61806
- const resolvedPrefix = persistCfg && typeof persistCfg === "object" && persistCfg.redis?.keyPrefix ? persistCfg.redis.keyPrefix : "mcp:transport:";
62232
+ if (this.haManager && isDistributed && this.transportService.getBackendKind() === "redis") {
61807
62233
  this.haManager.startOrphanScanner({
61808
- sessionKeyPrefix: resolvedPrefix,
62234
+ sessionKeyPrefix: this.transportService.getSessionKeyPrefix(),
61809
62235
  onOrphan: (sessionId, previousNodeId) => {
61810
62236
  this.logger.info(
61811
62237
  `[HA] Orphaned session ${sessionId.slice(0, 20)} from ${previousNodeId} \u2014 available for recreation`
@@ -61861,7 +62287,15 @@ var init_scope_instance = __esm({
61861
62287
  "[tasks] Background tasks are unavailable on this edge/serverless runtime: the in-process runner cannot continue past the HTTP response, and an in-memory store is not shared between isolates. Serving without tasks. Configure `tasks.redis` (Upstash Redis over HTTP works on Workers) to enable them."
61862
62288
  );
61863
62289
  }
61864
- const shouldInitTasks = !tasksExplicitlyDisabled && !tasksUnavailableOnEdge && (!this.cliMode || tasksEnabledForCli || isTaskWorker);
62290
+ const gateTasksRedis = tasksConfig?.redis ?? this.metadata.redis;
62291
+ const tasksUseSqlite = Boolean(tasksConfig?.sqlite ?? (gateTasksRedis ? void 0 : resolvedTopLevelSqlite));
62292
+ const tasksUnavailableOnVercelKv = !tasksExplicitlyEnabled && !tasksUseSqlite && resolvesToVercelKvTaskBackend(gateTasksRedis);
62293
+ if (tasksUnavailableOnVercelKv && !tasksExplicitlyDisabled) {
62294
+ this.logger.warn(
62295
+ "[tasks] Background tasks are disabled: Vercel KV has no pub/sub, which task results and cancellation require. Serving without tasks. Configure `tasks.redis` with Redis or Upstash to enable them."
62296
+ );
62297
+ }
62298
+ const shouldInitTasks = !tasksExplicitlyDisabled && !tasksUnavailableOnEdge && !tasksUnavailableOnVercelKv && (!this.cliMode || tasksEnabledForCli || isTaskWorker);
61865
62299
  const tasksPromise = shouldInitTasks ? (async () => {
61866
62300
  const tasksRedis = tasksConfig?.redis ?? this.metadata.redis;
61867
62301
  const tasksSqlite = tasksConfig?.sqlite ?? (tasksRedis ? void 0 : resolvedTopLevelSqlite);
@@ -62360,6 +62794,16 @@ var init_scope_instance = __esm({
62360
62794
  async compileUIWidgets() {
62361
62795
  const toolsWithUI = this.scopeTools.getTools(true).filter((t) => hasUIConfig(t.metadata));
62362
62796
  if (toolsWithUI.length === 0) return;
62797
+ for (const tool of toolsWithUI) {
62798
+ const resourceUri = tool.metadata.ui?.resourceUri;
62799
+ this.toolUIRegistry.registerTool(tool.metadata.name, typeof resourceUri === "string" ? resourceUri : void 0);
62800
+ for (const message of describeIgnoredUiOptions(
62801
+ tool.metadata.name,
62802
+ tool.metadata.ui
62803
+ )) {
62804
+ this.logger.warn(message);
62805
+ }
62806
+ }
62363
62807
  this.scopeResources.registerDynamicResource(StaticWidgetResourceTemplate);
62364
62808
  this.logger.verbose(`Registered UI resource template for ${toolsWithUI.length} tool(s) with UI configs`);
62365
62809
  const staticModeTools = toolsWithUI.filter(
@@ -62938,6 +63382,29 @@ var init_scope_instance = __esm({
62938
63382
  }
62939
63383
  throw new FlowExitedWithoutOutputError();
62940
63384
  }
63385
+ /**
63386
+ * Build the ioredis client shared by the HA manager and the transport bus from `metadata.redis`.
63387
+ * The client reconnects on its own and has a permanent, rate-limited 'error' listener.
63388
+ */
63389
+ createHaRedisClient() {
63390
+ const redis = this.metadata.redis;
63391
+ if (!redis || redis.provider === "vercel-kv") {
63392
+ throw new Error("Distributed HA requires a TCP Redis (ioredis); Vercel KV over REST has no Lua/pub-sub support");
63393
+ }
63394
+ const client = createRedisClient({ ...redis, label: "ha", logger: this.logger });
63395
+ this.haRedisClient = client;
63396
+ return client;
63397
+ }
63398
+ async closeHaRedisClient() {
63399
+ const client = this.haRedisClient;
63400
+ if (!client) return;
63401
+ this.haRedisClient = void 0;
63402
+ try {
63403
+ await client.quit();
63404
+ } catch {
63405
+ client.disconnect();
63406
+ }
63407
+ }
62941
63408
  /**
62942
63409
  * Shut down the scope and release resources.
62943
63410
  * Disconnects channel service connectors, unsubscribes event handlers,
@@ -62951,6 +63418,7 @@ var init_scope_instance = __esm({
62951
63418
  this.logger.error("[HA] Manager shutdown failed", { error: err });
62952
63419
  }
62953
63420
  }
63421
+ await this.closeHaRedisClient();
62954
63422
  if (this._channelTeardown) {
62955
63423
  try {
62956
63424
  await this._channelTeardown();
@@ -64079,6 +64547,177 @@ var init_scope_registry = __esm({
64079
64547
  }
64080
64548
  });
64081
64549
 
64550
+ // libs/sdk/src/transport/ipc-server.transport.ts
64551
+ function isIpcStdioRequested() {
64552
+ return !!process.env["FRONTMCP_DEV_STDIO_FD"] && typeof process.send === "function";
64553
+ }
64554
+ var IPC_READY_MESSAGE, IpcServerTransport;
64555
+ var init_ipc_server_transport = __esm({
64556
+ "libs/sdk/src/transport/ipc-server.transport.ts"() {
64557
+ "use strict";
64558
+ IPC_READY_MESSAGE = { __frontmcp: "ready" };
64559
+ IpcServerTransport = class {
64560
+ onclose;
64561
+ onerror;
64562
+ onmessage;
64563
+ started = false;
64564
+ handleMessage = (raw) => {
64565
+ if (!raw || typeof raw !== "object" || raw.__frontmcp !== void 0) return;
64566
+ this.onmessage?.(raw);
64567
+ };
64568
+ handleDisconnect = () => {
64569
+ this.onclose?.();
64570
+ };
64571
+ async start() {
64572
+ if (this.started) throw new Error("IpcServerTransport already started");
64573
+ if (typeof process.send !== "function") {
64574
+ throw new Error("IpcServerTransport requires a process forked with an IPC channel");
64575
+ }
64576
+ this.started = true;
64577
+ process.on("message", this.handleMessage);
64578
+ process.once("disconnect", this.handleDisconnect);
64579
+ process.send(IPC_READY_MESSAGE);
64580
+ }
64581
+ async send(message) {
64582
+ const send = process.send;
64583
+ if (!process.connected || typeof send !== "function") {
64584
+ throw new Error("IPC channel is closed");
64585
+ }
64586
+ await new Promise((resolve, reject) => {
64587
+ send.call(process, message, (err) => err ? reject(err) : resolve());
64588
+ });
64589
+ }
64590
+ async close() {
64591
+ process.off("message", this.handleMessage);
64592
+ process.off("disconnect", this.handleDisconnect);
64593
+ this.started = false;
64594
+ this.onclose?.();
64595
+ }
64596
+ };
64597
+ }
64598
+ });
64599
+
64600
+ // libs/sdk/src/server/middleware/csp.middleware.ts
64601
+ import { getEnv as getEnv8, getEnvFlag as getEnvFlag2 } from "@frontmcp/utils";
64602
+ function readCspFromEnv() {
64603
+ if (!getEnvFlag2("FRONTMCP_CSP_ENABLED")) return void 0;
64604
+ const rawDirectives = getEnv8("FRONTMCP_CSP_DIRECTIVES") ?? "";
64605
+ const directives = {};
64606
+ for (const part of rawDirectives.split(";")) {
64607
+ const trimmed = part.trim();
64608
+ if (!trimmed) continue;
64609
+ const spaceIdx = trimmed.indexOf(" ");
64610
+ if (spaceIdx > 0) {
64611
+ directives[trimmed.slice(0, spaceIdx)] = trimmed.slice(spaceIdx + 1);
64612
+ } else {
64613
+ directives[trimmed] = "";
64614
+ }
64615
+ }
64616
+ return {
64617
+ enabled: true,
64618
+ directives,
64619
+ reportUri: getEnv8("FRONTMCP_CSP_REPORT_URI"),
64620
+ reportOnly: getEnvFlag2("FRONTMCP_CSP_REPORT_ONLY")
64621
+ };
64622
+ }
64623
+ function buildCspHeaderValue(options) {
64624
+ const parts = [];
64625
+ for (const [directive, value] of Object.entries(options.directives)) {
64626
+ parts.push(value ? `${directive} ${value}` : directive);
64627
+ }
64628
+ if (options.reportUri) {
64629
+ parts.push(`report-uri ${options.reportUri}`);
64630
+ }
64631
+ return parts.join("; ");
64632
+ }
64633
+ function getCspHeaderName(reportOnly) {
64634
+ return reportOnly ? "Content-Security-Policy-Report-Only" : "Content-Security-Policy";
64635
+ }
64636
+ function readSecurityHeadersFromEnv() {
64637
+ return {
64638
+ hsts: readHeaderEnv("FRONTMCP_HSTS"),
64639
+ contentTypeOptions: readHeaderEnv("FRONTMCP_CONTENT_TYPE_OPTIONS", "nosniff"),
64640
+ frameOptions: readHeaderEnv("FRONTMCP_FRAME_OPTIONS", "DENY"),
64641
+ custom: readCustomHeadersFromEnv()
64642
+ };
64643
+ }
64644
+ function readHeaderEnv(name44, fallback) {
64645
+ const value = getEnv8(name44);
64646
+ if (value === void 0 || value === "") return fallback;
64647
+ return ["off", "false", "none"].includes(value.trim().toLowerCase()) ? void 0 : value;
64648
+ }
64649
+ function readCustomHeadersFromEnv() {
64650
+ const raw = getEnv8("FRONTMCP_HEADERS_CUSTOM");
64651
+ if (!raw) return void 0;
64652
+ try {
64653
+ const parsed = JSON.parse(raw);
64654
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
64655
+ return Object.fromEntries(
64656
+ Object.entries(parsed).filter((entry) => typeof entry[1] === "string")
64657
+ );
64658
+ } catch {
64659
+ return void 0;
64660
+ }
64661
+ }
64662
+ function applySecurityHeaders(res, headers, csp) {
64663
+ if (headers.hsts) {
64664
+ res.setHeader("Strict-Transport-Security", headers.hsts);
64665
+ }
64666
+ if (headers.contentTypeOptions) {
64667
+ res.setHeader("X-Content-Type-Options", headers.contentTypeOptions);
64668
+ }
64669
+ if (headers.frameOptions) {
64670
+ res.setHeader("X-Frame-Options", headers.frameOptions);
64671
+ }
64672
+ if (headers.custom) {
64673
+ for (const [name44, value] of Object.entries(headers.custom)) {
64674
+ res.setHeader(name44, value);
64675
+ }
64676
+ }
64677
+ if (csp?.enabled) {
64678
+ const headerName = getCspHeaderName(csp.reportOnly);
64679
+ const headerValue = buildCspHeaderValue(csp);
64680
+ if (headerValue) {
64681
+ res.setHeader(headerName, headerValue);
64682
+ }
64683
+ }
64684
+ }
64685
+ function resolveSecurityHeaders(options = {}) {
64686
+ const env = readSecurityHeadersFromEnv();
64687
+ const pick = (explicit, fromEnv) => explicit === false ? void 0 : explicit ?? fromEnv;
64688
+ const resolved = {
64689
+ hsts: pick(options.hsts, env.hsts),
64690
+ contentTypeOptions: pick(options.contentTypeOptions, env.contentTypeOptions),
64691
+ frameOptions: pick(options.frameOptions, env.frameOptions),
64692
+ custom: { ...env.custom, ...options.custom }
64693
+ };
64694
+ let csp;
64695
+ if (options.csp?.enabled === false) {
64696
+ csp = void 0;
64697
+ } else if (options.csp?.enabled === true) {
64698
+ const directives = {};
64699
+ for (const [name44, value] of Object.entries(options.csp.directives ?? {})) {
64700
+ directives[name44] = Array.isArray(value) ? value.join(" ") : value;
64701
+ }
64702
+ csp = {
64703
+ enabled: true,
64704
+ directives,
64705
+ reportUri: options.csp.reportUri,
64706
+ reportOnly: options.csp.reportOnly ?? false
64707
+ };
64708
+ } else {
64709
+ csp = readCspFromEnv();
64710
+ }
64711
+ const headers = {};
64712
+ applySecurityHeaders({ setHeader: (name44, value) => void (headers[name44] = value) }, resolved, csp);
64713
+ return headers;
64714
+ }
64715
+ var init_csp_middleware = __esm({
64716
+ "libs/sdk/src/server/middleware/csp.middleware.ts"() {
64717
+ "use strict";
64718
+ }
64719
+ });
64720
+
64082
64721
  // libs/sdk/src/transport/web-response.renderer.ts
64083
64722
  function toByteStream(streamLike) {
64084
64723
  const iter = streamLike[Symbol.asyncIterator];
@@ -64184,6 +64823,7 @@ function isCloudflareWorkersRuntime(request) {
64184
64823
  return typeof cloudflareProperties === "object" && cloudflareProperties !== null;
64185
64824
  }
64186
64825
  function resolvePeerAddress(request, ctx) {
64826
+ if (carriedPeerAddresses.has(request)) return carriedPeerAddresses.get(request);
64187
64827
  if (typeof ctx?.requestIP === "function") return ctx.requestIP(request)?.address;
64188
64828
  if (typeof ctx?.remoteAddr?.hostname === "string") return ctx.remoteAddr.hostname;
64189
64829
  if (isCloudflareWorkersRuntime(request)) return request.headers.get("cf-connecting-ip") ?? void 0;
@@ -64202,7 +64842,13 @@ function mapHttpCors(httpCors, scope) {
64202
64842
  }
64203
64843
  function createWebFetchHandler(scope, options = {}) {
64204
64844
  const httpConfig = scope.metadata.http;
64205
- const healthPaths = new Set(options.healthPaths ?? ["/healthz", "/readyz"]);
64845
+ const healthConfig = scope.metadata.health ?? {};
64846
+ const healthEnabled = healthConfig.enabled !== false;
64847
+ const healthzPath = healthConfig.healthzPath ?? "/healthz";
64848
+ const readyzPath = healthConfig.readyzPath ?? "/readyz";
64849
+ const readyzEnabled = isReadyzEnabled(healthConfig);
64850
+ const bodyLimit = parseByteLimit(httpConfig?.bodyLimit);
64851
+ const securityHeaders = resolveSecurityHeaders(httpConfig?.securityHeaders);
64206
64852
  const normalizePath = (p) => {
64207
64853
  const withSlash = p.startsWith("/") ? p : `/${p}`;
64208
64854
  let end = withSlash.length;
@@ -64246,7 +64892,13 @@ function createWebFetchHandler(scope, options = {}) {
64246
64892
  for (const [k, v] of Object.entries(corsHeadersFor(request))) headers.set(k, v);
64247
64893
  return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
64248
64894
  };
64249
- return async function handle(request, ctx, env) {
64895
+ const withSecurityHeaders = (response) => {
64896
+ const headers = new Headers(response.headers);
64897
+ for (const [k, v] of Object.entries(securityHeaders)) if (!headers.has(k)) headers.set(k, v);
64898
+ headers.delete("x-powered-by");
64899
+ return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
64900
+ };
64901
+ const handleRequest = async (request, ctx, env) => {
64250
64902
  const url = new URL(request.url);
64251
64903
  if (hostValidation) {
64252
64904
  const rejection = validateHostHeaders(
@@ -64261,17 +64913,34 @@ function createWebFetchHandler(scope, options = {}) {
64261
64913
  return Response.json({ error: rejection.error, message: rejection.message }, { status: rejection.status });
64262
64914
  }
64263
64915
  }
64916
+ const limited = await enforceBodyLimit(request, bodyLimit, ctx);
64917
+ if (limited instanceof Response) return withCors(limited, request);
64918
+ request = limited;
64264
64919
  if (corsEnabled && request.method === "OPTIONS") {
64265
64920
  return new Response(null, { status: 204, headers: corsHeadersFor(request) });
64266
64921
  }
64267
- if (healthPaths.has(url.pathname)) {
64268
- return withCors(
64269
- Response.json(
64270
- { status: "ok", server: scope.metadata.info, transport: "web-fetch" },
64271
- { headers: { "Cache-Control": "no-store" } }
64272
- ),
64273
- request
64274
- );
64922
+ if (options.healthPaths ? options.healthPaths.includes(url.pathname) : healthEnabled) {
64923
+ const isReadyz = !options.healthPaths && url.pathname === readyzPath && readyzEnabled;
64924
+ const isHealthz = options.healthPaths ? url.pathname !== "/readyz" : url.pathname === healthzPath || url.pathname === "/health";
64925
+ if (isReadyz && scope.healthService) {
64926
+ const readiness = await scope.healthService.getReadyz();
64927
+ return withCors(
64928
+ Response.json(
64929
+ { ...readiness, transport: "web-fetch" },
64930
+ { status: readiness.status === "ready" ? 200 : 503, headers: { "Cache-Control": "no-store" } }
64931
+ ),
64932
+ request
64933
+ );
64934
+ }
64935
+ if (isHealthz || isReadyz || options.healthPaths) {
64936
+ return withCors(
64937
+ Response.json(
64938
+ { status: "ok", server: scope.metadata.info, transport: "web-fetch" },
64939
+ { headers: { "Cache-Control": "no-store" } }
64940
+ ),
64941
+ request
64942
+ );
64943
+ }
64275
64944
  }
64276
64945
  if (!entryPaths.has(normalizePath(url.pathname))) {
64277
64946
  const authResponse = await runMatchingHttpFlowWeb(scope, request, { ctx, env });
@@ -64285,6 +64954,53 @@ function createWebFetchHandler(scope, options = {}) {
64285
64954
  const rendered = await runHttpRequestFlowWeb(scope, request, { ctx, env });
64286
64955
  return withCors(rendered ?? Response.json({ error: "Not Found" }, { status: 404 }), request);
64287
64956
  };
64957
+ return async function handle(request, ctx, env) {
64958
+ return withSecurityHeaders(await handleRequest(request, ctx, env));
64959
+ };
64960
+ }
64961
+ function parseByteLimit(limit) {
64962
+ if (typeof limit === "number" && Number.isFinite(limit) && limit >= 0) return limit;
64963
+ if (typeof limit === "string") {
64964
+ const match = /^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/i.exec(limit.trim());
64965
+ if (match) return Math.floor(Number(match[1]) * BYTE_UNITS[(match[2] ?? "b").toLowerCase()]);
64966
+ }
64967
+ return DEFAULT_BODY_LIMIT_BYTES;
64968
+ }
64969
+ function payloadTooLarge(limit, length) {
64970
+ const error = new PayloadTooLargeError(limit, length);
64971
+ return Response.json({ jsonrpc: "2.0", error: error.toJsonRpcError(), id: null }, { status: 413 });
64972
+ }
64973
+ async function enforceBodyLimit(request, limit, ctx) {
64974
+ const method = request.method.toUpperCase();
64975
+ if (method === "GET" || method === "HEAD" || method === "OPTIONS" || !request.body) return request;
64976
+ const declared = request.headers.get("content-length");
64977
+ if (declared !== null) {
64978
+ const length = Number(declared);
64979
+ if (Number.isFinite(length) && length > limit) return payloadTooLarge(limit, length);
64980
+ }
64981
+ const peerAddress = resolvePeerAddress(request, ctx);
64982
+ const reader = request.body.getReader();
64983
+ const chunks = [];
64984
+ let total = 0;
64985
+ for (; ; ) {
64986
+ const { done, value } = await reader.read();
64987
+ if (done) break;
64988
+ total += value.byteLength;
64989
+ if (total > limit) {
64990
+ await reader.cancel().catch(() => void 0);
64991
+ return payloadTooLarge(limit, total);
64992
+ }
64993
+ chunks.push(value);
64994
+ }
64995
+ const buffered = new Uint8Array(total);
64996
+ let offset = 0;
64997
+ for (const chunk of chunks) {
64998
+ buffered.set(chunk, offset);
64999
+ offset += chunk.byteLength;
65000
+ }
65001
+ const rebuilt = new Request(request.url, { method: request.method, headers: request.headers, body: buffered });
65002
+ carriedPeerAddresses.set(rebuilt, peerAddress);
65003
+ return rebuilt;
64288
65004
  }
64289
65005
  async function runHttpRequestFlowWeb(scope, request, opts = {}) {
64290
65006
  const url = new URL(request.url);
@@ -64375,22 +65091,7 @@ async function toServerRequest(request, url, ctx, persistent, env) {
64375
65091
  function misconfigurationResponse(error) {
64376
65092
  const misconfiguration = findMisconfiguration(error);
64377
65093
  if (!misconfiguration) return void 0;
64378
- return Response.json(
64379
- { error: "server_misconfigured", code: misconfiguration.code, message: misconfiguration.remedy },
64380
- { status: 500 }
64381
- );
64382
- }
64383
- function findMisconfiguration(error) {
64384
- let current = error;
64385
- for (let depth = 0; current instanceof Error && depth < 5; depth++) {
64386
- const code = current.code;
64387
- if (typeof code === "string" && MISCONFIGURATION_REMEDIES[code]) {
64388
- return { code, remedy: MISCONFIGURATION_REMEDIES[code] };
64389
- }
64390
- if (current.name === "ZodError") return { code: "CONFIG_INVALID", remedy: CONFIG_INVALID_REMEDY };
64391
- current = current.cause;
64392
- }
64393
- return void 0;
65094
+ return Response.json(misconfigurationBody(misconfiguration), { status: 500 });
64394
65095
  }
64395
65096
  function createDeferredServerBuild(build, options = {}) {
64396
65097
  const now = options.now ?? Date.now;
@@ -64445,7 +65146,7 @@ function flowErrorToHttpOutput(error) {
64445
65146
  kind: "json",
64446
65147
  status: 500,
64447
65148
  contentType: "application/json; charset=utf-8",
64448
- body: { error: "server_misconfigured", code: misconfiguration.code, message: misconfiguration.remedy }
65149
+ body: misconfigurationBody(misconfiguration)
64449
65150
  };
64450
65151
  }
64451
65152
  if (error instanceof FlowControl) {
@@ -64471,7 +65172,7 @@ function flowErrorToHttpOutput(error) {
64471
65172
  }
64472
65173
  return { kind: "text", status: 500, body: "Internal Server Error", contentType: "text/plain; charset=utf-8" };
64473
65174
  }
64474
- var CLOUDFLARE_WORKERS_USER_AGENT, MISCONFIGURATION_REMEDIES, CONFIG_INVALID_REMEDY, STARTUP_RETRY_MIN_MS, STARTUP_RETRY_MAX_MS;
65175
+ var CLOUDFLARE_WORKERS_USER_AGENT, carriedPeerAddresses, DEFAULT_BODY_LIMIT_BYTES, BYTE_UNITS, STARTUP_RETRY_MIN_MS, STARTUP_RETRY_MAX_MS;
64475
65176
  var init_web_fetch_handler = __esm({
64476
65177
  "libs/sdk/src/transport/web-fetch-handler.ts"() {
64477
65178
  "use strict";
@@ -64479,18 +65180,15 @@ var init_web_fetch_handler = __esm({
64479
65180
  init_server_tokens();
64480
65181
  init_path_utils();
64481
65182
  init_errors();
65183
+ init_misconfiguration();
65184
+ init_health_routes();
65185
+ init_csp_middleware();
64482
65186
  init_host_validation();
64483
65187
  init_web_response_renderer();
64484
65188
  CLOUDFLARE_WORKERS_USER_AGENT = "Cloudflare-Workers";
64485
- MISCONFIGURATION_REMEDIES = {
64486
- SESSION_SECRET_REQUIRED: "Set MCP_SESSION_SECRET in the deployment environment (e.g. `wrangler secret put MCP_SESSION_SECRET`). Session IDs are encrypted with it, and production refuses the development machine-id fallback.",
64487
- JWT_SECRET_INVALID: "JWT_SECRET is present but too weak: HS256 requires at least 32 bytes (RFC 7518). Replace it with `openssl rand -hex 32`.",
64488
- JWT_SECRET_REQUIRED: "Set JWT_SECRET in the deployment environment (e.g. `wrangler secret put JWT_SECRET`). Tokens are signed with it; production refuses the random per-process fallback because tokens would not survive a restart or verify across instances.",
64489
- // The startup checks: a server whose entries ask for protection nothing gives them does not start.
64490
- UNENFORCED_METADATA: "An entry declares a field only a plugin enforces (approval, featureFlag, ...) and no installed plugin that enforces it reaches the entry, so the server refuses to start. Install the plugin or remove the field; the server log names the entries.",
64491
- AUTH_CONFIGURATION_ERROR: "The auth or authorities configuration is invalid (for example, entries declare authorities and the server has no authorities option, or a rule checks nothing), so the server refuses to start. The server log names the entries."
64492
- };
64493
- CONFIG_INVALID_REMEDY = "The FrontMCP configuration failed validation, so the server refuses to start. The server log names the invalid fields.";
65189
+ carriedPeerAddresses = /* @__PURE__ */ new WeakMap();
65190
+ DEFAULT_BODY_LIMIT_BYTES = 4 * 1024 * 1024;
65191
+ BYTE_UNITS = { b: 1, kb: 1024, mb: 1024 ** 2, gb: 1024 ** 3 };
64494
65192
  STARTUP_RETRY_MIN_MS = 1e3;
64495
65193
  STARTUP_RETRY_MAX_MS = 6e4;
64496
65194
  }
@@ -64572,13 +65270,13 @@ function auditSecurityDefaults(config, isProduction7) {
64572
65270
  message: `Server bound to ${bindAddress}.`
64573
65271
  });
64574
65272
  }
64575
- const dnsProtectionEnabled = config.security?.dnsRebindingProtection?.enabled ?? strict;
65273
+ const dnsProtectionEnabled = config.security?.dnsRebindingProtection?.enabled !== false;
64576
65274
  if (!dnsProtectionEnabled) {
64577
65275
  findings.push({
64578
65276
  level: "warn",
64579
65277
  code: "DNS_REBINDING_UNPROTECTED",
64580
- message: "DNS rebinding protection is disabled.",
64581
- recommendation: "Enable security.dnsRebindingProtection with allowedHosts to prevent DNS rebinding attacks."
65278
+ message: "DNS rebinding protection is disabled (security.dnsRebindingProtection.enabled is false).",
65279
+ recommendation: "Remove `enabled: false`, or set security.dnsRebindingProtection.allowedHosts, to prevent DNS rebinding attacks."
64582
65280
  });
64583
65281
  } else {
64584
65282
  findings.push({
@@ -64767,6 +65465,7 @@ var init_express_host_adapter = __esm({
64767
65465
  "libs/sdk/src/server/adapters/express.host.adapter.ts"() {
64768
65466
  "use strict";
64769
65467
  init_mcp_error();
65468
+ init_csp_middleware();
64770
65469
  init_host_validation_middleware();
64771
65470
  init_resolve_allowed_hosts();
64772
65471
  init_base_host_adapter();
@@ -64781,6 +65480,12 @@ var init_express_host_adapter = __esm({
64781
65480
  deriveHostValidation;
64782
65481
  constructor(options) {
64783
65482
  super();
65483
+ this.app.disable("x-powered-by");
65484
+ const securityHeaders = resolveSecurityHeaders(options?.securityHeaders);
65485
+ this.app.use((_req, res, next) => {
65486
+ for (const [name44, value] of Object.entries(securityHeaders)) res.setHeader(name44, value);
65487
+ next();
65488
+ });
64784
65489
  const corsOptions = options?.cors;
64785
65490
  const corsEnabled = corsOptions?.origin !== void 0 && corsOptions.origin !== false;
64786
65491
  if (corsEnabled) {
@@ -65008,6 +65713,7 @@ var init_server_instance = __esm({
65008
65713
  this.host = new ExpressHostAdapter2({
65009
65714
  ...corsConfig ? { cors: corsConfig } : {},
65010
65715
  ...this.config.security ? { security: this.config.security } : {},
65716
+ ...this.config.securityHeaders ? { securityHeaders: this.config.securityHeaders } : {},
65011
65717
  ...this.config.bodyLimit !== void 0 ? { bodyLimit: this.config.bodyLimit } : {},
65012
65718
  ...this.config.urlencodedLimit !== void 0 ? { urlencodedLimit: this.config.urlencodedLimit } : {}
65013
65719
  });
@@ -65332,6 +66038,7 @@ var init_front_mcp = __esm({
65332
66038
  init_scope_registry();
65333
66039
  init_skill_instructions_helper();
65334
66040
  init_task();
66041
+ init_ipc_server_transport();
65335
66042
  init_web_fetch_handler();
65336
66043
  init_front_mcp_providers();
65337
66044
  init_static_startup_check();
@@ -65451,7 +66158,13 @@ var init_front_mcp = __esm({
65451
66158
  }
65452
66159
  }
65453
66160
  static async bootstrap(options) {
65454
- const parsedConfig = frontMcpMetadataSchema.parse(options);
66161
+ const sqlitePath = process.env["FRONTMCP_SQLITE_PATH"];
66162
+ const parsedConfig = frontMcpMetadataSchema.parse(
66163
+ sqlitePath ? {
66164
+ ...options,
66165
+ sqlite: { ...options.sqlite ?? {}, path: sqlitePath }
66166
+ } : options
66167
+ );
65455
66168
  const daemonSocket = process.env["FRONTMCP_DAEMON_SOCKET"];
65456
66169
  if (daemonSocket) {
65457
66170
  await _FrontMcpInstance2.runUnixSocket({ ...parsedConfig, socketPath: daemonSocket });
@@ -65828,7 +66541,7 @@ var init_front_mcp = __esm({
65828
66541
  };
65829
66542
  mcpServer.setRequestHandler(handler.requestSchema, wrappedHandler);
65830
66543
  }
65831
- const transport = new StdioServerTransport();
66544
+ const transport = isIpcStdioRequested() ? new IpcServerTransport() : new StdioServerTransport();
65832
66545
  startupCleanups.push(() => transport.close());
65833
66546
  await mcpServer.connect(transport);
65834
66547
  startupSucceeded = true;
@@ -66081,7 +66794,7 @@ var init_execute_task = __esm({
66081
66794
 
66082
66795
  // libs/sdk/src/common/decorators/front-mcp.decorator.ts
66083
66796
  import "reflect-metadata";
66084
- import { getEnvFlag as getEnvFlag2 } from "@frontmcp/utils";
66797
+ import { getEnvFlag as getEnvFlag3 } from "@frontmcp/utils";
66085
66798
  function getDecoratorConfig(target) {
66086
66799
  if (typeof target !== "function") return void 0;
66087
66800
  if (typeof Reflect === "undefined") return void 0;
@@ -66136,9 +66849,9 @@ function FrontMcp(providedMetadata) {
66136
66849
  Reflect.defineMetadata(FrontMcpTokens[property] ?? property, metadata[property], target);
66137
66850
  }
66138
66851
  Reflect.defineMetadata(FRONTMCP_CONFIG_METADATA_KEY, metadata, target);
66139
- const isServerless2 = getEnvFlag2("FRONTMCP_SERVERLESS");
66140
- const isSchemaExtract = getEnvFlag2("FRONTMCP_SCHEMA_EXTRACT");
66141
- const isStdio = getEnvFlag2("FRONTMCP_STDIO");
66852
+ const isServerless2 = getEnvFlag3("FRONTMCP_SERVERLESS");
66853
+ const isSchemaExtract = getEnvFlag3("FRONTMCP_SCHEMA_EXTRACT");
66854
+ const isStdio = getEnvFlag3("FRONTMCP_STDIO");
66142
66855
  const taskWorkerId = typeof process !== "undefined" ? process.env?.["FRONTMCP_RUN_TASK_ID"] : void 0;
66143
66856
  if (taskWorkerId && metadata.serve !== false) {
66144
66857
  void (async () => {
@@ -66164,7 +66877,7 @@ function FrontMcp(providedMetadata) {
66164
66877
  } else if (isServerless2) {
66165
66878
  const ServerlessInstance = getFrontMcpInstance();
66166
66879
  const { setServerlessHandler: setServerlessHandler2, setServerlessHandlerPromise: setServerlessHandlerPromise2, setServerlessHandlerError: setServerlessHandlerError2 } = getServerlessHandlerFns();
66167
- const isWorker = getEnvFlag2("FRONTMCP_WORKER");
66880
+ const isWorker = getEnvFlag3("FRONTMCP_WORKER");
66168
66881
  const handlerPromise = isWorker ? ServerlessInstance.createFetchHandler(metadata) : ServerlessInstance.createHandler(metadata);
66169
66882
  setServerlessHandlerPromise2(handlerPromise);
66170
66883
  handlerPromise.then(setServerlessHandler2).catch((err) => {
@@ -66888,7 +67601,7 @@ var init_dynamic_adapter = __esm({
66888
67601
  * @param options - Adapter options including required `name` field
66889
67602
  * @throws Error if `name` is missing/empty or if a duplicate name is detected
66890
67603
  */
66891
- static init(options) {
67604
+ static init(options = {}) {
66892
67605
  const typedOptions = options;
66893
67606
  const adapterName = typedOptions.name;
66894
67607
  if (!adapterName || typeof adapterName !== "string" || adapterName.trim() === "") {
@@ -69065,6 +69778,7 @@ export {
69065
69778
  resolveSchemaDescriptionFormat,
69066
69779
  resolveToolVisibility,
69067
69780
  resolveWithFallbacks,
69781
+ resolvesToVercelKvTaskBackend,
69068
69782
  frontMcpResource as resource,
69069
69783
  frontMcpResourceTemplate as resourceTemplate,
69070
69784
  resourceUriMatches,