@tmcp/session-manager 0.1.2 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @tmcp/session-manager
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 5613d43: fix: handle `resources/unsubscribe`
8
+ - Updated dependencies [5613d43]
9
+ - tmcp@1.16.3
10
+
11
+ ## 0.2.0
12
+
13
+ ### Minor Changes
14
+
15
+ - 8a04ee2: breaking: move sessions out of core into the transports and allow for persistent mcp state
16
+
17
+ This release moves the session management out of the core package into the SSE and HTTP transport separately.
18
+ While technically a breaking change if you update both `tmcp` and your transport (`@tmcp/transport-http`,
19
+ `@tmcp/transport-sse`, or `@tmcp/transport-stdio`), you will not face a breaking change unless you were using a
20
+ session manager.
21
+
22
+ If you were testing your `McpServer` instance manually you might need to update them to pass the `sessionInfo`
23
+ in the context parameter (only if you were reading them in the tool/resource/prompt).
24
+
25
+ Sorry for the "breaking" but this was a necessary step to unlock persistent state. 🧡
26
+
27
+ ### Patch Changes
28
+
29
+ - Updated dependencies [8a04ee2]
30
+ - Updated dependencies [a9254cb]
31
+ - tmcp@1.16.0
32
+
3
33
  ## 0.1.2
4
34
 
5
35
  ### Patch Changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @tmcp/session-manager
2
2
 
3
- Session management for TMCP (TypeScript Model Context Protocol) transport implementations. This package provides the base `SessionManager` interface and an in-memory implementation for managing client sessions in MCP servers.
3
+ Session management for TMCP (TypeScript Model Context Protocol) transport implementations. This package provides the base classes and in-memory implementations for both streaming session coordination and session metadata persistence.
4
4
 
5
5
  ## Installation
6
6
 
@@ -10,132 +10,151 @@ pnpm add @tmcp/session-manager
10
10
 
11
11
  ## Overview
12
12
 
13
- Session managers handle the storage and communication with client sessions in TMCP transport layers. They manage:
13
+ Session management is split into two concerns:
14
+
15
+ - **Stream session managers** handle the storage of long-lived streaming connections (SSE/HTTP) and the fan-out of notifications back to the right session.
16
+ - **Info session managers** persist metadata that MCP transports need across requests, such as client capabilities, client info, requested log level, and resource subscriptions.
17
+
18
+ Together they manage:
14
19
 
15
20
  - **Session Creation**: Establishing new client sessions with stream controllers
16
- - **Session Deletion**: Cleaning up disconnected sessions
17
- - **Session Queries**: Checking if sessions exist
18
- - **Message Broadcasting**: Sending messages to specific sessions or all sessions
21
+ - **Session Deletion**: Cleaning up disconnected sessions and metadata
22
+ - **Session Queries**: Checking whether a given session is still attached
23
+ - **Message Delivery**: Sending messages to specific sessions or everyone
24
+ - **Client Metadata**: Persisting capabilities, `clientInfo`, and log level between requests
25
+ - **Resource Subscriptions**: Tracking which sessions subscribed to which URIs
19
26
 
20
27
  ## Usage
21
28
 
22
- ### In-Memory Session Manager (Default)
29
+ ### In-Memory Session Managers (Default)
23
30
 
24
- The `InMemorySessionManager` stores sessions in memory and is suitable for single-server deployments:
31
+ The package ships with `InMemoryStreamSessionManager` and `InMemoryInfoSessionManager`. Together they are suitable for single-server deployments or tests:
25
32
 
26
33
  ```javascript
27
- import { InMemorySessionManager } from '@tmcp/session-manager';
28
-
29
- const sessionManager = new InMemorySessionManager();
34
+ import {
35
+ InMemoryStreamSessionManager,
36
+ InMemoryInfoSessionManager,
37
+ } from '@tmcp/session-manager';
38
+
39
+ const sessionManagers = {
40
+ streams: new InMemoryStreamSessionManager(),
41
+ info: new InMemoryInfoSessionManager(),
42
+ };
30
43
  ```
