@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.
- package/README.md +1 -0
- package/package.json +12 -16
- package/src/adapter/adapter.instance.js +5 -0
- package/src/adapter/adapter.instance.js.map +1 -1
- package/src/auth/authorization/authorization.class.d.ts +1 -4
- package/src/auth/authorization/authorization.class.js +6 -13
- package/src/auth/authorization/authorization.class.js.map +1 -1
- package/src/auth/flows/session.verify.flow.d.ts +1 -0
- package/src/auth/flows/session.verify.flow.js +11 -1
- package/src/auth/flows/session.verify.flow.js.map +1 -1
- package/src/auth/flows/well-known.jwks.flow.js +2 -2
- package/src/auth/flows/well-known.jwks.flow.js.map +1 -1
- package/src/auth/jwks/dev-key-persistence.d.ts +63 -0
- package/src/auth/jwks/dev-key-persistence.js +219 -0
- package/src/auth/jwks/dev-key-persistence.js.map +1 -0
- package/src/auth/jwks/index.d.ts +1 -0
- package/src/auth/jwks/index.js +1 -0
- package/src/auth/jwks/index.js.map +1 -1
- package/src/auth/jwks/jwks.service.d.ts +7 -4
- package/src/auth/jwks/jwks.service.js +81 -12
- package/src/auth/jwks/jwks.service.js.map +1 -1
- package/src/auth/jwks/jwks.types.d.ts +7 -0
- package/src/auth/jwks/jwks.types.js.map +1 -1
- package/src/auth/machine-id.d.ts +5 -0
- package/src/auth/machine-id.js +32 -0
- package/src/auth/machine-id.js.map +1 -0
- package/src/auth/session/index.d.ts +2 -0
- package/src/auth/session/index.js +5 -1
- package/src/auth/session/index.js.map +1 -1
- package/src/auth/session/record/session.base.js +5 -3
- package/src/auth/session/record/session.base.js.map +1 -1
- package/src/auth/session/record/session.stateless.d.ts +2 -2
- package/src/auth/session/record/session.stateless.js +5 -3
- package/src/auth/session/record/session.stateless.js.map +1 -1
- package/src/auth/session/redis-session.store.d.ts +64 -0
- package/src/auth/session/redis-session.store.js +204 -0
- package/src/auth/session/redis-session.store.js.map +1 -0
- package/src/auth/session/session.service.d.ts +0 -2
- package/src/auth/session/session.service.js +1 -7
- package/src/auth/session/session.service.js.map +1 -1
- package/src/auth/session/transport-session.manager.js +3 -5
- package/src/auth/session/transport-session.manager.js.map +1 -1
- package/src/auth/session/transport-session.types.d.ts +4 -0
- package/src/auth/session/transport-session.types.js +4 -3
- package/src/auth/session/transport-session.types.js.map +1 -1
- package/src/auth/session/utils/session-id.utils.d.ts +12 -1
- package/src/auth/session/utils/session-id.utils.js +48 -9
- package/src/auth/session/utils/session-id.utils.js.map +1 -1
- package/src/auth/session/vercel-kv-session.store.d.ts +96 -0
- package/src/auth/session/vercel-kv-session.store.js +216 -0
- package/src/auth/session/vercel-kv-session.store.js.map +1 -0
- package/src/auth/ui/base-layout.d.ts +0 -8
- package/src/auth/ui/base-layout.js +1 -14
- package/src/auth/ui/base-layout.js.map +1 -1
- package/src/auth/ui/index.d.ts +3 -4
- package/src/auth/ui/index.js +10 -11
- package/src/auth/ui/index.js.map +1 -1
- package/src/auth/ui/{htmx-templates.d.ts → templates.d.ts} +5 -6
- package/src/auth/ui/{htmx-templates.js → templates.js} +8 -15
- package/src/auth/ui/templates.js.map +1 -0
- package/src/common/decorators/decorator-utils.js.map +1 -1
- package/src/common/decorators/front-mcp.decorator.js +26 -3
- package/src/common/decorators/front-mcp.decorator.js.map +1 -1
- package/src/common/index.d.ts +0 -1
- package/src/common/index.js +0 -1
- package/src/common/index.js.map +1 -1
- package/src/common/interfaces/adapter.interface.d.ts +6 -0
- package/src/common/interfaces/adapter.interface.js.map +1 -1
- package/src/common/interfaces/execution-context.interface.d.ts +52 -3
- package/src/common/interfaces/execution-context.interface.js +88 -3
- package/src/common/interfaces/execution-context.interface.js.map +1 -1
- package/src/common/interfaces/flow.interface.d.ts +13 -0
- package/src/common/interfaces/flow.interface.js +24 -0
- package/src/common/interfaces/flow.interface.js.map +1 -1
- package/src/common/interfaces/server.interface.d.ts +9 -0
- package/src/common/interfaces/server.interface.js.map +1 -1
- package/src/common/metadata/app.metadata.d.ts +108 -0
- package/src/common/metadata/front-mcp.metadata.d.ts +1341 -2
- package/src/common/metadata/front-mcp.metadata.js +4 -1
- package/src/common/metadata/front-mcp.metadata.js.map +1 -1
- package/src/common/metadata/prompt.metadata.d.ts +4 -0
- package/src/common/metadata/provider.metadata.d.ts +14 -0
- package/src/common/metadata/provider.metadata.js +18 -2
- package/src/common/metadata/provider.metadata.js.map +1 -1
- package/src/common/metadata/resource.metadata.d.ts +8 -0
- package/src/common/metadata/tool-ui.metadata.d.ts +2 -2
- package/src/common/metadata/tool-ui.metadata.js +1 -1
- package/src/common/metadata/tool-ui.metadata.js.map +1 -1
- package/src/common/metadata/tool.metadata.d.ts +5 -1
- package/src/common/metadata/tool.metadata.js.map +1 -1
- package/src/common/migrate/auth-transport.migrate.d.ts +62 -0
- package/src/common/migrate/auth-transport.migrate.js +140 -0
- package/src/common/migrate/auth-transport.migrate.js.map +1 -0
- package/src/common/migrate/index.d.ts +1 -0
- package/src/common/migrate/index.js +6 -0
- package/src/common/migrate/index.js.map +1 -0
- package/src/common/schemas/http-output.schema.d.ts +24 -6
- package/src/common/schemas/index.d.ts +1 -0
- package/src/common/schemas/index.js +1 -0
- package/src/common/schemas/index.js.map +1 -1
- package/src/common/schemas/session-header.schema.d.ts +16 -0
- package/src/common/schemas/session-header.schema.js +42 -0
- package/src/common/schemas/session-header.schema.js.map +1 -0
- package/src/common/tokens/front-mcp.tokens.js +4 -1
- package/src/common/tokens/front-mcp.tokens.js.map +1 -1
- package/src/common/types/options/auth.options.d.ts +233 -3
- package/src/common/types/options/auth.options.js +29 -40
- package/src/common/types/options/auth.options.js.map +1 -1
- package/src/common/types/options/index.d.ts +2 -0
- package/src/common/types/options/index.js +2 -0
- package/src/common/types/options/index.js.map +1 -1
- package/src/common/types/options/redis.options.d.ts +190 -0
- package/src/common/types/options/redis.options.js +191 -0
- package/src/common/types/options/redis.options.js.map +1 -0
- package/src/common/types/options/server-info.options.d.ts +4 -0
- package/src/common/types/options/transport.options.d.ts +148 -0
- package/src/common/types/options/transport.options.js +121 -0
- package/src/common/types/options/transport.options.js.map +1 -0
- package/src/common/utils/global-config.utils.d.ts +36 -0
- package/src/common/utils/global-config.utils.js +44 -0
- package/src/common/utils/global-config.utils.js.map +1 -0
- package/src/common/utils/index.d.ts +1 -0
- package/src/common/utils/index.js +1 -0
- package/src/common/utils/index.js.map +1 -1
- package/src/completion/flows/complete.flow.d.ts +6 -8
- package/src/context/frontmcp-context-storage.d.ts +94 -0
- package/src/context/frontmcp-context-storage.js +183 -0
- package/src/context/frontmcp-context-storage.js.map +1 -0
- package/src/context/frontmcp-context.d.ts +269 -0
- package/src/context/frontmcp-context.js +360 -0
- package/src/context/frontmcp-context.js.map +1 -0
- package/src/context/frontmcp-context.provider.d.ts +43 -0
- package/src/context/frontmcp-context.provider.js +61 -0
- package/src/context/frontmcp-context.provider.js.map +1 -0
- package/src/context/index.d.ts +34 -0
- package/src/context/index.js +64 -0
- package/src/context/index.js.map +1 -0
- package/src/context/request-context-storage.d.ts +89 -0
- package/src/context/request-context-storage.js +183 -0
- package/src/context/request-context-storage.js.map +1 -0
- package/src/context/request-context.d.ts +184 -0
- package/src/context/request-context.js +209 -0
- package/src/context/request-context.js.map +1 -0
- package/src/context/request-context.provider.d.ts +37 -0
- package/src/context/request-context.provider.js +51 -0
- package/src/context/request-context.provider.js.map +1 -0
- package/src/context/session-key.provider.d.ts +45 -0
- package/src/context/session-key.provider.js +65 -0
- package/src/context/session-key.provider.js.map +1 -0
- package/src/context/trace-context.d.ts +43 -0
- package/src/context/trace-context.js +142 -0
- package/src/context/trace-context.js.map +1 -0
- package/src/errors/index.d.ts +1 -1
- package/src/errors/index.js +4 -1
- package/src/errors/index.js.map +1 -1
- package/src/errors/mcp.error.d.ts +16 -0
- package/src/errors/mcp.error.js +29 -1
- package/src/errors/mcp.error.js.map +1 -1
- package/src/flows/flow.instance.d.ts +16 -0
- package/src/flows/flow.instance.js +166 -80
- package/src/flows/flow.instance.js.map +1 -1
- package/src/flows/flow.registry.d.ts +5 -0
- package/src/flows/flow.registry.js +45 -3
- package/src/flows/flow.registry.js.map +1 -1
- package/src/front-mcp/front-mcp.d.ts +12 -0
- package/src/front-mcp/front-mcp.js +22 -3
- package/src/front-mcp/front-mcp.js.map +1 -1
- package/src/front-mcp/front-mcp.providers.d.ts +474 -1
- package/src/front-mcp/front-mcp.providers.js +2 -1
- package/src/front-mcp/front-mcp.providers.js.map +1 -1
- package/src/front-mcp/index.d.ts +1 -0
- package/src/front-mcp/index.js +3 -0
- package/src/front-mcp/index.js.map +1 -1
- package/src/front-mcp/serverless-handler.d.ts +28 -0
- package/src/front-mcp/serverless-handler.js +61 -0
- package/src/front-mcp/serverless-handler.js.map +1 -0
- package/src/hooks/hooks.utils.d.ts +1 -1
- package/src/hooks/hooks.utils.js +10 -3
- package/src/hooks/hooks.utils.js.map +1 -1
- package/src/index.d.ts +9 -5
- package/src/index.js +21 -1
- package/src/index.js.map +1 -1
- package/src/logger/instances/instance.logger.js +0 -1
- package/src/logger/instances/instance.logger.js.map +1 -1
- package/src/logging/flows/set-level.flow.d.ts +6 -8
- package/src/notification/notification.service.js +5 -1
- package/src/notification/notification.service.js.map +1 -1
- package/src/prompt/flows/get-prompt.flow.d.ts +14 -8
- package/src/prompt/flows/prompts-list.flow.d.ts +8 -7
- package/src/provider/provider.registry.d.ts +97 -5
- package/src/provider/provider.registry.js +306 -9
- package/src/provider/provider.registry.js.map +1 -1
- package/src/provider/provider.types.d.ts +21 -3
- package/src/provider/provider.types.js.map +1 -1
- package/src/resource/flows/read-resource.flow.d.ts +8 -9
- package/src/resource/flows/resource-templates-list.flow.d.ts +8 -7
- package/src/resource/flows/resources-list.flow.d.ts +8 -7
- package/src/resource/flows/subscribe-resource.flow.d.ts +6 -8
- package/src/resource/flows/unsubscribe-resource.flow.d.ts +6 -8
- package/src/scope/flows/http.request.flow.js +43 -7
- package/src/scope/flows/http.request.flow.js.map +1 -1
- package/src/scope/scope.instance.js +12 -5
- package/src/scope/scope.instance.js.map +1 -1
- package/src/server/adapters/base.host.adapter.d.ts +9 -0
- package/src/server/adapters/base.host.adapter.js.map +1 -1
- package/src/server/adapters/express.host.adapter.d.ts +12 -0
- package/src/server/adapters/express.host.adapter.js +21 -1
- package/src/server/adapters/express.host.adapter.js.map +1 -1
- package/src/server/server.instance.d.ts +3 -0
- package/src/server/server.instance.js +14 -7
- package/src/server/server.instance.js.map +1 -1
- package/src/store/adapters/store.vercel-kv.adapter.d.ts +86 -0
- package/src/store/adapters/store.vercel-kv.adapter.js +155 -0
- package/src/store/adapters/store.vercel-kv.adapter.js.map +1 -0
- package/src/store/index.d.ts +2 -0
- package/src/store/index.js +2 -0
- package/src/store/index.js.map +1 -1
- package/src/store/store.factory.d.ts +86 -0
- package/src/store/store.factory.js +194 -0
- package/src/store/store.factory.js.map +1 -0
- package/src/tool/flows/call-tool.flow.d.ts +38 -19
- package/src/tool/flows/call-tool.flow.js +240 -194
- package/src/tool/flows/call-tool.flow.js.map +1 -1
- package/src/tool/flows/tools-list.flow.d.ts +14 -17
- package/src/tool/flows/tools-list.flow.js +84 -33
- package/src/tool/flows/tools-list.flow.js.map +1 -1
- package/src/tool/tool.instance.d.ts +1 -4
- package/src/tool/ui/index.d.ts +4 -4
- package/src/tool/ui/index.js +4 -4
- package/src/tool/ui/index.js.map +1 -1
- package/src/tool/ui/platform-adapters.d.ts +2 -2
- package/src/tool/ui/platform-adapters.js +3 -3
- package/src/tool/ui/platform-adapters.js.map +1 -1
- package/src/tool/ui/template-helpers.d.ts +5 -7
- package/src/tool/ui/template-helpers.js +9 -26
- package/src/tool/ui/template-helpers.js.map +1 -1
- package/src/tool/ui/ui-resource.handler.d.ts +1 -1
- package/src/tool/ui/ui-resource.handler.js +5 -5
- package/src/tool/ui/ui-resource.handler.js.map +1 -1
- package/src/transport/adapters/transport.streamable-http.adapter.js +1 -0
- package/src/transport/adapters/transport.streamable-http.adapter.js.map +1 -1
- package/src/transport/flows/handle.sse.flow.js +9 -2
- package/src/transport/flows/handle.sse.flow.js.map +1 -1
- package/src/transport/flows/handle.streamable-http.flow.js +63 -6
- package/src/transport/flows/handle.streamable-http.flow.js.map +1 -1
- package/src/transport/mcp-handlers/complete-request.handler.d.ts +4 -15
- package/src/transport/mcp-handlers/get-prompt-request.handler.d.ts +5 -15
- package/src/transport/mcp-handlers/index.d.ts +67 -195
- package/src/transport/mcp-handlers/initialize-request.handler.js +12 -2
- package/src/transport/mcp-handlers/initialize-request.handler.js.map +1 -1
- package/src/transport/mcp-handlers/list-prompts-request.handler.d.ts +5 -15
- package/src/transport/mcp-handlers/list-resource-templates-request.handler.d.ts +5 -15
- package/src/transport/mcp-handlers/list-resources-request.handler.d.ts +5 -15
- package/src/transport/mcp-handlers/list-tools-request.handler.d.ts +5 -15
- package/src/transport/mcp-handlers/logging-set-level-request.handler.d.ts +3 -14
- package/src/transport/mcp-handlers/read-resource-request.handler.d.ts +4 -15
- package/src/transport/mcp-handlers/subscribe-request.handler.d.ts +3 -14
- package/src/transport/mcp-handlers/unsubscribe-request.handler.d.ts +3 -14
- package/src/transport/transport.registry.d.ts +72 -4
- package/src/transport/transport.registry.js +342 -11
- package/src/transport/transport.registry.js.map +1 -1
- package/src/auth/ui/htmx-templates.js.map +0 -1
- package/src/common/providers/session.provider.d.ts +0 -13
- package/src/common/providers/session.provider.js +0 -27
- 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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
//
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
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
|
}
|