@frontmcp/sdk 0.5.0 → 0.6.0

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 (226) hide show
  1. package/README.md +3 -3
  2. package/package.json +8 -19
  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 +1 -0
  28. package/src/auth/session/index.js +3 -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/ui/base-layout.d.ts +0 -8
  50. package/src/auth/ui/base-layout.js +1 -14
  51. package/src/auth/ui/base-layout.js.map +1 -1
  52. package/src/auth/ui/index.d.ts +3 -4
  53. package/src/auth/ui/index.js +10 -11
  54. package/src/auth/ui/index.js.map +1 -1
  55. package/src/auth/ui/{htmx-templates.d.ts → templates.d.ts} +5 -6
  56. package/src/auth/ui/{htmx-templates.js → templates.js} +8 -15
  57. package/src/auth/ui/templates.js.map +1 -0
  58. package/src/common/decorators/decorator-utils.js.map +1 -1
  59. package/src/common/decorators/front-mcp.decorator.js +28 -2
  60. package/src/common/decorators/front-mcp.decorator.js.map +1 -1
  61. package/src/common/index.d.ts +0 -1
  62. package/src/common/index.js +0 -1
  63. package/src/common/index.js.map +1 -1
  64. package/src/common/interfaces/adapter.interface.d.ts +6 -0
  65. package/src/common/interfaces/adapter.interface.js.map +1 -1
  66. package/src/common/interfaces/execution-context.interface.d.ts +52 -3
  67. package/src/common/interfaces/execution-context.interface.js +88 -3
  68. package/src/common/interfaces/execution-context.interface.js.map +1 -1
  69. package/src/common/interfaces/flow.interface.d.ts +13 -0
  70. package/src/common/interfaces/flow.interface.js +24 -0
  71. package/src/common/interfaces/flow.interface.js.map +1 -1
  72. package/src/common/interfaces/server.interface.d.ts +9 -0
  73. package/src/common/interfaces/server.interface.js.map +1 -1
  74. package/src/common/metadata/app.metadata.d.ts +108 -0
  75. package/src/common/metadata/front-mcp.metadata.d.ts +659 -2
  76. package/src/common/metadata/front-mcp.metadata.js +3 -1
  77. package/src/common/metadata/front-mcp.metadata.js.map +1 -1
  78. package/src/common/metadata/provider.metadata.d.ts +14 -0
  79. package/src/common/metadata/provider.metadata.js +18 -2
  80. package/src/common/metadata/provider.metadata.js.map +1 -1
  81. package/src/common/metadata/tool.metadata.d.ts +33 -1
  82. package/src/common/metadata/tool.metadata.js.map +1 -1
  83. package/src/common/migrate/auth-transport.migrate.d.ts +62 -0
  84. package/src/common/migrate/auth-transport.migrate.js +140 -0
  85. package/src/common/migrate/auth-transport.migrate.js.map +1 -0
  86. package/src/common/migrate/index.d.ts +1 -0
  87. package/src/common/migrate/index.js +6 -0
  88. package/src/common/migrate/index.js.map +1 -0
  89. package/src/common/schemas/http-output.schema.d.ts +10 -2
  90. package/src/common/schemas/index.d.ts +1 -0
  91. package/src/common/schemas/index.js +1 -0
  92. package/src/common/schemas/index.js.map +1 -1
  93. package/src/common/schemas/session-header.schema.d.ts +16 -0
  94. package/src/common/schemas/session-header.schema.js +42 -0
  95. package/src/common/schemas/session-header.schema.js.map +1 -0
  96. package/src/common/tokens/front-mcp.tokens.js +3 -1
  97. package/src/common/tokens/front-mcp.tokens.js.map +1 -1
  98. package/src/common/types/options/auth.options.d.ts +233 -3
  99. package/src/common/types/options/auth.options.js +29 -40
  100. package/src/common/types/options/auth.options.js.map +1 -1
  101. package/src/common/types/options/index.d.ts +2 -0
  102. package/src/common/types/options/index.js +2 -0
  103. package/src/common/types/options/index.js.map +1 -1
  104. package/src/common/types/options/redis.options.d.ts +22 -0
  105. package/src/common/types/options/redis.options.js +45 -0
  106. package/src/common/types/options/redis.options.js.map +1 -0
  107. package/src/common/types/options/transport.options.d.ts +84 -0
  108. package/src/common/types/options/transport.options.js +121 -0
  109. package/src/common/types/options/transport.options.js.map +1 -0
  110. package/src/completion/flows/complete.flow.d.ts +17 -2
  111. package/src/context/frontmcp-context-storage.d.ts +94 -0
  112. package/src/context/frontmcp-context-storage.js +183 -0
  113. package/src/context/frontmcp-context-storage.js.map +1 -0
  114. package/src/context/frontmcp-context.d.ts +269 -0
  115. package/src/context/frontmcp-context.js +360 -0
  116. package/src/context/frontmcp-context.js.map +1 -0
  117. package/src/context/frontmcp-context.provider.d.ts +43 -0
  118. package/src/context/frontmcp-context.provider.js +61 -0
  119. package/src/context/frontmcp-context.provider.js.map +1 -0
  120. package/src/context/index.d.ts +34 -0
  121. package/src/context/index.js +64 -0
  122. package/src/context/index.js.map +1 -0
  123. package/src/context/request-context-storage.d.ts +89 -0
  124. package/src/context/request-context-storage.js +183 -0
  125. package/src/context/request-context-storage.js.map +1 -0
  126. package/src/context/request-context.d.ts +184 -0
  127. package/src/context/request-context.js +209 -0
  128. package/src/context/request-context.js.map +1 -0
  129. package/src/context/request-context.provider.d.ts +37 -0
  130. package/src/context/request-context.provider.js +51 -0
  131. package/src/context/request-context.provider.js.map +1 -0
  132. package/src/context/session-key.provider.d.ts +45 -0
  133. package/src/context/session-key.provider.js +65 -0
  134. package/src/context/session-key.provider.js.map +1 -0
  135. package/src/context/trace-context.d.ts +43 -0
  136. package/src/context/trace-context.js +142 -0
  137. package/src/context/trace-context.js.map +1 -0
  138. package/src/errors/index.d.ts +1 -1
  139. package/src/errors/index.js +3 -1
  140. package/src/errors/index.js.map +1 -1
  141. package/src/errors/mcp.error.d.ts +7 -0
  142. package/src/errors/mcp.error.js +11 -1
  143. package/src/errors/mcp.error.js.map +1 -1
  144. package/src/flows/flow.instance.d.ts +16 -0
  145. package/src/flows/flow.instance.js +166 -80
  146. package/src/flows/flow.instance.js.map +1 -1
  147. package/src/flows/flow.registry.d.ts +5 -0
  148. package/src/flows/flow.registry.js +45 -3
  149. package/src/flows/flow.registry.js.map +1 -1
  150. package/src/front-mcp/front-mcp.d.ts +12 -0
  151. package/src/front-mcp/front-mcp.js +22 -3
  152. package/src/front-mcp/front-mcp.js.map +1 -1
  153. package/src/front-mcp/front-mcp.providers.d.ts +266 -1
  154. package/src/front-mcp/front-mcp.providers.js +2 -1
  155. package/src/front-mcp/front-mcp.providers.js.map +1 -1
  156. package/src/front-mcp/serverless-handler.d.ts +28 -0
  157. package/src/front-mcp/serverless-handler.js +61 -0
  158. package/src/front-mcp/serverless-handler.js.map +1 -0
  159. package/src/hooks/hooks.utils.d.ts +1 -1
  160. package/src/hooks/hooks.utils.js +10 -3
  161. package/src/hooks/hooks.utils.js.map +1 -1
  162. package/src/index.d.ts +8 -4
  163. package/src/index.js +20 -1
  164. package/src/index.js.map +1 -1
  165. package/src/logger/instances/instance.logger.js +0 -1
  166. package/src/logger/instances/instance.logger.js.map +1 -1
  167. package/src/logging/flows/set-level.flow.d.ts +17 -2
  168. package/src/notification/notification.service.js +5 -1
  169. package/src/notification/notification.service.js.map +1 -1
  170. package/src/prompt/flows/get-prompt.flow.d.ts +97 -2
  171. package/src/prompt/flows/prompts-list.flow.d.ts +12 -1
  172. package/src/provider/provider.registry.d.ts +97 -5
  173. package/src/provider/provider.registry.js +306 -9
  174. package/src/provider/provider.registry.js.map +1 -1
  175. package/src/provider/provider.types.d.ts +21 -3
  176. package/src/provider/provider.types.js.map +1 -1
  177. package/src/resource/flows/read-resource.flow.d.ts +22 -3
  178. package/src/resource/flows/resource-templates-list.flow.d.ts +20 -1
  179. package/src/resource/flows/resources-list.flow.d.ts +20 -1
  180. package/src/resource/flows/subscribe-resource.flow.d.ts +17 -2
  181. package/src/resource/flows/unsubscribe-resource.flow.d.ts +17 -2
  182. package/src/scope/flows/http.request.flow.js +43 -7
  183. package/src/scope/flows/http.request.flow.js.map +1 -1
  184. package/src/scope/scope.instance.js +12 -5
  185. package/src/scope/scope.instance.js.map +1 -1
  186. package/src/server/adapters/base.host.adapter.d.ts +9 -0
  187. package/src/server/adapters/base.host.adapter.js.map +1 -1
  188. package/src/server/adapters/express.host.adapter.d.ts +12 -0
  189. package/src/server/adapters/express.host.adapter.js +21 -1
  190. package/src/server/adapters/express.host.adapter.js.map +1 -1
  191. package/src/server/server.instance.d.ts +3 -0
  192. package/src/server/server.instance.js +14 -7
  193. package/src/server/server.instance.js.map +1 -1
  194. package/src/tool/flows/call-tool.flow.d.ts +118 -13
  195. package/src/tool/flows/call-tool.flow.js +240 -194
  196. package/src/tool/flows/call-tool.flow.js.map +1 -1
  197. package/src/tool/flows/tools-list.flow.d.ts +25 -11
  198. package/src/tool/flows/tools-list.flow.js +82 -31
  199. package/src/tool/flows/tools-list.flow.js.map +1 -1
  200. package/src/tool/tool.instance.d.ts +1 -4
  201. package/src/transport/adapters/transport.streamable-http.adapter.js +1 -0
  202. package/src/transport/adapters/transport.streamable-http.adapter.js.map +1 -1
  203. package/src/transport/flows/handle.sse.flow.js +9 -2
  204. package/src/transport/flows/handle.sse.flow.js.map +1 -1
  205. package/src/transport/flows/handle.streamable-http.flow.js +63 -6
  206. package/src/transport/flows/handle.streamable-http.flow.js.map +1 -1
  207. package/src/transport/mcp-handlers/complete-request.handler.d.ts +27 -1
  208. package/src/transport/mcp-handlers/get-prompt-request.handler.d.ts +52 -1
  209. package/src/transport/mcp-handlers/index.d.ts +413 -7
  210. package/src/transport/mcp-handlers/initialize-request.handler.js +12 -2
  211. package/src/transport/mcp-handlers/initialize-request.handler.js.map +1 -1
  212. package/src/transport/mcp-handlers/list-prompts-request.handler.d.ts +27 -1
  213. package/src/transport/mcp-handlers/list-resource-templates-request.handler.d.ts +32 -1
  214. package/src/transport/mcp-handlers/list-resources-request.handler.d.ts +32 -1
  215. package/src/transport/mcp-handlers/list-tools-request.handler.d.ts +30 -1
  216. package/src/transport/mcp-handlers/logging-set-level-request.handler.d.ts +20 -0
  217. package/src/transport/mcp-handlers/read-resource-request.handler.d.ts +27 -1
  218. package/src/transport/mcp-handlers/subscribe-request.handler.d.ts +20 -0
  219. package/src/transport/mcp-handlers/unsubscribe-request.handler.d.ts +20 -0
  220. package/src/transport/transport.registry.d.ts +68 -4
  221. package/src/transport/transport.registry.js +313 -11
  222. package/src/transport/transport.registry.js.map +1 -1
  223. package/src/auth/ui/htmx-templates.js.map +0 -1
  224. package/src/common/providers/session.provider.d.ts +0 -13
  225. package/src/common/providers/session.provider.js +0 -27
  226. package/src/common/providers/session.provider.js.map +0 -1