31
44
 
32
- ### Custom Session Manager
45
+ ### Custom Session Managers
46
+
47
+ You can implement your own managers by extending the base classes that ship with this package.
33
48
 
34
- You can implement your own session manager by extending the base `SessionManager` class:
49
+ #### Stream session manager
35
50
 
36
51
  ```javascript
37
- import { SessionManager } from '@tmcp/session-manager';
38
-
39
- class CustomSessionManager extends SessionManager {
40
- /**
41
- * Create a new session with the given ID and controller
42
- * @param {string} id - Session ID
43
- * @param {ReadableStreamDefaultController} controller - Stream controller
44
- */
52
+ import { StreamSessionManager } from '@tmcp/session-manager';
53
+
54
+ class CustomStreamSessionManager extends StreamSessionManager {
45
55
  create(id, controller) {
46
- // Implementation here
56
+ // Persist the ReadableStream controller for later notifications
47
57
  }
48
58
 
49
- /**
50
- * Delete a session by ID
51
- * @param {string} id - Session ID to delete
52
- */
53
59
  delete(id) {
54
- // Implementation here
60
+ // Clean up the controller and any associated timers
55
61
  }
56
62
 
57
- /**
58
- * Check if a session exists
59
- * @param {string} id - Session ID to check
60
- * @returns {Promise<boolean>} - Whether the session exists
61
- */
62
63
  async has(id) {
63
- // Implementation here
64
+ // Return whether a controller for the session exists
64
65
  }
65
66
 
66
- /**
67
- * Send data to specific sessions or all sessions
68
- * @param {string[] | undefined} sessions - Session IDs to send to (undefined = all sessions)
69
- * @param {string} data - Data to send
70
- */
71
67
  send(sessions, data) {
72
- // Implementation here
68
+ // Fan out the payload to the targeted sessions (or everyone if sessions is undefined)
73
69
  }
74
70
  }
75
71
  ```
76
72
 
77
- ## API
78
-
79
- ### `SessionManager` (Abstract Base Class)
73
+ #### Info session manager
80
74
 
81
- #### Methods
82
-
83
- ##### `create(id: string, controller: ReadableStreamDefaultController): void`
84
-
85
- Creates a new session with the specified ID and associates it with a stream controller.
86
-
87
- **Parameters:**
75
+ ```javascript
76
+ import { InfoSessionManager } from '@tmcp/session-manager';
88
77
 
89
- - `id` - Unique session identifier
90
- - `controller` - ReadableStreamDefaultController for sending data to the client
78
+ class CustomInfoSessionManager extends InfoSessionManager {
79
+ async getClientInfo(id) {
80
+ // Return the last clientInfo payload for the session
81
+ }
91
82
 
92
- ##### `delete(id: string): void`
83
+ setClientInfo(id, info) {
84
+ // Persist clientInfo for later requests
85
+ }
93
86
 
94
- Removes a session and cleans up its resources.
87
+ async getClientCapabilities(id) {
88
+ // Retrieve cached client capabilities
89
+ }
95
90
 
96
- **Parameters:**
91
+ setClientCapabilities(id, capabilities) {
92
+ // Persist the negotiated capabilities
93
+ }
97
94
 
98
- - `id` - Session ID to delete
95
+ async getLogLevel(id) {
96
+ // Return the log level requested by the client
97
+ }
99
98
 
100
- ##### `has(id: string): Promise<boolean>`
99
+ setLogLevel(id, level) {
100
+ // Store the latest log level
101
+ }
101
102
 
102
- Checks whether a session with the given ID exists.
103
+ async getSubscriptions(uri) {
104
+ // Return all session ids subscribed to the URI
105
+ }
103
106
 
104
- **Parameters:**
107
+ addSubscription(id, uri) {
108
+ // Track that the session subscribed to the URI
109
+ }
105
110
 
106
- - `id` - Session ID to check
111
+ delete(id) {
112
+ // Remove all metadata for the session (client info, capabilities, subscriptions, etc.)
113
+ }
114
+ }
115
+ ```
107
116
 
