@frontmcp/sdk 0.5.1 → 0.6.1

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 (265) hide show
  1. package/README.md +1 -0
  2. package/package.json +12 -16
  3. package/src/adapter/adapter.instance.js +5 -0
  4. package/src/adapter/adapter.instance.js.map +1 -1
  5. package/src/auth/authorization/authorization.class.d.ts +1 -4
  6. package/src/auth/authorization/authorization.class.js +6 -13
  7. package/src/auth/authorization/authorization.class.js.map +1 -1
  8. package/src/auth/flows/session.verify.flow.d.ts +1 -0
  9. package/src/auth/flows/session.verify.flow.js +11 -1
  10. package/src/auth/flows/session.verify.flow.js.map +1 -1
  11. package/src/auth/flows/well-known.jwks.flow.js +2 -2
  12. package/src/auth/flows/well-known.jwks.flow.js.map +1 -1
  13. package/src/auth/jwks/dev-key-persistence.d.ts +63 -0
  14. package/src/auth/jwks/dev-key-persistence.js +219 -0
  15. package/src/auth/jwks/dev-key-persistence.js.map +1 -0
  16. package/src/auth/jwks/index.d.ts +1 -0
  17. package/src/auth/jwks/index.js +1 -0
  18. package/src/auth/jwks/index.js.map +1 -1
  19. package/src/auth/jwks/jwks.service.d.ts +7 -4
  20. package/src/auth/jwks/jwks.service.js +81 -12
  21. package/src/auth/jwks/jwks.service.js.map +1 -1
  22. package/src/auth/jwks/jwks.types.d.ts +7 -0
  23. package/src/auth/jwks/jwks.types.js.map +1 -1
  24. package/src/auth/machine-id.d.ts +5 -0
  25. package/src/auth/machine-id.js +32 -0
  26. package/src/auth/machine-id.js.map +1 -0
  27. package/src/auth/session/index.d.ts +2 -0
  28. package/src/auth/session/index.js +5 -1
  29. package/src/auth/session/index.js.map +1 -1
  30. package/src/auth/session/record/session.base.js +5 -3
  31. package/src/auth/session/record/session.base.js.map +1 -1
  32. package/src/auth/session/record/session.stateless.d.ts +2 -2
  33. package/src/auth/session/record/session.stateless.js +5 -3
  34. package/src/auth/session/record/session.stateless.js.map +1 -1
  35. package/src/auth/session/redis-session.store.d.ts +64 -0
  36. package/src/auth/session/redis-session.store.js +204 -0
  37. package/src/auth/session/redis-session.store.js.map +1 -0
  38. package/src/auth/session/session.service.d.ts +0 -2
  39. package/src/auth/session/session.service.js +1 -7
  40. package/src/auth/session/session.service.js.map +1 -1
  41. package/src/auth/session/transport-session.manager.js +3 -5
  42. package/src/auth/session/transport-session.manager.js.map +1 -1
  43. package/src/auth/session/transport-session.types.d.ts +4 -0
  44. package/src/auth/session/transport-session.types.js +4 -3
  45. package/src/auth/session/transport-session.types.js.map +1 -1
  46. package/src/auth/session/utils/session-id.utils.d.ts +12 -1
  47. package/src/auth/session/utils/session-id.utils.js +48 -9
  48. package/src/auth/session/utils/session-id.utils.js.map +1 -1
  49. package/src/auth/session/vercel-kv-session.store.d.ts +96 -0
  50. package/src/auth/session/vercel-kv-session.store.js +216 -0
  51. package/src/auth/session/vercel-kv-session.store.js.map +1 -0
  52. package/src/auth/ui/base-layout.d.ts +0 -8
  53. package/src/auth/ui/base-layout.js +1 -14
  54. package/src/auth/ui/base-layout.js.map +1 -1
  55. package/src/auth/ui/index.d.ts +3 -4
  56. package/src/auth/ui/index.js +10 -11
  57. package/src/auth/ui/index.js.map +1 -1
  58. package/src/auth/ui/{htmx-templates.d.ts → templates.d.ts} +5 -6
  59. package/src/auth/ui/{htmx-templates.js → templates.js} +8 -15
  60. package/src/auth/ui/templates.js.map +1 -0
  61. package/src/common/decorators/decorator-utils.js.map +1 -1
  62. package/src/common/decorators/front-mcp.decorator.js +26 -3
  63. package/src/common/decorators/front-mcp.decorator.js.map +1 -1
  64. package/src/common/index.d.ts +0 -1
  65. package/src/common/index.js +0 -1
  66. package/src/common/index.js.map +1 -1
  67. package/src/common/interfaces/adapter.interface.d.ts +6 -0
  68. package/src/common/interfaces/adapter.interface.js.map +1 -1
  69. package/src/common/interfaces/execution-context.interface.d.ts +52 -3
  70. package/src/common/interfaces/execution-context.interface.js +88 -3
  71. package/src/common/interfaces/execution-context.interface.js.map +1 -1
  72. package/src/common/interfaces/flow.interface.d.ts +13 -0
  73. package/src/common/interfaces/flow.interface.js +24 -0
  74. package/src/common/interfaces/flow.interface.js.map +1 -1
  75. package/src/common/interfaces/server.interface.d.ts +9 -0
  76. package/src/common/interfaces/server.interface.js.map +1 -1
  77. package/src/common/metadata/app.metadata.d.ts +108 -0
  78. package/src/common/metadata/front-mcp.metadata.d.ts +1341 -2
  79. package/src/common/metadata/front-mcp.metadata.js +4 -1
  80. package/src/common/metadata/front-mcp.metadata.js.map +1 -1
  81. package/src/common/metadata/prompt.metadata.d.ts +4 -0
  82. package/src/common/metadata/provider.metadata.d.ts +14 -0
  83. package/src/common/metadata/provider.metadata.js +18 -2
  84. package/src/common/metadata/provider.metadata.js.map +1 -1
  85. package/src/common/metadata/resource.metadata.d.ts +8 -0
  86. package/src/common/metadata/tool-ui.metadata.d.ts +2 -2
  87. package/src/common/metadata/tool-ui.metadata.js +1 -1
  88. package/src/common/metadata/tool-ui.metadata.js.map +1 -1
  89. package/src/common/metadata/tool.metadata.d.ts +5 -1
  90. package/src/common/metadata/tool.metadata.js.map +1 -1
  91. package/src/common/migrate/auth-transport.migrate.d.ts +62 -0
  92. package/src/common/migrate/auth-transport.migrate.js +140 -0
  93. package/src/common/migrate/auth-transport.migrate.js.map +1 -0
  94. package/src/common/migrate/index.d.ts +1 -0
  95. package/src/common/migrate/index.js +6 -0
  96. package/src/common/migrate/index.js.map +1 -0
  97. package/src/common/schemas/http-output.schema.d.ts +24 -6
  98. package/src/common/schemas/index.d.ts +1 -0
  99. package/src/common/schemas/index.js +1 -0
  100. package/src/common/schemas/index.js.map +1 -1
  101. package/src/common/schemas/session-header.schema.d.ts +16 -0
  102. package/src/common/schemas/session-header.schema.js +42 -0
  103. package/src/common/schemas/session-header.schema.js.map +1 -0
  104. package/src/common/tokens/front-mcp.tokens.js +4 -1
  105. package/src/common/tokens/front-mcp.tokens.js.map +1 -1
  106. package/src/common/types/options/auth.options.d.ts +233 -3
  107. package/src/common/types/options/auth.options.js +29 -40
  108. package/src/common/types/options/auth.options.js.map +1 -1
  109. package/src/common/types/options/index.d.ts +2 -0
  110. package/src/common/types/options/index.js +2 -0
  111. package/src/common/types/options/index.js.map +1 -1
  112. package/src/common/types/options/redis.options.d.ts +190 -0
  113. package/src/common/types/options/redis.options.js +191 -0
  114. package/src/common/types/options/redis.options.js.map +1 -0
  115. package/src/common/types/options/server-info.options.d.ts +4 -0
  116. package/src/common/types/options/transport.options.d.ts +148 -0
  117. package/src/common/types/options/transport.options.js +121 -0
  118. package/src/common/types/options/transport.options.js.map +1 -0
  119. package/src/common/utils/global-config.utils.d.ts +36 -0
  120. package/src/common/utils/global-config.utils.js +44 -0
  121. package/src/common/utils/global-config.utils.js.map +1 -0
  122. package/src/common/utils/index.d.ts +1 -0
  123. package/src/common/utils/index.js +1 -0
  124. package/src/common/utils/index.js.map +1 -1
  125. package/src/completion/flows/complete.flow.d.ts +6 -8
  126. package/src/context/frontmcp-context-storage.d.ts +94 -0
  127. package/src/context/frontmcp-context-storage.js +183 -0
  128. package/src/context/frontmcp-context-storage.js.map +1 -0
  129. package/src/context/frontmcp-context.d.ts +269 -0
  130. package/src/context/frontmcp-context.js +360 -0
  131. package/src/context/frontmcp-context.js.map +1 -0
  132. package/src/context/frontmcp-context.provider.d.ts +43 -0
  133. package/src/context/frontmcp-context.provider.js +61 -0
  134. package/src/context/frontmcp-context.provider.js.map +1 -0
  135. package/src/context/index.d.ts +34 -0
  136. package/src/context/index.js +64 -0
  137. package/src/context/index.js.map +1 -0
  138. package/src/context/request-context-storage.d.ts +89 -0
  139. package/src/context/request-context-storage.js +183 -0
  140. package/src/context/request-context-storage.js.map +1 -0
  141. package/src/context/request-context.d.ts +184 -0
  142. package/src/context/request-context.js +209 -0
  143. package/src/context/request-context.js.map +1 -0
  144. package/src/context/request-context.provider.d.ts +37 -0
  145. package/src/context/request-context.provider.js +51 -0
  146. package/src/context/request-context.provider.js.map +1 -0
  147. package/src/context/session-key.provider.d.ts +45 -0
  148. package/src/context/session-key.provider.js +65 -0
  149. package/src/context/session-key.provider.js.map +1 -0
  150. package/src/context/trace-context.d.ts +43 -0
  151. package/src/context/trace-context.js +142 -0
  152. package/src/context/trace-context.js.map +1 -0
  153. package/src/errors/index.d.ts +1 -1
  154. package/src/errors/index.js +4 -1
  155. package/src/errors/index.js.map +1 -1
  156. package/src/errors/mcp.error.d.ts +16 -0
  157. package/src/errors/mcp.error.js +29 -1
  158. package/src/errors/mcp.error.js.map +1 -1
  159. package/src/flows/flow.instance.d.ts +16 -0
  160. package/src/flows/flow.instance.js +166 -80
  161. package/src/flows/flow.instance.js.map +1 -1
  162. package/src/flows/flow.registry.d.ts +5 -0
  163. package/src/flows/flow.registry.js +45 -3
  164. package/src/flows/flow.registry.js.map +1 -1
  165. package/src/front-mcp/front-mcp.d.ts +12 -0
  166. package/src/front-mcp/front-mcp.js +22 -3
  167. package/src/front-mcp/front-mcp.js.map +1 -1
  168. package/src/front-mcp/front-mcp.providers.d.ts +474 -1
  169. package/src/front-mcp/front-mcp.providers.js +2 -1
  170. package/src/front-mcp/front-mcp.providers.js.map +1 -1
  171. package/src/front-mcp/index.d.ts +1 -0
  172. package/src/front-mcp/index.js +3 -0
  173. package/src/front-mcp/index.js.map +1 -1
  174. package/src/front-mcp/serverless-handler.d.ts +28 -0
  175. package/src/front-mcp/serverless-handler.js +61 -0
  176. package/src/front-mcp/serverless-handler.js.map +1 -0
  177. package/src/hooks/hooks.utils.d.ts +1 -1
  178. package/src/hooks/hooks.utils.js +10 -3
  179. package/src/hooks/hooks.utils.js.map +1 -1
  180. package/src/index.d.ts +9 -5
  181. package/src/index.js +21 -1
  182. package/src/index.js.map +1 -1
  183. package/src/logger/instances/instance.logger.js +0 -1
  184. package/src/logger/instances/instance.logger.js.map +1 -1
  185. package/src/logging/flows/set-level.flow.d.ts +6 -8
  186. package/src/notification/notification.service.js +5 -1
  187. package/src/notification/notification.service.js.map +1 -1
  188. package/src/prompt/flows/get-prompt.flow.d.ts +14 -8
  189. package/src/prompt/flows/prompts-list.flow.d.ts +8 -7
  190. package/src/provider/provider.registry.d.ts +97 -5
  191. package/src/provider/provider.registry.js +306 -9
  192. package/src/provider/provider.registry.js.map +1 -1
  193. package/src/provider/provider.types.d.ts +21 -3
  194. package/src/provider/provider.types.js.map +1 -1
  195. package/src/resource/flows/read-resource.flow.d.ts +8 -9
  196. package/src/resource/flows/resource-templates-list.flow.d.ts +8 -7
  197. package/src/resource/flows/resources-list.flow.d.ts +8 -7
  198. package/src/resource/flows/subscribe-resource.flow.d.ts +6 -8
  199. package/src/resource/flows/unsubscribe-resource.flow.d.ts +6 -8
  200. package/src/scope/flows/http.request.flow.js +43 -7
  201. package/src/scope/flows/http.request.flow.js.map +1 -1
  202. package/src/scope/scope.instance.js +12 -5
  203. package/src/scope/scope.instance.js.map +1 -1
  204. package/src/server/adapters/base.host.adapter.d.ts +9 -0
  205. package/src/server/adapters/base.host.adapter.js.map +1 -1
  206. package/src/server/adapters/express.host.adapter.d.ts +12 -0
  207. package/src/server/adapters/express.host.adapter.js +21 -1
  208. package/src/server/adapters/express.host.adapter.js.map +1 -1
  209. package/src/server/server.instance.d.ts +3 -0
  210. package/src/server/server.instance.js +14 -7
  211. package/src/server/server.instance.js.map +1 -1
  212. package/src/store/adapters/store.vercel-kv.adapter.d.ts +86 -0
  213. package/src/store/adapters/store.vercel-kv.adapter.js +155 -0
  214. package/src/store/adapters/store.vercel-kv.adapter.js.map +1 -0
  215. package/src/store/index.d.ts +2 -0
  216. package/src/store/index.js +2 -0
  217. package/src/store/index.js.map +1 -1
  218. package/src/store/store.factory.d.ts +86 -0
  219. package/src/store/store.factory.js +194 -0
  220. package/src/store/store.factory.js.map +1 -0
  221. package/src/tool/flows/call-tool.flow.d.ts +38 -19
  222. package/src/tool/flows/call-tool.flow.js +240 -194
  223. package/src/tool/flows/call-tool.flow.js.map +1 -1
  224. package/src/tool/flows/tools-list.flow.d.ts +14 -17
  225. package/src/tool/flows/tools-list.flow.js +84 -33
  226. package/src/tool/flows/tools-list.flow.js.map +1 -1
  227. package/src/tool/tool.instance.d.ts +1 -4
  228. package/src/tool/ui/index.d.ts +4 -4
  229. package/src/tool/ui/index.js +4 -4
  230. package/src/tool/ui/index.js.map +1 -1
  231. package/src/tool/ui/platform-adapters.d.ts +2 -2
  232. package/src/tool/ui/platform-adapters.js +3 -3
  233. package/src/tool/ui/platform-adapters.js.map +1 -1
  234. package/src/tool/ui/template-helpers.d.ts +5 -7
  235. package/src/tool/ui/template-helpers.js +9 -26
  236. package/src/tool/ui/template-helpers.js.map +1 -1
  237. package/src/tool/ui/ui-resource.handler.d.ts +1 -1
  238. package/src/tool/ui/ui-resource.handler.js +5 -5
  239. package/src/tool/ui/ui-resource.handler.js.map +1 -1
  240. package/src/transport/adapters/transport.streamable-http.adapter.js +1 -0
  241. package/src/transport/adapters/transport.streamable-http.adapter.js.map +1 -1
  242. package/src/transport/flows/handle.sse.flow.js +9 -2
  243. package/src/transport/flows/handle.sse.flow.js.map +1 -1
  244. package/src/transport/flows/handle.streamable-http.flow.js +63 -6
  245. package/src/transport/flows/handle.streamable-http.flow.js.map +1 -1
  246. package/src/transport/mcp-handlers/complete-request.handler.d.ts +4 -15
  247. package/src/transport/mcp-handlers/get-prompt-request.handler.d.ts +5 -15
  248. package/src/transport/mcp-handlers/index.d.ts +67 -195
  249. package/src/transport/mcp-handlers/initialize-request.handler.js +12 -2
  250. package/src/transport/mcp-handlers/initialize-request.handler.js.map +1 -1
  251. package/src/transport/mcp-handlers/list-prompts-request.handler.d.ts +5 -15
  252. package/src/transport/mcp-handlers/list-resource-templates-request.handler.d.ts +5 -15
  253. package/src/transport/mcp-handlers/list-resources-request.handler.d.ts +5 -15
  254. package/src/transport/mcp-handlers/list-tools-request.handler.d.ts +5 -15
  255. package/src/transport/mcp-handlers/logging-set-level-request.handler.d.ts +3 -14
  256. package/src/transport/mcp-handlers/read-resource-request.handler.d.ts +4 -15
  257. package/src/transport/mcp-handlers/subscribe-request.handler.d.ts +3 -14
  258. package/src/transport/mcp-handlers/unsubscribe-request.handler.d.ts +3 -14
  259. package/src/transport/transport.registry.d.ts +72 -4
  260. package/src/transport/transport.registry.js +342 -11
  261. package/src/transport/transport.registry.js.map +1 -1
  262. package/src/auth/ui/htmx-templates.js.map +0 -1
  263. package/src/common/providers/session.provider.d.ts +0 -13
  264. package/src/common/providers/session.provider.js +0 -27
  265. package/src/common/providers/session.provider.js.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import { Transporter, TransportType } from './transport.types';