@@ -4,8 +4,15 @@ import { z } from 'zod';
4
4
  declare const inputSchema: z.ZodObject<{
5
5
  request: z.ZodObject<{
6
6
  params: z.ZodOptional<z.ZodObject<{
7
+ task: z.ZodOptional<z.ZodObject<{
8
+ ttl: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>>;
9
+ pollInterval: z.ZodOptional<z.ZodNumber>;
10
+ }, z.core.$loose>>;
7
11
  _meta: z.ZodOptional<z.ZodObject<{
8
12
  progressToken: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
13
+ "io.modelcontextprotocol/related-task": z.ZodOptional<z.ZodObject<{
14
+ taskId: z.ZodString;
15
+ }, z.core.$loose>>;
9
16
  }, z.core.$loose>>;
10
17
  cursor: z.ZodOptional<z.ZodString>;
11
18
  }, z.core.$loose>>;
@@ -14,7 +21,11 @@ declare const inputSchema: z.ZodObject<{
14
21
  ctx: z.ZodUnknown;
15
22
  }, z.core.$strip>;
16
23
  declare const outputSchema: z.ZodObject<{
17
- _meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
24
+ _meta: z.ZodOptional<z.ZodObject<{
25
+ "io.modelcontextprotocol/related-task": z.ZodOptional<z.ZodObject<{
26
+ taskId: z.ZodString;
27
+ }, z.core.$loose>>;
28
+ }, z.core.$loose>>;
18
29
  nextCursor: z.ZodOptional<z.ZodString>;
19
30
  prompts: z.ZodArray<z.ZodObject<{
20
31
  description: z.ZodOptional<z.ZodString>;
@@ -11,6 +11,18 @@ export default class ProviderRegistry extends RegistryAbstract<ProviderEntry, Pr
11
11
  /** topo order (deps first) */
12
12
  private order;
13
13
  private registries;
14
+ /** Session-scoped provider instance cache by sessionKey */
15
+ private sessionStores;
16
+ /** Locks to prevent concurrent session builds (race condition prevention) */
17
+ private sessionBuildLocks;
18
+ /** Maximum number of sessions to cache (LRU eviction) */
19
+ private static readonly MAX_SESSION_CACHE_SIZE;
20
+ /** Session cache TTL in milliseconds (1 hour) */
21
+ private static readonly SESSION_CACHE_TTL_MS;
22
+ /** Cleanup interval in milliseconds (1 minute) */
23
+ private static readonly SESSION_CLEANUP_INTERVAL_MS;
24
+ /** Handle for the session cleanup interval */
25
+ private sessionCleanupInterval;
14
26
  constructor(list: ProviderType[], parentProviders?: ProviderRegistry | undefined);
15
27
  getProviders(): ProviderEntry[];
16
28
  /** Walk up the registry chain to find a def for a token. */
@@ -32,6 +44,15 @@ export default class ProviderRegistry extends RegistryAbstract<ProviderEntry, Pr
32
44
  getAllSingletons(): ReadonlyMap<Token, unknown>;
33
45
  discoveryDeps(rec: ProviderRecord): Token[];
34
46
  invocationTokens(_token: Token, rec: ProviderRecord): Token[];
47
+ /**
48
+ * Normalize deprecated scopes to their modern equivalents.
49
+ *
50
+ * - SESSION → CONTEXT (deprecated)
51
+ * - REQUEST → CONTEXT (deprecated)
52
+ *
53
+ * This enables backwards compatibility while unifying the scope model.
54
+ */
55
+ private normalizeScope;
35
56
  getProviderScope(rec: ProviderRecord): ProviderScope;
36
57
  getScope(): ScopeEntry;
37
58
  private withTimeout;
@@ -73,10 +94,81 @@ export default class ProviderRegistry extends RegistryAbstract<ProviderEntry, Pr
73
94
  getActiveScope(): Scope;
74
95
  getActiveServer(): FrontMcpServer;
75
96
  /**
76
- * Build provider instance views for different scopes
77
- * NOTE: This is currently a stub implementation returning empty maps.
78
- * Actual view building logic is planned as part of the session management refactoring.
79
- * See the ProviderRegistryInterface TODO for fixing the session type.
97
+ * Clean up a specific session's provider cache.
98
+ * Call this when a session is terminated or expired.
99
+ *
100
+ * @param sessionKey - The session identifier to clean up
101
+ */
102
+ cleanupSession(sessionKey: string): void;
103
+ /**
104
+ * Clean up expired sessions from the cache.
105
+ * Sessions older than SESSION_CACHE_TTL_MS are removed.
106
+ *
107
+ * @returns Number of sessions cleaned up
108
+ */
109
+ cleanupExpiredSessions(): number;
110
+ /**
111
+ * Start the background session cleanup timer.
112
+ * This periodically removes expired sessions from the cache.
113
+ */
114
+ startSessionCleanup(): void;
115
+ /**
116
+ * Stop the background session cleanup timer.
117
+ * Call this when shutting down the server gracefully.
118
+ */
119
+ stopSessionCleanup(): void;
120
+ /**
121
+ * Dispose of the registry, cleaning up all resources.
122
+ * Call this when the registry/scope is being destroyed to prevent:
123
+ * - Memory leaks from retained interval handles
124
+ * - Orphaned session cleanup timers
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * // In scope teardown
129
+ * scope.providers.dispose();
130
+ * ```
131
+ */
132
+ dispose(): void;
133
+ /**
134
+ * Get session cache statistics for monitoring.
135
+ */
136
+ getSessionCacheStats(): {
137
+ size: number;
138
+ maxSize: number;
139
+ ttlMs: number;
140
+ };
141
+ /**
142
+ * Build provider instance views for different scopes.
143
+ *
144
+ * This method creates a complete view of providers across all scopes:
145
+ * - GLOBAL: Returns existing singleton instances (read-only)
146
+ * - CONTEXT: Builds per-context providers (unified session + request)
147
+ *
148
+ * Note: For backwards compatibility, SESSION and REQUEST scopes are normalized
149
+ * to CONTEXT. The returned views include `session` and `request` aliases that
150
+ * both point to the `context` store.
151
+ *
152
+ * @param sessionKey - Unique context/session identifier for CONTEXT-scoped providers
153
+ * @param contextProviders - Optional pre-built CONTEXT-scoped providers (e.g., FrontMcpContext)
154
+ * @returns ProviderViews with global and context provider maps (session/request as aliases)
155
+ */
156
+ buildViews(sessionKey: string, contextProviders?: Map<Token, unknown>): Promise<ProviderViews>;
157
+ /**
158
+ * Build a provider into a store, with access to context and global views for dependencies.
159
+ */
160
+ private buildIntoStoreWithViews;
161
+ /**
162
+ * Resolve a dependency from the available views (context → global).
163
+ */
164
+ private resolveFromViews;
165
+ /**
166
+ * Get a provider from the given views, checking context → global.
167
+ *
168
+ * @param token - The provider token to look up
169
+ * @param views - The provider views to search
170
+ * @returns The provider instance
171
+ * @throws Error if provider not found in any view
80
172
  */
81
- buildViews(session: string): Promise<ProviderViews>;
173
+ getScoped<T>(token: Token<T>, views: ProviderViews): T;
82
174
  }
@@ -7,6 +7,7 @@ const token_utils_1 = require("../utils/token.utils");
7
7
  const metadata_utils_1 = require("../utils/metadata.utils");
8
8
  const regsitry_1 = require("../regsitry");
9
9
  const scope_1 = require("../scope");
10
+ const session_key_provider_1 = require("../context/session-key.provider");
10
11
  class ProviderRegistry extends regsitry_1.RegistryAbstract {
11
12
  parentProviders;
12
13
  /** used to track which registry provided which token */
@@ -16,6 +17,18 @@ class ProviderRegistry extends regsitry_1.RegistryAbstract {
16
17
  // /** scoped instance stores by key */
17
18
  // private scoped = new Map<string, Map<Token, any>>();
18
19
  registries = new Map();
20
+ /** Session-scoped provider instance cache by sessionKey */
21
+ sessionStores = new Map();
22
+ /** Locks to prevent concurrent session builds (race condition prevention) */
23
+ sessionBuildLocks = new Map();
24
+ /** Maximum number of sessions to cache (LRU eviction) */
25
+ static MAX_SESSION_CACHE_SIZE = 10000;
26
+ /** Session cache TTL in milliseconds (1 hour) */
27
+ static SESSION_CACHE_TTL_MS = 3600000;
28
+ /** Cleanup interval in milliseconds (1 minute) */
29
+ static SESSION_CLEANUP_INTERVAL_MS = 60000;
30
+ /** Handle for the session cleanup interval */
31
+ sessionCleanupInterval = null;
19
32
  constructor(list, parentProviders) {
20
33
  super('ProviderRegistry', parentProviders, list, false);
21
34
  this.parentProviders = parentProviders;
@@ -141,6 +154,13 @@ class ProviderRegistry extends regsitry_1.RegistryAbstract {
141
154
  throw new Error(`Failed constructing ${(0, token_utils_1.tokenName)(token)}: ${msg}`);
142
155
  }
143
156
  }
157
+ // Start background session cleanup after initialization
158
+ // This ensures expired sessions are periodically cleaned up (TTL enforcement)
159
+ // Only start for scope-level registries (those with parentProviders) since
160
+ // SESSION providers are stored in scope registries, not the global registry
161
+ if (this.parentProviders) {
162
+ this.startSessionCleanup();
163
+ }
144
164
  }
145
165
  /* -------------------- Views & session stores -------------------- */
146
166
  /** Return the live singleton map as a read-only view. No copying. */
@@ -153,8 +173,26 @@ class ProviderRegistry extends regsitry_1.RegistryAbstract {
153
173
  invocationTokens(_token, rec) {
154
174
  return (0, provider_utils_1.providerInvocationTokens)(rec, (k, phase) => (0, token_utils_1.depsOfClass)(k, phase));
155
175
  }
176
+ /**
177
+ * Normalize deprecated scopes to their modern equivalents.
178
+ *
179
+ * - SESSION → CONTEXT (deprecated)
180
+ * - REQUEST → CONTEXT (deprecated)
181
+ *
182
+ * This enables backwards compatibility while unifying the scope model.
183
+ */
184
+ normalizeScope(scope) {
185
+ switch (scope) {
186
+ case common_1.ProviderScope.SESSION:
187
+ case common_1.ProviderScope.REQUEST:
188
+ return common_1.ProviderScope.CONTEXT;
189
+ default:
190
+ return scope;
191
+ }
192
+ }
156
193
  getProviderScope(rec) {
157
- return rec.metadata?.scope ?? common_1.ProviderScope.GLOBAL;
194
+ const rawScope = rec.metadata?.scope ?? common_1.ProviderScope.GLOBAL;
195
+ return this.normalizeScope(rawScope);
158
196
  }
159
197
  getScope() {
160
198
  return this.getActiveScope();
@@ -553,19 +591,278 @@ class ProviderRegistry extends regsitry_1.RegistryAbstract {
553
591
  getActiveServer() {
554
592
  return this.getWithParents(common_1.FrontMcpServer);
555
593
  }
594
+ /* -------------------- Session Cache Management -------------------- */
595
+ /**
596
+ * Clean up a specific session's provider cache.
597
+ * Call this when a session is terminated or expired.
598
+ *
599
+ * @param sessionKey - The session identifier to clean up
600
+ */
601
+ cleanupSession(sessionKey) {
602
+ this.sessionStores.delete(sessionKey);
603
+ // Resolve any waiters before deleting the lock to prevent hung promises
604
+ const lock = this.sessionBuildLocks.get(sessionKey);
605
+ if (lock) {
606
+ lock.resolve();
607
+ this.sessionBuildLocks.delete(sessionKey);
608
+ }
609
+ }
610
+ /**
611
+ * Clean up expired sessions from the cache.
612
+ * Sessions older than SESSION_CACHE_TTL_MS are removed.
613
+ *
614
+ * @returns Number of sessions cleaned up
615
+ */
616
+ cleanupExpiredSessions() {
617
+ const now = Date.now();
618
+ const cutoff = now - ProviderRegistry.SESSION_CACHE_TTL_MS;
619
+ let cleaned = 0;
620
+ for (const [key, entry] of this.sessionStores) {
621
+ if (entry.lastAccess < cutoff) {
622
+ this.sessionStores.delete(key);
623
+ // Resolve any waiters before deleting the lock to prevent hung promises
624
+ const lock = this.sessionBuildLocks.get(key);
625
+ if (lock) {
626
+ lock.resolve();
627
+ this.sessionBuildLocks.delete(key);
628
+ }
629
+ cleaned++;
630
+ }
631
+ }
632
+ return cleaned;
633
+ }
556
634
  /**
557
- * Build provider instance views for different scopes
558
- * NOTE: This is currently a stub implementation returning empty maps.
559
- * Actual view building logic is planned as part of the session management refactoring.
560
- * See the ProviderRegistryInterface TODO for fixing the session type.
635
+ * Start the background session cleanup timer.
636
+ * This periodically removes expired sessions from the cache.
561
637
  */
562
- async buildViews(session) {
638
+ startSessionCleanup() {
639
+ // Only start if not already running
640
+ if (this.sessionCleanupInterval) {
641
+ return;
642
+ }
643
+ this.sessionCleanupInterval = setInterval(() => {
644
+ const cleaned = this.cleanupExpiredSessions();
645
+ if (cleaned > 0) {
646
+ console.debug(`[ProviderRegistry] Cleaned up ${cleaned} expired sessions`);
647
+ }
648
+ }, ProviderRegistry.SESSION_CLEANUP_INTERVAL_MS);
649
+ // Allow the process to exit even if the interval is running
650
+ // This is important for graceful shutdown in serverless environments
651
+ if (this.sessionCleanupInterval.unref) {
652
+ this.sessionCleanupInterval.unref();
653
+ }
654
+ }
655
+ /**
656
+ * Stop the background session cleanup timer.
657
+ * Call this when shutting down the server gracefully.
658
+ */
659
+ stopSessionCleanup() {
660
+ if (this.sessionCleanupInterval) {
661
+ clearInterval(this.sessionCleanupInterval);
662
+ this.sessionCleanupInterval = null;
663
+ }
664
+ }
665
+ /**
666
+ * Dispose of the registry, cleaning up all resources.
667
+ * Call this when the registry/scope is being destroyed to prevent:
668
+ * - Memory leaks from retained interval handles
669
+ * - Orphaned session cleanup timers
670
+ *
671
+ * @example
672
+ * ```typescript
673
+ * // In scope teardown
674
+ * scope.providers.dispose();
675
+ * ```
676
+ */
677
+ dispose() {
678
+ this.stopSessionCleanup();
679
+ // Clear all session stores to help garbage collection
680
+ this.sessionStores.clear();
681
+ // Resolve any pending locks to prevent hung promises
682
+ for (const lock of this.sessionBuildLocks.values()) {
683
+ lock.resolve();
684
+ }
685
+ this.sessionBuildLocks.clear();
686
+ }
687
+ /**
688
+ * Get session cache statistics for monitoring.
689
+ */
690
+ getSessionCacheStats() {
691
+ return {
692
+ size: this.sessionStores.size,
693
+ maxSize: ProviderRegistry.MAX_SESSION_CACHE_SIZE,
694
+ ttlMs: ProviderRegistry.SESSION_CACHE_TTL_MS,
695
+ };
696
+ }
697
+ /* -------------------- Scoped Provider Views -------------------- */
698
+ /**
699
+ * Build provider instance views for different scopes.
700
+ *
701
+ * This method creates a complete view of providers across all scopes:
702
+ * - GLOBAL: Returns existing singleton instances (read-only)
703
+ * - CONTEXT: Builds per-context providers (unified session + request)
704
+ *
705
+ * Note: For backwards compatibility, SESSION and REQUEST scopes are normalized
706
+ * to CONTEXT. The returned views include `session` and `request` aliases that
707
+ * both point to the `context` store.
708
+ *
709
+ * @param sessionKey - Unique context/session identifier for CONTEXT-scoped providers
710
+ * @param contextProviders - Optional pre-built CONTEXT-scoped providers (e.g., FrontMcpContext)
711
+ * @returns ProviderViews with global and context provider maps (session/request as aliases)
712
+ */
713
+ async buildViews(sessionKey, contextProviders) {
714
+ // Early validation BEFORE any cache operations or lock acquisition
715
+ // This prevents cache pollution with invalid session keys
716
+ session_key_provider_1.SessionKey.validate(sessionKey);
717
+ // 1. Global providers - return existing singletons
718
+ const global = this.getAllSingletons();
719
+ // 2. Context providers - build per-context (unified session+request)
720
+ // Note: We don't cache context providers because they're per-request.
721
+ // The unified context model means all non-global providers are fresh per-request.
722
+ const contextStore = new Map(contextProviders);
723
+ // Inject SessionKey for backwards compatibility with providers that depend on it
724
+ // @deprecated - Providers should migrate to using FRONTMCP_CONTEXT instead
725
+ if (!contextStore.has(session_key_provider_1.SessionKey)) {
726
+ contextStore.set(session_key_provider_1.SessionKey, new session_key_provider_1.SessionKey(sessionKey));
727
+ }
728
+ // Build all CONTEXT-scoped providers (including normalized SESSION/REQUEST)
729
+ for (const token of this.order) {
730
+ const rec = this.defs.get(token);
731
+ if (!rec)
732
+ continue;
733
+ // getProviderScope() already normalizes SESSION/REQUEST → CONTEXT
734
+ if (this.getProviderScope(rec) !== common_1.ProviderScope.CONTEXT)
735
+ continue;
736
+ if (contextStore.has(token))
737
+ continue;
738
+ await this.buildIntoStoreWithViews(token, rec, contextStore, sessionKey, contextStore, global);
739
+ }
740
+ // Return views with backwards-compatible aliases
741
+ // All three (context, session, request) point to the same map
563
742
  return {
564
- global: new Map(),
565
- session: new Map(),
566
- request: new Map(),
743
+ global,
744
+ context: contextStore,
745
+ // Backwards compatibility aliases
746
+ session: contextStore,
747
+ request: contextStore,
567
748
  };
568
749
  }
750
+ /**
751
+ * Build a provider into a store, with access to context and global views for dependencies.
752
+ */
753
+ async buildIntoStoreWithViews(token, rec, store, scopeKey, contextStore, globalStore) {
754
+ if (store.has(token))
755
+ return;
756
+ try {
757
+ switch (rec.kind) {
758
+ case common_1.ProviderKind.VALUE: {
759
+ store.set(token, rec.useValue);
760
+ return;
761
+ }
762
+ case common_1.ProviderKind.FACTORY: {
763
+ const deps = this.invocationTokens(token, rec);
764
+ const args = [];
765
+ for (const d of deps) {
766
+ args.push(await this.resolveFromViews(d, contextStore, globalStore, scopeKey));
767
+ }
768
+ const out = rec.useFactory(...args);
769
+ const val = (0, token_utils_1.isPromise)(out)
770
+ ? await this.withTimeout(out, this.asyncTimeoutMs, `${(0, token_utils_1.tokenName)(token)}.useFactory(...)`)
771
+ : out;
772
+ store.set(token, val);
773
+ return;
774
+ }
775
+ case common_1.ProviderKind.CLASS:
776
+ case common_1.ProviderKind.CLASS_TOKEN: {
777
+ const depsTokens = this.invocationTokens(token, rec);
778
+ const deps = [];
779
+ for (const d of depsTokens) {
780
+ deps.push(await this.resolveFromViews(d, contextStore, globalStore, scopeKey));
781
+ }
782
+ const klass = rec.kind === common_1.ProviderKind.CLASS ? rec.useClass : rec.provide;
783
+ if ((0, metadata_utils_1.hasAsyncWith)(klass)) {
784
+ const out = klass.with(...deps);
785
+ const val = (0, token_utils_1.isPromise)(out)
786
+ ? await this.withTimeout(out, this.asyncTimeoutMs, `${klass.name}.with(...)`)
787
+ : out;
788
+ store.set(token, val);
789
+ }
790
+ else {
791
+ const instance = new klass(...deps);
792
+ const init = instance?.init;
793
+ if (typeof init === 'function') {
794
+ const ret = init.call(instance);
795
+ if ((0, token_utils_1.isPromise)(ret))
796
+ await this.withTimeout(ret, this.asyncTimeoutMs, `${klass.name}.init()`);
797
+ }
798
+ store.set(token, instance);
799
+ }
800
+ return;
801
+ }
802
+ }
803
+ }
804
+ catch (e) {
805
+ const msg = e?.message ?? e;
806
+ console.error(`Failed constructing (context-scoped):`, msg);
807
+ throw new Error(`Failed constructing (context-scoped) ${(0, token_utils_1.tokenName)(token)}: ${msg}`);
808
+ }
809
+ }
810
+ /**
811
+ * Resolve a dependency from the available views (context → global).
812
+ */
813
+ async resolveFromViews(token, contextStore, globalStore, scopeKey) {
814
+ // Check stores in order: context -> global -> instances
815
+ if (contextStore.has(token))
816
+ return contextStore.get(token);
817
+ if (globalStore.has(token))
818
+ return globalStore.get(token);
819
+ if (this.instances.has(token))
820
+ return this.instances.get(token);
821
+ // Try to build if it's a registered provider
822
+ const rec = this.defs.get(token);
823
+ if (rec) {
824
+ const scope = this.getProviderScope(rec);
825
+ if (scope === common_1.ProviderScope.GLOBAL) {
826
+ throw new Error(`GLOBAL dependency ${(0, token_utils_1.tokenName)(token)} is not instantiated`);
827
+ }
828
+ else {
829
+ // CONTEXT scope - build into context store
830
+ await this.buildIntoStoreWithViews(token, rec, contextStore, scopeKey, contextStore, globalStore);
831
+ return contextStore.get(token);
832
+ }
833
+ }
834
+ // Check parent hierarchy
835
+ const up = this.lookupDefInHierarchy(token);
836
+ if (up) {
837
+ const sc = up.registry.getProviderScope(up.rec);
838
+ if (sc === common_1.ProviderScope.GLOBAL) {
839
+ const v = up.registry.instances.get(token);
840
+ if (v !== undefined)
841
+ return v;
842
+ throw new Error(`GLOBAL dependency ${(0, token_utils_1.tokenName)(token)} is not instantiated in parent`);
843
+ }
844
+ }
845
+ throw new Error(`Cannot resolve dependency ${(0, token_utils_1.tokenName)(token)} from views`);
846
+ }
847
+ /**
848
+ * Get a provider from the given views, checking context → global.
849
+ *
850
+ * @param token - The provider token to look up
851
+ * @param views - The provider views to search
852
+ * @returns The provider instance
853
+ * @throws Error if provider not found in any view
854
+ */
855
+ getScoped(token, views) {
856
+ // Check context first (unified session+request store)
857
+ if (views.context.has(token))
858
+ return views.context.get(token);
859
+ if (views.global.has(token))
860
+ return views.global.get(token);
861
+ // Check instances as fallback for global providers
862
+ if (this.instances.has(token))
863
+ return this.instances.get(token);
864
+ throw new Error(`Provider ${(0, token_utils_1.tokenName)(token)} not found in views. Ensure it was built via buildViews().`);
865
+ }
569
866
  }
570
867
  exports.default = ProviderRegistry;
571
868
  //# sourceMappingURL=provider.registry.js.map