108
- **Returns:**
117
+ ## API
109
118
 
110
- - Promise resolving to true if the session exists, false otherwise
119
+ ### `StreamSessionManager` (Abstract Base Class)
111
120
 
112
- ##### `send(sessions: string[] | undefined, data: string): void`
121
+ Responsible for creating and managing streaming controllers.
113
122
 
114
- Sends data to one or more sessions.
123
+ - `create(id, controller)` – register a session and associate its stream controller
124
+ - `delete(id)` – remove the controller and clean up resources
125
+ - `has(id)` – resolve to `true` when a controller for the session exists
126
+ - `send(sessions, data)` – push a payload to selected sessions (or everyone when `sessions` is `undefined`)
115
127
 
116
- **Parameters:**
128
+ ### `InfoSessionManager` (Abstract Base Class)
117
129
 
118
- - `sessions` - Array of session IDs to send to, or undefined to send to all sessions
119
- - `data` - String data to send (typically SSE-formatted)
130
+ Stores session metadata that needs to survive across HTTP requests or reconnects.
120
131
 
121
- ### `InMemorySessionManager`
132
+ - `getClientInfo(id)` / `setClientInfo(id, info)`
133
+ - `getClientCapabilities(id)` / `setClientCapabilities(id, capabilities)`
134
+ - `getLogLevel(id)` / `setLogLevel(id, level)`
135
+ - `getSubscriptions(uri)` – return all session IDs that subscribed to a resource
136
+ - `addSubscription(id, uri)` – record a new resource subscription
137
+ - `delete(id)` – remove all metadata for a session when it disconnects
122
138
 
123
- Concrete implementation that stores sessions in a JavaScript Map. Suitable for single-server deployments.
139
+ ### `InMemoryStreamSessionManager` & `InMemoryInfoSessionManager`
124
140
 
125
- #### Usage with Transport Layers
141
+ Concrete in-memory implementations that cover both responsibilities. Combine them when configuring a transport:
126
142
 
127
143
  ```javascript
128
144
  import { HttpTransport } from '@tmcp/transport-http';
129
145
  import { SseTransport } from '@tmcp/transport-sse';
130
- import { InMemorySessionManager } from '@tmcp/session-manager';
131
-
132
- const sessionManager = new InMemorySessionManager();
133
-
134
- // Use with HTTP transport
135
- const httpTransport = new HttpTransport(server, { sessionManager });
136
-
137
- // Use with SSE transport
138
- const sseTransport = new SseTransport(server, { sessionManager });
146
+ import {
147
+ InMemoryStreamSessionManager,
148
+ InMemoryInfoSessionManager,
149
+ } from '@tmcp/session-manager';
150
+
151
+ const sessionManagers = {
152
+ streams: new InMemoryStreamSessionManager(),
153
+ info: new InMemoryInfoSessionManager(),
154
+ };
155
+
156
+ const httpTransport = new HttpTransport(server, { sessionManager: sessionManagers });
157
+ const sseTransport = new SseTransport(server, { sessionManager: sessionManagers });
139
158
  ```
140
159
 
141
160
  ## Related Packages
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmcp/session-manager",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "repository": {
@@ -23,6 +23,9 @@
23
23
  }
24
24
  },
25
25
  "type": "module",
26
+ "peerDependencies": {
27
+ "tmcp": "^1.16.3"
28
+ },
26
29
  "publishConfig": {
27
30
  "access": "public"
28
31
  },
package/src/index.js CHANGED
@@ -1,9 +1,13 @@
1
1
  /* eslint-disable no-unused-vars */
2
2
 
3
+ /**
4
+ * @import { Context } from "tmcp";
5
+ */
6
+
3
7
  /**
4
8
  * @abstract
5
9
  */
