@mcp-ts/sdk 2.1.0 → 2.3.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.
@@ -274,6 +274,11 @@ interface ToolRouterOptions {
274
274
  * Pinned tools are removed from discovery results and should be called directly.
275
275
  */
276
276
  pinnedTools?: string[];
277
+ /**
278
+ * Tool names to omit from direct exposure while keeping them indexed for
279
+ * search/schema lookup/calls through meta-tools.
280
+ */
281
+ deferredTools?: string[];
277
282
  /**
278
283
  * Tool names or glob-style patterns to omit entirely from the router catalog.
279
284
  */
@@ -306,6 +311,7 @@ declare class ToolRouter {
306
311
  private index;
307
312
  private allTools;
308
313
  private pinnedTools;
314
+ private deferredTools;
309
315
  private discoverableTools;
310
316
  private groupsMap;
311
317
  private strategy;
@@ -314,6 +320,7 @@ declare class ToolRouter {
314
320
  private activeGroups;
315
321
  private customGroups?;
316
322
  private pinnedToolNames;
323
+ private deferredToolNames;
317
324
  private excludeToolMatchers;
318
325
  private initialized;
319
326
  constructor(client: ToolRouterClientInput, options?: ToolRouterOptions);
@@ -385,7 +392,9 @@ declare class ToolRouter {
385
392
  /** The 4 meta-tool definitions exposed in `search` strategy. */
386
393
  private getMetaToolDefinitions;
387
394
  private matchesPinnedTool;
395
+ private matchesDeferredTool;
388
396
  private matchesExcludedTool;
397
+ private getDirectlyVisibleTools;
389
398
  }
390
399
 
391
400
  export { type CompactTool as C, type EmbedFn as E, type IndexedTool as I, SchemaCompressor as S, type ToolGroupInfo as T, ToolIndex as a, type ToolIndexOptions as b, type ToolListResult as c, ToolRouter as d, type ToolRouterClientInput as e, type ToolRouterOptions as f, type ToolRouterStrategy as g, type ToolSearchOptions as h, type ToolServerSummary as i, type ToolSummary as j };
@@ -274,6 +274,11 @@ interface ToolRouterOptions {
274
274
  * Pinned tools are removed from discovery results and should be called directly.
275
275
  */
276
276
  pinnedTools?: string[];
277
+ /**
278
+ * Tool names to omit from direct exposure while keeping them indexed for
279
+ * search/schema lookup/calls through meta-tools.
280
+ */
281
+ deferredTools?: string[];
277
282
  /**
278
283
  * Tool names or glob-style patterns to omit entirely from the router catalog.
279
284
  */
@@ -306,6 +311,7 @@ declare class ToolRouter {
306
311
  private index;
307
312
  private allTools;
308
313
  private pinnedTools;
314
+ private deferredTools;
309
315
  private discoverableTools;
310
316
  private groupsMap;
311
317
  private strategy;
@@ -314,6 +320,7 @@ declare class ToolRouter {
314
320
  private activeGroups;
315
321
  private customGroups?;
316
322
  private pinnedToolNames;
323
+ private deferredToolNames;
317
324
  private excludeToolMatchers;
318
325
  private initialized;
319
326
  constructor(client: ToolRouterClientInput, options?: ToolRouterOptions);
@@ -385,7 +392,9 @@ declare class ToolRouter {
385
392
  /** The 4 meta-tool definitions exposed in `search` strategy. */
386
393
  private getMetaToolDefinitions;
387
394
  private matchesPinnedTool;
395
+ private matchesDeferredTool;
388
396
  private matchesExcludedTool;
397
+ private getDirectlyVisibleTools;
389
398
  }
390
399
 
391
400
  export { type CompactTool as C, type EmbedFn as E, type IndexedTool as I, SchemaCompressor as S, type ToolGroupInfo as T, ToolIndex as a, type ToolIndexOptions as b, type ToolListResult as c, ToolRouter as d, type ToolRouterClientInput as e, type ToolRouterOptions as f, type ToolRouterStrategy as g, type ToolSearchOptions as h, type ToolServerSummary as i, type ToolSummary as j };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-ts/sdk",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -6,7 +6,7 @@
6
6
  /** Core MCP client and session management */
7
7
  export { MCPClient } from './mcp/oauth-client.js';
8
8
  export { UnauthorizedError } from '../shared/errors.js';
9
- export { sessions, type SessionStore } from './storage/index.js';
9
+ export { sessions, onSessionMutation, type SessionStore } from './storage/index.js';
10
10
  export { StorageOAuthClientProvider } from './mcp/storage-oauth-provider.js';
11
11
  export { MultiSessionClient } from './mcp/multi-session-client.js';
12
12
 
@@ -44,6 +44,13 @@ export type {
44
44
  CallToolResponse,
45
45
  } from '../shared/types';
46
46
 
47
+ export type {
48
+ Session,
49
+ SessionMutationEvent,
50
+ SessionMutationListener,
51
+ SessionMutationType,
52
+ } from './storage/types.js';
53
+
47
54
  /** Re-export MCP SDK types for convenience */
48
55
  export type {
49
56
  OAuthClientMetadata,
@@ -5,7 +5,7 @@ import { FileStorageBackend } from './file-backend';
5
5
  import { SqliteStorage } from './sqlite-backend.js';
6
6
  import { SupabaseStorageBackend } from './supabase-backend.js';
7
7
  import { NeonStorageBackend, type NeonStorageOptions } from './neon-backend.js';
8
- import type { SessionStore } from './types.js';
8
+ import type { SessionStore, Session, SessionMutationEvent, SessionMutationListener } from './types.js';
9
9
 
10
10
  // Re-export types
11
11
  export * from './types.js';
@@ -42,6 +42,65 @@ function warnIfNeonConnectionStringIsInsecure(connectionString: string): void {
42
42
 
43
43
  let storageInstance: SessionStore | null = null;
44
44
  let storagePromise: Promise<SessionStore> | null = null;
45
+ const sessionMutationListeners = new Set<SessionMutationListener>();
46
+
47
+ function emitSessionMutation(event: SessionMutationEvent): void {
48
+ for (const listener of sessionMutationListeners) {
49
+ try {
50
+ const result = listener(event);
51
+ if (result && typeof (result as Promise<void>).catch === 'function') {
52
+ void (result as Promise<void>).catch((error) => {
53
+ console.error('[mcp-ts][Storage] Session mutation listener failed:', error);
54
+ });
55
+ }
56
+ } catch (error) {
57
+ console.error('[mcp-ts][Storage] Session mutation listener failed:', error);
58
+ }
59
+ }
60
+ }
61
+
62
+ function createSessionMutationEvent(prop: PropertyKey, args: any[]): SessionMutationEvent | null {
63
+ const timestamp = Date.now();
64
+
65
+ if (prop === 'create') {
66
+ const [session, ttl] = args as [Session, number | undefined];
67
+ if (!session?.userId || !session?.sessionId) return null;
68
+ return {
69
+ type: 'create',
70
+ userId: session.userId,
71
+ sessionId: session.sessionId,
72
+ session,
73
+ ttl,
74
+ timestamp,
75
+ };
76
+ }
77
+
78
+ if (prop === 'update') {
79
+ const [userId, sessionId, patch, ttl] = args as [string, string, Partial<Session>, number | undefined];
80
+ if (!userId || !sessionId) return null;
81
+ return {
82
+ type: 'update',
83
+ userId,
84
+ sessionId,
85
+ patch,
86
+ ttl,
87
+ timestamp,
88
+ };
89
+ }
90
+
91
+ if (prop === 'delete') {
92
+ const [userId, sessionId] = args as [string, string];
93
+ if (!userId || !sessionId) return null;
94
+ return {
95
+ type: 'delete',
96
+ userId,
97
+ sessionId,
98
+ timestamp,
99
+ };
100
+ }
101
+
102
+ return null;
103
+ }
45
104
 
46
105
  async function initializeStorage<T extends SessionStore>(store: T): Promise<T> {
47
106
  if (typeof store.init === 'function') {
@@ -215,6 +274,17 @@ export function _setStorageInstanceForTesting(instance: SessionStore | null): vo
215
274
  }
216
275
  }
217
276
 
277
+ export function onSessionMutation(listener: SessionMutationListener): () => void {
278
+ sessionMutationListeners.add(listener);
279
+ return () => {
280
+ sessionMutationListeners.delete(listener);
281
+ };
282
+ }
283
+
284
+ export function _resetSessionMutationListenersForTesting(): void {
285
+ sessionMutationListeners.clear();
286
+ }
287
+
218
288
  /**
219
289
  * Global session store instance
220
290
  * Uses lazy initialization with a Proxy to handle async setup transparently
@@ -225,7 +295,12 @@ export const sessions: SessionStore = new Proxy({} as SessionStore, {
225
295
  const instance = await getStorage();
226
296
  const value = (instance as any)[prop];
227
297
  if (typeof value === 'function') {
228
- return value.apply(instance, args);
298
+ const result = await value.apply(instance, args);
299
+ const event = createSessionMutationEvent(prop, args);
300
+ if (event) {
301
+ emitSessionMutation(event);
302
+ }
303
+ return result;
229
304
  }
230
305
  return value;
231
306
  };
@@ -29,6 +29,20 @@ export interface Session {
29
29
  clientId?: string;
30
30
  }
31
31
 
32
+ export type SessionMutationType = 'create' | 'update' | 'delete';
33
+
34
+ export interface SessionMutationEvent {
35
+ type: SessionMutationType;
36
+ userId: string;
37
+ sessionId: string;
38
+ timestamp: number;
39
+ session?: Session;
40
+ patch?: Partial<Session>;
41
+ ttl?: number;
42
+ }
43
+
44
+ export type SessionMutationListener = (event: SessionMutationEvent) => void | Promise<void>;
45
+
32
46
  export interface SetClientOptions {
33
47
  sessionId: string;
34
48
  serverId?: string; // Database server ID
@@ -105,6 +105,12 @@ export interface ToolRouterOptions {
105
105
  */
106
106
  pinnedTools?: string[];
107
107
 
108
+ /**
109
+ * Tool names to omit from direct exposure while keeping them indexed for
110
+ * search/schema lookup/calls through meta-tools.
111
+ */
112
+ deferredTools?: string[];
113
+
108
114
  /**
109
115
  * Tool names or glob-style patterns to omit entirely from the router catalog.
110
116
  */
@@ -148,6 +154,7 @@ export class ToolRouter {
148
154
  private index: ToolIndex;
149
155
  private allTools: IndexedTool[] = [];
150
156
  private pinnedTools: IndexedTool[] = [];
157
+ private deferredTools: IndexedTool[] = [];
151
158
  private discoverableTools: IndexedTool[] = [];
152
159
  private groupsMap = new Map<string, ToolGroupInfo>();
153
160
  private strategy: ToolRouterStrategy;
@@ -156,6 +163,7 @@ export class ToolRouter {
156
163
  private activeGroups: Set<string>;
157
164
  private customGroups?: Record<string, string[]>;
158
165
  private pinnedToolNames: Set<string>;
166
+ private deferredToolNames: Set<string>;
159
167
  private excludeToolMatchers: RegExp[];
160
168
  private initialized = false;
161
169
 
@@ -169,6 +177,7 @@ export class ToolRouter {
169
177
  this.activeGroups = new Set(options.activeGroups ?? []);
170
178
  this.customGroups = options.groups;
171
179
  this.pinnedToolNames = new Set(options.pinnedTools ?? []);
180
+ this.deferredToolNames = new Set(options.deferredTools ?? []);
172
181
  this.excludeToolMatchers = (options.excludeTools ?? []).map((pattern) =>
173
182
  globToRegExp(pattern)
174
183
  );
@@ -203,9 +212,10 @@ export class ToolRouter {
203
212
 
204
213
  case 'all':
205
214
  default:
215
+ const directlyVisibleTools = this.getDirectlyVisibleTools();
206
216
  if (this.compactSchemas) {
207
217
  // Return tools with inputSchema stripped
208
- return this.allTools.map((t) => {
218
+ return directlyVisibleTools.map((t) => {
209
219
  const compact = SchemaCompressor.toCompact(t);
210
220
  return {
211
221
  name: compact.name,
@@ -216,7 +226,7 @@ export class ToolRouter {
216
226
  };
217
227
  });
218
228
  }
219
- return [...this.allTools];
229
+ return [...directlyVisibleTools];
220
230
  }
221
231
  }
222
232
 
@@ -374,6 +384,9 @@ export class ToolRouter {
374
384
  const fetchedTools = await this.fetchAllTools();
375
385
  this.allTools = fetchedTools.filter((tool) => !this.matchesExcludedTool(tool.name));
376
386
  this.pinnedTools = this.allTools.filter((tool) => this.matchesPinnedTool(tool.name));
387
+ this.deferredTools = this.allTools.filter(
388
+ (tool) => !this.matchesPinnedTool(tool.name) && this.matchesDeferredTool(tool)
389
+ );
377
390
  this.discoverableTools = this.allTools.filter((tool) => !this.matchesPinnedTool(tool.name));
378
391
  await this.index.buildIndex(this.discoverableTools);
379
392
  this.buildGroups();
@@ -469,7 +482,7 @@ export class ToolRouter {
469
482
  }
470
483
  }
471
484
 
472
- const filtered = this.allTools.filter((t) => activeToolNames.has(t.name));
485
+ const filtered = this.getDirectlyVisibleTools().filter((t) => activeToolNames.has(t.name));
473
486
 
474
487
  if (this.compactSchemas) {
475
488
  return filtered.slice(0, this.maxTools).map((t) => {
@@ -502,9 +515,24 @@ export class ToolRouter {
502
515
  return this.pinnedToolNames.has(toolName);
503
516
  }
504
517
 
518
+ private matchesDeferredTool(tool: Tool): boolean {
519
+ if (this.deferredToolNames.has(tool.name)) {
520
+ return true;
521
+ }
522
+
523
+ const meta = (tool as Tool & {
524
+ _meta?: { toolRouter?: { deferred?: boolean } };
525
+ })._meta;
526
+ return meta?.toolRouter?.deferred === true;
527
+ }
528
+
505
529
  private matchesExcludedTool(toolName: string): boolean {
506
530
  return this.excludeToolMatchers.some((matcher) => matcher.test(toolName));
507
531
  }
532
+
533
+ private getDirectlyVisibleTools(): IndexedTool[] {
534
+ return this.allTools.filter((tool) => !this.matchesDeferredTool(tool) || this.matchesPinnedTool(tool.name));
535
+ }
508
536
  }
509
537
 
510
538
  function globToRegExp(pattern: string): RegExp {