2
- import { ServerResponse } from '../common';
2
+ import { ServerResponse, TransportPersistenceConfigInput } from '../common';
3
3
  import { Scope } from '../scope';
4
+ import { StoredSession } from '../auth/session';
4
5
  export declare class TransportService {
5
6
  readonly ready: Promise<void>;
6
7
  private readonly byType;
@@ -12,15 +13,64 @@ export declare class TransportService {
12
13
  * Used to differentiate between "session never initialized" (HTTP 400) and
13
14
  * "session expired/terminated" (HTTP 404) per MCP Spec 2025-11-25.
14
15
  *
15
- * Key: "type:tokenHash:sessionId", Value: creation timestamp
16
+ * Key: JSON-encoded {type, tokenHash, sessionId}, Value: creation timestamp
17
+ * Note: We use JSON instead of colon-delimiter because sessionId can contain colons.
16
18
  */
17
19
  private readonly sessionHistory;
18
20
  private readonly MAX_SESSION_HISTORY;
19
- constructor(scope: Scope);
21
+ /**
22
+ * Session store for transport persistence (Redis or Vercel KV)
23
+ * Used to persist session metadata across server restarts
24
+ */
25
+ private sessionStore?;
26
+ /**
27
+ * Transport persistence configuration
28
+ */
29
+ private persistenceConfig?;
30
+ /**
31
+ * Pending store configuration for async initialization
32
+ */
33
+ private pendingStoreConfig?;
34
+ /**
35
+ * Mutex map for preventing concurrent transport creation for the same key.
36
+ * Key: JSON-encoded {t: type, h: tokenHash, s: sessionId}, Value: Promise that resolves when creation completes
37
+ */
38
+ private readonly creationMutex;
39
+ constructor(scope: Scope, persistenceConfig?: TransportPersistenceConfigInput);
20
40
  private initialize;
21
41
  destroy(): Promise<void>;
22
42
  getTransporter(type: TransportType, token: string, sessionId: string): Promise<Transporter | undefined>;
43
+ /**
44
+ * Get stored session from Redis (without creating a transport).
45
+ * Used by flows to check if session exists and can be recreated.
46
+ *
47
+ * @param type - Transport type
48
+ * @param token - Authorization token
49
+ * @param sessionId - Session ID
50
+ * @returns Stored session data if exists and token matches, undefined otherwise
51
+ */
52
+ getStoredSession(type: TransportType, token: string, sessionId: string): Promise<StoredSession | undefined>;
53
+ /**
54
+ * Recreate a transport from stored session data.
55
+ * Must be called with a valid response object to create the actual transport.
56
+ *
57
+ * @param type - Transport type
58
+ * @param token - Authorization token
59
+ * @param sessionId - Session ID
60
+ * @param storedSession - Previously stored session data
61
+ * @param res - Server response object for the new transport
62
+ * @returns The recreated transport
63
+ */
64
+ recreateTransporter(type: TransportType, token: string, sessionId: string, storedSession: StoredSession, res: ServerResponse): Promise<Transporter>;
65
+ /**
66
+ * Internal method to actually recreate the transport (called with mutex protection)
67
+ */
68
+ private doRecreateTransporter;
23
69
  createTransporter(type: TransportType, token: string, sessionId: string, res: ServerResponse): Promise<Transporter>;
70
+ /**
71
+ * Internal method to actually create the transport (called with mutex protection)
72
+ */
73
+ private doCreateTransporter;
24
74
  destroyTransporter(type: TransportType, token: string, sessionId: string, reason?: string): Promise<void>;
25
75
  /**
26
76
  * Get or create a shared singleton transport for anonymous stateless requests.
@@ -37,13 +87,31 @@ export declare class TransportService {
37
87
  * Used to differentiate between "session never initialized" (HTTP 400) and
38
88
  * "session expired/terminated" (HTTP 404) per MCP Spec 2025-11-25.
39
89
  *
90
+ * Note: This is synchronous and only checks local history. For async Redis check,
91
+ * use wasSessionCreatedAsync.
92
+ *
40
93
  * @param type - Transport type (e.g., 'streamable-http', 'sse')
41
94
  * @param token - The authorization token
42
95
  * @param sessionId - The session ID to check
43
- * @returns true if session was ever created, false otherwise
96
+ * @returns true if session was ever created locally, false otherwise
44
97
  */
45
98
  wasSessionCreated(type: TransportType, token: string, sessionId: string): boolean;
99
+ /**
100
+ * Async version that also checks Redis for session existence.
101
+ * Used when we need to check if session was ever created across server restarts.
102
+ */
103
+ wasSessionCreatedAsync(type: TransportType, token: string, sessionId: string): Promise<boolean>;
46
104
  private sha256;
105
+ /**
106
+ * Create a history key from components.
107
+ * Uses JSON encoding to handle sessionIds that contain special characters.
108
+ */
109
+ private makeHistoryKey;
110
+ /**
111
+ * Parse a history key back into components.
112
+ * Returns undefined if the key is malformed.
113
+ */
114
+ private parseHistoryKey;
47
115
  private keyOf;
48
116
  private ensureTypeBucket;
49
117
  private ensureTokenBucket;
@@ -9,6 +9,8 @@ const transport_local_1 = require("./transport.local");
9
9
  const handle_streamable_http_flow_1 = tslib_1.__importDefault(require("./flows/handle.streamable-http.flow"));
10
10
  const handle_sse_flow_1 = tslib_1.__importDefault(require("./flows/handle.sse.flow"));
11
11
  const handle_stateless_http_flow_1 = tslib_1.__importDefault(require("./flows/handle.stateless-http.flow"));
12
+ const store_1 = require("../store");
13
+ const authorization_class_1 = require("../auth/authorization/authorization.class");
12
14
  class TransportService {
13
15
  ready;
14
16
  byType = new Map();
@@ -20,52 +22,309 @@ class TransportService {
20
22
  * Used to differentiate between "session never initialized" (HTTP 400) and
21
23
  * "session expired/terminated" (HTTP 404) per MCP Spec 2025-11-25.
22
24
  *
23
- * Key: "type:tokenHash:sessionId", Value: creation timestamp
25
+ * Key: JSON-encoded {type, tokenHash, sessionId}, Value: creation timestamp
26
+ * Note: We use JSON instead of colon-delimiter because sessionId can contain colons.
24
27
  */
25
28
  sessionHistory = new Map();
26
29
  MAX_SESSION_HISTORY = 10000;
27
- constructor(scope) {
30
+ /**
31
+ * Session store for transport persistence (Redis or Vercel KV)
32
+ * Used to persist session metadata across server restarts
33
+ */
34
+ sessionStore;
35
+ /**
36
+ * Transport persistence configuration
37
+ */
38
+ persistenceConfig;
39
+ /**
40
+ * Pending store configuration for async initialization
41
+ */
42
+ pendingStoreConfig;
43
+ /**
44
+ * Mutex map for preventing concurrent transport creation for the same key.
45
+ * Key: JSON-encoded {t: type, h: tokenHash, s: sessionId}, Value: Promise that resolves when creation completes
46
+ */
47
+ creationMutex = new Map();
48
+ constructor(scope, persistenceConfig) {
28
49
  this.scope = scope;
50
+ this.persistenceConfig = persistenceConfig;
29
51
  this.distributed = false; // get from scope metadata
30
52
  this.bus = undefined; // get from scope metadata
31
53
  if (this.distributed && !this.bus) {
32
54
  throw new Error('TransportRegistry: distributed=true requires a TransportBus implementation.');
33
55
  }
56
+ // Initialize session store if persistence is enabled (Redis or Vercel KV)
57
+ if (persistenceConfig?.enabled && persistenceConfig.redis) {
58
+ // Use factory to create appropriate session store based on provider
59
+ const redisConfig = persistenceConfig.redis;
60
+ const providerType = 'provider' in redisConfig ? redisConfig.provider : 'redis';
61
+ // Override keyPrefix for transport persistence (separate from auth sessions)
62
+ // Cast to RedisOptions since we're modifying the config
63
+ this.pendingStoreConfig = {
64
+ ...redisConfig,
65
+ keyPrefix: redisConfig.keyPrefix ?? 'mcp:transport:',
66
+ defaultTtlMs: persistenceConfig.defaultTtlMs ?? 3600000, // 1 hour default
67
+ };
68
+ this.scope.logger.info(`[TransportService] ${providerType} session store will be initialized for transport persistence`);
69
+ }
34
70
  this.ready = this.initialize();
35
71
  }
36
72
  async initialize() {
73
+ // Create session store if configuration is pending
74
+ if (this.pendingStoreConfig) {
75
+ try {
76
+ const store = await (0, store_1.createSessionStore)(this.pendingStoreConfig, this.scope.logger.child('SessionStore'));
77
+ // Cast to our extended type (both RedisSessionStore and VercelKvSessionStore have ping/disconnect)
78
+ this.sessionStore = store;
79
+ this.pendingStoreConfig = undefined;
80
+ }
81
+ catch (error) {
82
+ this.scope.logger.error('[TransportService] Failed to create session store - session persistence disabled', {
83
+ error: error.message,
84
+ });
85
+ }
86
+ }
87
+ // Validate connection if session store is configured
88
+ if (this.sessionStore) {
89
+ const isConnected = this.sessionStore.ping ? await this.sessionStore.ping() : true;
90
+ if (!isConnected) {
91
+ const providerType = this.persistenceConfig?.redis && 'provider' in this.persistenceConfig.redis
92
+ ? this.persistenceConfig.redis.provider
93
+ : 'redis';
94
+ this.scope.logger.error(`[TransportService] Failed to connect to ${providerType} - session persistence disabled`);
95
+ // Nullify sessionStore to prevent silent failures on all subsequent operations
96
+ // This ensures clean graceful degradation - sessions will only persist in memory
97
+ if (this.sessionStore.disconnect) {
98
+ await this.sessionStore.disconnect().catch(() => void 0);
99
+ }
100
+ this.sessionStore = undefined;
101
+ }
102
+ else {
103
+ this.scope.logger.info('[TransportService] Session store connection validated successfully');
104
+ }
105
+ }
37
106
  await this.scope.registryFlows(handle_streamable_http_flow_1.default, handle_sse_flow_1.default, handle_stateless_http_flow_1.default);
38
107
  }
39
108
  async destroy() {
40
- /* empty */
109
+ // Close session store connection if it was created
110
+ if (this.sessionStore && this.sessionStore.disconnect) {
111
+ try {
112
+ await this.sessionStore.disconnect();
113
+ this.scope.logger.info('[TransportService] Session store disconnected');
114
+ }
115
+ catch (error) {
116
+ this.scope.logger.warn('[TransportService] Error disconnecting session store', {
117
+ error: error.message,
118
+ });
119
+ }
120
+ }
41
121
  }
42
122
  async getTransporter(type, token, sessionId) {
43
123
  const key = this.keyOf(type, token, sessionId);
124
+ // 1. Check local in-memory cache first
44
125
  const local = this.lookupLocal(key);
45
126
  if (local)
46
127
  return local;
128
+ // 2. Check distributed bus (if enabled)
47
129
  if (this.distributed && this.bus) {
48
130
  const location = await this.bus.lookup(key);
49
131
  if (location) {
50
132
  return new transport_remote_1.RemoteTransporter(key, this.bus);
51
133
  }
52
134
  }
135
+ // Note: Redis-stored sessions require recreation via recreateTransporter()
136
+ // Flows should use getStoredSession() to check if session exists in Redis,
137
+ // then call recreateTransporter() with the response object.
53
138
  return undefined;
54
139
  }
140
+ /**
141
+ * Get stored session from Redis (without creating a transport).
142
+ * Used by flows to check if session exists and can be recreated.
143
+ *
144
+ * @param type - Transport type
145
+ * @param token - Authorization token
146
+ * @param sessionId - Session ID
147
+ * @returns Stored session data if exists and token matches, undefined otherwise
148
+ */
149
+ async getStoredSession(type, token, sessionId) {
150
+ if (!this.sessionStore || type !== 'streamable-http')
151
+ return undefined;
152
+ const tokenHash = this.sha256(token);
153
+ const stored = await this.sessionStore.get(sessionId);
154
+ if (!stored)
155
+ return undefined;
156
+ // Verify the token hash matches
157
+ if (stored.authorizationId !== tokenHash) {
158
+ this.scope.logger.warn('[TransportService] Session token mismatch during lookup', {
159
+ sessionId: sessionId.slice(0, 20),
160
+ storedTokenHash: stored.authorizationId.slice(0, 8),
161
+ requestTokenHash: tokenHash.slice(0, 8),
162
+ });
163
+ return undefined;
164
+ }
165
+ return stored;
166
+ }
167
+ /**
168
+ * Recreate a transport from stored session data.
169
+ * Must be called with a valid response object to create the actual transport.
170
+ *
171
+ * @param type - Transport type
172
+ * @param token - Authorization token
173
+ * @param sessionId - Session ID
174
+ * @param storedSession - Previously stored session data
175
+ * @param res - Server response object for the new transport
176
+ * @returns The recreated transport
177
+ */
178
+ async recreateTransporter(type, token, sessionId, storedSession, res) {
179
+ const key = this.keyOf(type, token, sessionId);
180
+ // Check if already recreated in memory
181
+ const existing = this.lookupLocal(key);
182
+ if (existing)
183
+ return existing;
184
+ // Use mutex to prevent concurrent recreation of the same transport
185
+ // Use JSON encoding for mutex key (consistent with history key format, handles colons in sessionId)
186
+ const mutexKey = JSON.stringify({ t: type, h: key.tokenHash, s: sessionId });
187
+ const pendingCreation = this.creationMutex.get(mutexKey);
188
+ if (pendingCreation) {
189
+ // Another request is already recreating this transport - wait for it
190
+ return pendingCreation;
191
+ }
192
+ // Recreate the transport with mutex protection
193
+ const recreationPromise = this.doRecreateTransporter(key, sessionId, storedSession, res);
194
+ this.creationMutex.set(mutexKey, recreationPromise);
195
+ try {
196
+ return await recreationPromise;
197
+ }
198
+ catch (error) {
199
+ // Log recreation errors for debugging
200
+ this.scope.logger.error('[TransportService] Failed to recreate transport from stored session', {
201
+ sessionId: sessionId.slice(0, 20),
202
+ error: error instanceof Error ? { name: error.name, message: error.message } : String(error),
203
+ });
204
+ throw error;
205
+ }
206
+ finally {
207
+ this.creationMutex.delete(mutexKey);
208
+ }
209
+ }
210
+ /**
211
+ * Internal method to actually recreate the transport (called with mutex protection)
212
+ */
213
+ async doRecreateTransporter(key, sessionId, storedSession, res) {
214
+ // Double-check in case another request completed while we were waiting
215
+ const existing = this.lookupLocal(key);
216
+ if (existing)
217
+ return existing;
218
+ this.scope.logger.info('[TransportService] Recreating transport from stored session', {
219
+ sessionId: sessionId.slice(0, 20),
220
+ protocol: storedSession.session.protocol,
221
+ createdAt: storedSession.createdAt,
222
+ });
223
+ // Mark session as recreated in history
224
+ const historyKey = this.makeHistoryKey(key.type, key.tokenHash, sessionId);
225
+ this.sessionHistory.set(historyKey, storedSession.createdAt);
226
+ const sessionStore = this.sessionStore;
227
+ const persistenceConfig = this.persistenceConfig;
228
+ // Create new transport
229
+ const transporter = new transport_local_1.LocalTransporter(this.scope, key, res, () => {
230
+ key.sessionId = sessionId;
231
+ this.evictLocal(key);
232
+ if (this.distributed && this.bus) {
233
+ this.bus.revoke(key).catch(() => void 0);
234
+ }
235
+ // Remove from Redis on dispose
236
+ if (sessionStore) {
237
+ sessionStore.delete(sessionId).catch(() => void 0);
238
+ }
239
+ });
240
+ await transporter.ready();
241
+ this.insertLocal(key, transporter);
242
+ // Update session access time in Redis
243
+ if (sessionStore) {
244
+ const updatedSession = {
245
+ ...storedSession,
246
+ lastAccessedAt: Date.now(),
247
+ };
248
+ sessionStore.set(sessionId, updatedSession, persistenceConfig?.defaultTtlMs).catch((err) => {
249
+ this.scope.logger.warn('[TransportService] Failed to update session in Redis', {
250
+ sessionId: sessionId.slice(0, 20),
251
+ error: err instanceof Error ? err.message : String(err),
252
+ });
253
+ });
254
+ }
255
+ if (this.distributed && this.bus) {
256
+ await this.bus.advertise(key);
257
+ }
258
+ return transporter;
259
+ }
55
260
  async createTransporter(type, token, sessionId, res) {
56
261
  const key = this.keyOf(type, token, sessionId);
262
+ // Check if already exists
57
263
  const existing = this.lookupLocal(key);
58
264
  if (existing)
59
265
  return existing;
266
+ // Use mutex to prevent concurrent creation of the same transport
267
+ // Use JSON encoding for mutex key (consistent with history key format, handles colons in sessionId)
268
+ const mutexKey = JSON.stringify({ t: type, h: key.tokenHash, s: sessionId });
269
+ const pendingCreation = this.creationMutex.get(mutexKey);
270
+ if (pendingCreation) {
271
+ // Another request is already creating this transport - wait for it
272
+ return pendingCreation;
273
+ }
274
+ // Create the transport with mutex protection
275
+ const creationPromise = this.doCreateTransporter(key, sessionId, res, type);
276
+ this.creationMutex.set(mutexKey, creationPromise);
277
+ try {
278
+ return await creationPromise;
279
+ }
280
+ finally {
281
+ this.creationMutex.delete(mutexKey);
282
+ }
283
+ }
284
+ /**
285
+ * Internal method to actually create the transport (called with mutex protection)
286
+ */
287
+ async doCreateTransporter(key, sessionId, res, type) {
288
+ // Double-check in case another request completed while we were waiting
289
+ const existing = this.lookupLocal(key);
290
+ if (existing)
291
+ return existing;
292
+ const sessionStore = this.sessionStore;
293
+ const persistenceConfig = this.persistenceConfig;
60
294
  const transporter = new transport_local_1.LocalTransporter(this.scope, key, res, () => {
61
295
  key.sessionId = sessionId;
62
296
  this.evictLocal(key);
63
297
  if (this.distributed && this.bus) {
64
298
  this.bus.revoke(key).catch(() => void 0);
65
299
  }
300
+ // Remove from Redis on dispose
301
+ if (sessionStore) {
302
+ sessionStore.delete(sessionId).catch(() => void 0);
303
+ }
66
304
  });
67
305
  await transporter.ready();
68
306
  this.insertLocal(key, transporter);
307
+ // Persist session to Redis (streamable-http only for now)
308
+ if (sessionStore && type === 'streamable-http') {
309
+ const storedSession = {
310
+ session: {
311
+ id: sessionId,
312
+ authorizationId: key.tokenHash,
313
+ protocol: 'streamable-http',
314
+ createdAt: Date.now(),
315
+ nodeId: (0, authorization_class_1.getMachineId)(),
316
+ },
317
+ authorizationId: key.tokenHash,
318
+ createdAt: Date.now(),
319
+ lastAccessedAt: Date.now(),
320
+ };
321
+ sessionStore.set(sessionId, storedSession, persistenceConfig?.defaultTtlMs).catch((err) => {
322
+ this.scope.logger.warn('[TransportService] Failed to persist session to Redis', {
323
+ sessionId: sessionId.slice(0, 20),
324
+ error: err instanceof Error ? err.message : String(err),
325
+ });
326
+ });
327
+ }
69
328
  if (this.distributed && this.bus) {
70
329
  await this.bus.advertise(key);
71
330
  }
@@ -138,20 +397,63 @@ class TransportService {
138
397
  * Used to differentiate between "session never initialized" (HTTP 400) and
139
398
  * "session expired/terminated" (HTTP 404) per MCP Spec 2025-11-25.
140
399
  *
400
+ * Note: This is synchronous and only checks local history. For async Redis check,
401
+ * use wasSessionCreatedAsync.
402
+ *
141
403
  * @param type - Transport type (e.g., 'streamable-http', 'sse')
142
404
  * @param token - The authorization token
143
405
  * @param sessionId - The session ID to check
144
- * @returns true if session was ever created, false otherwise
406
+ * @returns true if session was ever created locally, false otherwise
145
407
  */
146
408
  wasSessionCreated(type, token, sessionId) {
147
409
  const tokenHash = this.sha256(token);
148
- const historyKey = `${type}:${tokenHash}:${sessionId}`;
410
+ const historyKey = this.makeHistoryKey(type, tokenHash, sessionId);
149
411
  return this.sessionHistory.has(historyKey);
150
412
  }
413
+ /**
414
+ * Async version that also checks Redis for session existence.
415
+ * Used when we need to check if session was ever created across server restarts.
416
+ */
417
+ async wasSessionCreatedAsync(type, token, sessionId) {
418
+ // Check local history first (fast path)
419
+ if (this.wasSessionCreated(type, token, sessionId)) {
420
+ return true;
421
+ }
422
+ // Check Redis if available - use getStoredSession() to verify token hash
423
+ // (sessionStore.exists() would leak session existence to unauthorized callers)
424
+ if (this.sessionStore && type === 'streamable-http') {
425
+ const stored = await this.getStoredSession(type, token, sessionId);
426
+ return stored !== undefined;
427
+ }
428
+ return false;
429
+ }
151
430
  /* --------------------------------- internals -------------------------------- */
152
431
  sha256(value) {
153
432
  return (0, crypto_1.createHash)('sha256').update(value, 'utf8').digest('hex');
154
433
  }
434
+ /**
435
+ * Create a history key from components.
436
+ * Uses JSON encoding to handle sessionIds that contain special characters.
437
+ */
438
+ makeHistoryKey(type, tokenHash, sessionId) {
439
+ return JSON.stringify({ t: type, h: tokenHash, s: sessionId });
440
+ }
441
+ /**
442
+ * Parse a history key back into components.
443
+ * Returns undefined if the key is malformed.
444
+ */
445
+ parseHistoryKey(key) {
446
+ try {
447
+ const parsed = JSON.parse(key);
448
+ if (typeof parsed.t === 'string' && typeof parsed.h === 'string' && typeof parsed.s === 'string') {
449
+ return { type: parsed.t, tokenHash: parsed.h, sessionId: parsed.s };
450
+ }
451
+ return undefined;
452
+ }
453
+ catch {
454
+ return undefined;
455
+ }
456
+ }
155
457
  keyOf(type, token, sessionId, sessionIdSse) {
156
458
  return {
157
459
  type,
@@ -191,15 +493,44 @@ class TransportService {
191
493
  const tokenBucket = this.ensureTokenBucket(typeBucket, key.tokenHash);
192
494
  tokenBucket.set(key.sessionId, t);
193
495
  // Record session creation in history for HTTP 404 detection
194
- const historyKey = `${key.type}:${key.tokenHash}:${key.sessionId}`;
496
+ const historyKey = this.makeHistoryKey(key.type, key.tokenHash, key.sessionId);
195
497
  this.sessionHistory.set(historyKey, Date.now());
196
- // Evict oldest entries if cache exceeds max size (LRU-like eviction)
498
+ // Evict oldest entries if cache exceeds max size
499
+ // Only evict entries that don't have active transports (to avoid inconsistent state)
197
500
  if (this.sessionHistory.size > this.MAX_SESSION_HISTORY) {
198
501
  const entries = [...this.sessionHistory.entries()].sort((a, b) => a[1] - b[1]);
199
- // Remove oldest 10% of entries
200
- const toEvict = Math.ceil(this.MAX_SESSION_HISTORY * 0.1);
201
- for (let i = 0; i < toEvict && i < entries.length; i++) {
202
- this.sessionHistory.delete(entries[i][0]);
502
+ // Try to remove oldest 10% of entries (skip those with active transports)
503
+ const targetEvictions = Math.ceil(this.MAX_SESSION_HISTORY * 0.1);
504
+ let evicted = 0;
505
+ for (const [histKey] of entries) {
506
+ if (evicted >= targetEvictions)
507
+ break;
508
+ // Parse history key to check if transport still exists
509
+ const parsed = this.parseHistoryKey(histKey);
510
+ if (!parsed) {
511
+ // Invalid key format - safe to evict
512
+ this.sessionHistory.delete(histKey);
513
+ evicted++;
514
+ continue;
515
+ }
516
+ const { type, tokenHash, sessionId } = parsed;
517
+ const typeBucket = this.byType.get(type);
518
+ const tokenBucket = typeBucket?.get(tokenHash);
519
+ const hasActiveTransport = tokenBucket?.has(sessionId) ?? false;
520
+ // Only evict if there's no active transport for this session
521
+ if (!hasActiveTransport) {
522
+ this.sessionHistory.delete(histKey);
523
+ evicted++;
524
+ }
525
+ }
526
+ // Log warning if we couldn't evict enough entries (all have active transports)
527
+ if (evicted < targetEvictions) {
528
+ this.scope.logger.warn('[TransportService] Session history eviction: unable to free target memory', {
529
+ targetEvictions,
530
+ actualEvictions: evicted,
531
+ currentSize: this.sessionHistory.size,
532
+ maxSize: this.MAX_SESSION_HISTORY,
533
+ });
203
534
  }
204
535
  }
205
536
  }