6
- export class SessionManager {
10
+ export class StreamSessionManager {
7
11
  /**
8
12
  * @abstract
9
13
  * @param {string} id
@@ -43,7 +47,7 @@ export class SessionManager {
43
47
  }
44
48
  }
45
49
 
46
- export class InMemorySessionManager extends SessionManager {
50
+ export class InMemoryStreamSessionManager extends StreamSessionManager {
47
51
  /**
48
52
  * @type {Map<string, ReadableStreamDefaultController>}
49
53
  */
@@ -74,7 +78,6 @@ export class InMemorySessionManager extends SessionManager {
74
78
  }
75
79
 
76
80
  /**
77
- * @abstract
78
81
  * @param {string} id
79
82
  * @returns {Promise<boolean>}
80
83
  */
@@ -94,3 +97,213 @@ export class InMemorySessionManager extends SessionManager {
94
97
  }
95
98
  }
96
99
  }
100
+
101
+ /**
102
+ * @abstract
103
+ */
104
+ export class InfoSessionManager {
105
+ /**
106
+ * @abstract
107
+ * @param {string} id
108
+ * @returns {Promise<NonNullable<Context["sessionInfo"]>["clientInfo"]>}
109
+ */
110
+ getClientInfo(id) {
111
+ throw new Error('Method not implemented.');
112
+ }
113
+
114
+ /**
115
+ * @abstract
116
+ * @param {string} id
117
+ * @param {NonNullable<Context["sessionInfo"]>["clientInfo"]} client_info
118
+ */
119
+ setClientInfo(id, client_info) {
120
+ throw new Error('Method not implemented.');
121
+ }
122
+
123
+ /**
124
+ * @abstract
125
+ * @param {string} id
126
+ * @returns {Promise<NonNullable<Context["sessionInfo"]>["clientCapabilities"]>}
127
+ */
128
+ getClientCapabilities(id) {
129
+ throw new Error('Method not implemented.');
130
+ }
131
+
132
+ /**
133
+ * @abstract
134
+ * @param {string} id
135
+ * @param {NonNullable<Context["sessionInfo"]>["clientCapabilities"]} client_capabilities
136
+ */
137
+ setClientCapabilities(id, client_capabilities) {
138
+ throw new Error('Method not implemented.');
139
+ }
140
+
141
+ /**
142
+ * @abstract
143
+ * @param {string} id
144
+ * @returns {Promise<NonNullable<Context["sessionInfo"]>["logLevel"]>}
145
+ */
146
+ getLogLevel(id) {
147
+ throw new Error('Method not implemented.');
148
+ }
149
+
150
+ /**
151
+ * @abstract
152
+ * @param {string} id
153
+ * @param {NonNullable<Context["sessionInfo"]>["logLevel"]} log_level
154
+ */
155
+ setLogLevel(id, log_level) {
156
+ throw new Error('Method not implemented.');
157
+ }
158
+
159
+ /**
160
+ * @abstract
161
+ * @param {string} uri
162
+ * @returns {Promise<string[]>}
163
+ */
164
+ getSubscriptions(uri) {
165
+ throw new Error('Method not implemented.');
166
+ }
167
+
168
+ /**
169
+ * @abstract
170
+ * @param {string} id
171
+ * @param {string} uri
172
+ */
173
+ addSubscription(id, uri) {
174
+ throw new Error('Method not implemented.');
175
+ }
176
+
177
+ /**
178
+ * @abstract
179
+ * @param {string} id
180
+ * @param {string} uri
181
+ */
182
+ removeSubscription(id, uri) {
183
+ throw new Error('Method not implemented.');
184
+ }
185
+
186
+ /**
187
+ * @abstract
188
+ * @param {string} id
189
+ */
190
+ delete(id) {
191
+ throw new Error('Method not implemented.');
192
+ }
193
+ }
194
+
195
+ export class InMemoryInfoSessionManager extends InfoSessionManager {
196
+ /**
197
+ * @type {Map<string, NonNullable<Context["sessionInfo"]>["clientInfo"]>}
198
+ */
199
+ #client_info = new Map();
200
+ /**
201
+ * @type {Map<string, NonNullable<Context["sessionInfo"]>["clientCapabilities"]>}
202
+ */
203
+ #client_capabilities = new Map();
204
+ /**
205
+ * @type {Map<string, NonNullable<Context["sessionInfo"]>["logLevel"]>}
206
+ */
207
+ #log_level = new Map();
208
+ /**
209
+ * @type {Map<string, Set<string>>}
210
+ */
211
+ #subscriptions = new Map();
212
+
213
+ /**
214
+ * @param {string} session
215
+ * @param {string} name
216
+ * @returns {Promise<never>}
217
+ */
218
+ async #invariant(session, name) {
219
+ throw new Error(`${name} not found for session ${session}`);
220
+ }
221
+
222
+ /**
223
+ * @type {InfoSessionManager["getClientInfo"]}
224
+ */
225
+ getClientInfo(id) {
226
+ return Promise.resolve(
227
+ this.#client_info.get(id) ?? this.#invariant(id, 'Client info'),
228
+ );
229
+ }
230
+
231
+ /**
232
+ * @type {InfoSessionManager["setClientInfo"]}
233
+ */
234
+ setClientInfo(id, client_info) {
235
+ this.#client_info.set(id, client_info);
236
+ }
237
+
238
+ /**
239
+ * @type {InfoSessionManager["getClientCapabilities"]}
240
+ */
241
+ getClientCapabilities(id) {
242
+ return Promise.resolve(
243
+ this.#client_capabilities.get(id) ??
244
+ this.#invariant(id, 'Client capabilities'),
245
+ );
246
+ }
247
+
248
+ /**
249
+ * @type {InfoSessionManager["setClientCapabilities"]}
250
+ */
251
+ setClientCapabilities(id, client_capabilities) {
252
+ this.#client_capabilities.set(id, client_capabilities);
253
+ }
254
+
255
+ /**
256
+ * @type {InfoSessionManager["getLogLevel"]}
257
+ */
258
+ getLogLevel(id) {
259
+ return Promise.resolve(
260
+ this.#log_level.get(id) ?? this.#invariant(id, 'Log Level'),
261
+ );
262
+ }
263
+
264
+ /**
265
+ * @type {InfoSessionManager["setLogLevel"]}
266
+ */
267
+ setLogLevel(id, log_level) {
268
+ this.#log_level.set(id, log_level);
269
+ }
270
+
271
+ /**
272
+ * @type {InfoSessionManager["getSubscriptions"]}
273
+ */
274
+ getSubscriptions(uri) {
275
+ return Promise.resolve([...(this.#subscriptions.get(uri) ?? [])]);
276
+ }
277
+
278
+ /**
279
+ * @type {InfoSessionManager["addSubscription"]}
280
+ */
281
+ addSubscription(id, uri) {
282
+ let subscriptions = this.#subscriptions.get(uri);
283
+ if (!subscriptions) {
284
+ subscriptions = new Set();
285
+ this.#subscriptions.set(uri, subscriptions);
286
+ }
287
+ subscriptions.add(id);
288
+ }
289
+
290
+ /**
291
+ * @type {InfoSessionManager["removeSubscription"]}
292
+ */
293
+ removeSubscription(id, uri) {
294
+ let subscriptions = this.#subscriptions.get(uri);
295
+ if (subscriptions) {
296
+ subscriptions.delete(id);
297
+ }
298
+ }
299
+
300
+ /**
301
+ * @type {InfoSessionManager["delete"]}
302
+ */
303
+ delete(id) {
304
+ this.#subscriptions.delete(id);
305
+ this.#log_level.delete(id);
306
+ this.#client_capabilities.delete(id);
307
+ this.#client_info.delete(id);
308
+ }
309
+ }
@@ -1,8 +1,12 @@
1
1
  declare module '@tmcp/session-manager' {
2
+ import type { Context } from 'tmcp';
3
+ /**
4
+ * @import { Context } from "tmcp";
5
+ */
2
6
  /**
3
7
  * @abstract
4
8
  */
5
- export abstract class SessionManager {
9
+ export abstract class StreamSessionManager {
6
10
  /**
7
11
  * @abstract
8
12
  * */
@@ -20,14 +24,59 @@ declare module '@tmcp/session-manager' {
20
24
  * */
21
25
  abstract send(sessions: string[] | undefined, data: string): void | Promise<void>;
22
26
  }
23
- export class InMemorySessionManager extends SessionManager {
27
+ export class InMemoryStreamSessionManager extends StreamSessionManager {
24
28
  create(id: string, controller: ReadableStreamDefaultController): void;
25
29
  delete(id: string): void;
30
+ has(id: string): Promise<boolean>;
31
+ send(sessions: string[] | undefined, data: string): void;
32
+ #private;
33
+ }
34
+ /**
35
+ * @abstract
36
+ */
37
+ export abstract class InfoSessionManager {
26
38
  /**
27
39
  * @abstract
28
40
  * */
29
- abstract has(id: string): Promise<boolean>;
30
- send(sessions: string[] | undefined, data: string): void;
41
+ abstract getClientInfo(id: string): Promise<NonNullable<Context["sessionInfo"]>["clientInfo"]>;
42
+ /**
43
+ * @abstract
44
+ * */
45
+ abstract setClientInfo(id: string, client_info: NonNullable<Context["sessionInfo"]>["clientInfo"]): void;
46
+ /**
47
+ * @abstract
48
+ * */
49
+ abstract getClientCapabilities(id: string): Promise<NonNullable<Context["sessionInfo"]>["clientCapabilities"]>;
50
+ /**
51
+ * @abstract
52
+ * */
53
+ abstract setClientCapabilities(id: string, client_capabilities: NonNullable<Context["sessionInfo"]>["clientCapabilities"]): void;
54
+ /**
55
+ * @abstract
56
+ * */
57
+ abstract getLogLevel(id: string): Promise<NonNullable<Context["sessionInfo"]>["logLevel"]>;
58
+ /**
59
+ * @abstract
60
+ * */
61
+ abstract setLogLevel(id: string, log_level: NonNullable<Context["sessionInfo"]>["logLevel"]): void;
62
+ /**
63
+ * @abstract
64
+ * */
65
+ abstract getSubscriptions(uri: string): Promise<string[]>;
66
+ /**
67
+ * @abstract
68
+ * */
69
+ abstract addSubscription(id: string, uri: string): void;
70
+ /**
71
+ * @abstract
72
+ * */
73
+ abstract removeSubscription(id: string, uri: string): void;
74
+ /**
75
+ * @abstract
76
+ * */
77
+ abstract delete(id: string): void;
78
+ }
79
+ export class InMemoryInfoSessionManager extends InfoSessionManager {
31
80
  #private;
32
81
  }
33
82
  export {};
@@ -2,8 +2,10 @@
2
2
  "version": 3,
3
3
  "file": "index.d.ts",
4
4
  "names": [
5
- "SessionManager",
6
- "InMemorySessionManager"
5
+ "StreamSessionManager",
6
+ "InMemoryStreamSessionManager",
7
+ "InfoSessionManager",
8
+ "InMemoryInfoSessionManager"
7
9
  ],
8
10
  "sources": [
9
11
  "../index.js"
@@ -11,6 +13,6 @@
11
13
  "sourcesContent": [
12
14
  null
13
15
  ],
14
- "mappings": ";;;;cAKaA,cAAcA;;;;;;;;;;;;;;;;;;cAwCdC,sBAAsBA",
16
+ "mappings": ";;;;;;;;cASaA,oBAAoBA;;;;;;;;;;;;;;;;;;cAwCpBC,4BAA4BA;;;;;;;;;;;;;;cAsD5BC,kBAAkBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA2FlBC,0BAA0BA",
15
17
  "ignoreList": []
16
18
  }