@mcp-ts/sdk 2.4.5 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/adapters/agui-adapter.d.mts +3 -3
  2. package/dist/adapters/agui-adapter.d.ts +3 -3
  3. package/dist/adapters/agui-adapter.js.map +1 -1
  4. package/dist/adapters/agui-adapter.mjs.map +1 -1
  5. package/dist/adapters/agui-middleware.d.mts +3 -3
  6. package/dist/adapters/agui-middleware.d.ts +3 -3
  7. package/dist/adapters/agui-middleware.js.map +1 -1
  8. package/dist/adapters/agui-middleware.mjs.map +1 -1
  9. package/dist/adapters/ai-adapter.d.mts +3 -3
  10. package/dist/adapters/ai-adapter.d.ts +3 -3
  11. package/dist/adapters/ai-adapter.js.map +1 -1
  12. package/dist/adapters/ai-adapter.mjs.map +1 -1
  13. package/dist/adapters/langchain-adapter.d.mts +3 -3
  14. package/dist/adapters/langchain-adapter.d.ts +3 -3
  15. package/dist/adapters/langchain-adapter.js +2 -2
  16. package/dist/adapters/langchain-adapter.js.map +1 -1
  17. package/dist/adapters/langchain-adapter.mjs +2 -2
  18. package/dist/adapters/langchain-adapter.mjs.map +1 -1
  19. package/dist/adapters/mastra-adapter.d.mts +2 -2
  20. package/dist/adapters/mastra-adapter.d.ts +2 -2
  21. package/dist/adapters/mastra-adapter.js +2 -2
  22. package/dist/adapters/mastra-adapter.js.map +1 -1
  23. package/dist/adapters/mastra-adapter.mjs +2 -2
  24. package/dist/adapters/mastra-adapter.mjs.map +1 -1
  25. package/dist/client/index.d.mts +2 -2
  26. package/dist/client/index.d.ts +2 -2
  27. package/dist/client/index.js +9 -0
  28. package/dist/client/index.js.map +1 -1
  29. package/dist/client/index.mjs +9 -0
  30. package/dist/client/index.mjs.map +1 -1
  31. package/dist/client/react.d.mts +15 -4
  32. package/dist/client/react.d.ts +15 -4
  33. package/dist/client/react.js +56 -37
  34. package/dist/client/react.js.map +1 -1
  35. package/dist/client/react.mjs +56 -37
  36. package/dist/client/react.mjs.map +1 -1
  37. package/dist/client/vue.d.mts +4 -4
  38. package/dist/client/vue.d.ts +4 -4
  39. package/dist/client/vue.js +9 -0
  40. package/dist/client/vue.js.map +1 -1
  41. package/dist/client/vue.mjs +9 -0
  42. package/dist/client/vue.mjs.map +1 -1
  43. package/dist/{index-CZk2gu2E.d.ts → index-BEdyuz1M.d.ts} +6 -1
  44. package/dist/{index-CkM3p0eE.d.mts → index-C99wE2Zf.d.mts} +6 -1
  45. package/dist/index.d.mts +4 -4
  46. package/dist/index.d.ts +4 -4
  47. package/dist/index.js +380 -82
  48. package/dist/index.js.map +1 -1
  49. package/dist/index.mjs +376 -82
  50. package/dist/index.mjs.map +1 -1
  51. package/dist/{multi-session-client-QOOPdEVp.d.ts → multi-session-client-CclWRxTD.d.mts} +14 -2
  52. package/dist/{multi-session-client-ChQrBZhF.d.mts → multi-session-client-DzvZD6Tt.d.ts} +14 -2
  53. package/dist/server/index.d.mts +18 -4
  54. package/dist/server/index.d.ts +18 -4
  55. package/dist/server/index.js +371 -82
  56. package/dist/server/index.js.map +1 -1
  57. package/dist/server/index.mjs +367 -82
  58. package/dist/server/index.mjs.map +1 -1
  59. package/dist/shared/index.d.mts +4 -4
  60. package/dist/shared/index.d.ts +4 -4
  61. package/dist/shared/index.js.map +1 -1
  62. package/dist/shared/index.mjs.map +1 -1
  63. package/dist/{tool-router-CGs3IDOJ.d.mts → tool-router-C9ECn3FX.d.mts} +1 -1
  64. package/dist/{tool-router-DQ-HrD7W.d.ts → tool-router-CUSSk6lT.d.ts} +1 -1
  65. package/dist/{types-Bf-7GOLW.d.mts → types-CbFbPkfQ.d.mts} +38 -3
  66. package/dist/{types-Bf-7GOLW.d.ts → types-CbFbPkfQ.d.ts} +38 -3
  67. package/migrations/neon/20260513010000_install_mcp_sessions.sql +1 -0
  68. package/migrations/supabase/20260330195700_install_mcp_sessions.sql +1 -0
  69. package/package.json +2 -2
  70. package/src/adapters/agui-adapter.ts +6 -2
  71. package/src/adapters/ai-adapter.ts +3 -1
  72. package/src/adapters/langchain-adapter.ts +5 -3
  73. package/src/adapters/mastra-adapter.ts +5 -3
  74. package/src/client/core/sse-client.ts +21 -0
  75. package/src/client/react/oauth-popup.tsx +17 -24
  76. package/src/client/react/use-mcp.ts +69 -15
  77. package/src/server/handlers/sse-handler.ts +202 -9
  78. package/src/server/mcp/multi-session-client.ts +7 -3
  79. package/src/server/mcp/oauth-client.ts +25 -10
  80. package/src/server/mcp/tool-policy-gateway.ts +90 -0
  81. package/src/server/storage/neon-backend.ts +65 -60
  82. package/src/server/storage/session-lifecycle.ts +5 -0
  83. package/src/server/storage/supabase-backend.ts +27 -15
  84. package/src/server/storage/tool-policy.ts +89 -0
  85. package/src/server/storage/types.ts +11 -0
  86. package/src/shared/types.ts +52 -1
@@ -18,6 +18,7 @@ import type {
18
18
  McpRpcResponse,
19
19
  ConnectParams,
20
20
  DisconnectParams,
21
+ ReconnectParams,
21
22
  SessionParams,
22
23
  CallToolParams,
23
24
  GetPromptParams,
@@ -32,13 +33,19 @@ import type {
32
33
  ListPromptsResult,
33
34
  ListResourcesResult,
34
35
  CallToolResult,
36
+ SetToolPolicyParams,
37
+ SetToolPolicyResult,
38
+ GetToolPolicyParams,
39
+ GetToolPolicyResult,
35
40
  } from '../../shared/types.js';
36
41
  import { RpcErrorCodes } from '../../shared/errors.js';
37
42
  import { UnauthorizedError } from '../../shared/errors.js';
38
43
  import { isConnectionEvent, isRpcResponseEvent } from '../../shared/event-routing.js';
39
44
  import { parseOAuthState } from '../../shared/utils.js';
40
45
  import { MCPClient } from '../mcp/oauth-client.js';
41
- import { sessions, generateServerId } from '../storage/index.js';
46
+ import { sessions, generateServerId, type Session } from '../storage/index.js';
47
+ import { createToolId, isToolAllowed, normalizeToolPolicyForUpdate, validateToolPolicyAgainstTools } from '../storage/tool-policy.js';
48
+ import { createToolPolicyGateway } from '../mcp/tool-policy-gateway.js';
42
49
 
43
50
  // ============================================
44
51
  // Types & Interfaces
@@ -149,7 +156,7 @@ export class SSEConnectionManager {
149
156
  */
150
157
  async handleRequest(request: McpRpcRequest): Promise<McpRpcResponse> {
151
158
  try {
152
- let result: SessionListResult | ConnectResult | DisconnectResult | GetSessionResult | FinishAuthResult | ListToolsRpcResult | ListPromptsResult | ListResourcesResult | unknown;
159
+ let result: SessionListResult | ConnectResult | DisconnectResult | GetSessionResult | FinishAuthResult | ListToolsRpcResult | SetToolPolicyResult | GetToolPolicyResult | ListPromptsResult | ListResourcesResult | unknown;
153
160
 
154
161
  switch (request.method) {
155
162
  case 'listSessions':
@@ -160,6 +167,10 @@ export class SSEConnectionManager {
160
167
  result = await this.connect(request.params as ConnectParams);
161
168
  break;
162
169
 
170
+ case 'reconnect':
171
+ result = await this.reconnect(request.params as ReconnectParams);
172
+ break;
173
+
163
174
  case 'disconnect':
164
175
  result = await this.disconnect(request.params as DisconnectParams);
165
176
  break;
@@ -168,6 +179,14 @@ export class SSEConnectionManager {
168
179
  result = await this.listTools(request.params as SessionParams);
169
180
  break;
170
181
 
182
+ case 'setToolPolicy':
183
+ result = await this.setToolPolicy(request.params as SetToolPolicyParams);
184
+ break;
185
+
186
+ case 'getToolPolicy':
187
+ result = await this.getToolPolicy(request.params as GetToolPolicyParams);
188
+ break;
189
+
171
190
  case 'callTool':
172
191
  result = await this.callTool(request.params as CallToolParams);
173
192
  break;
@@ -241,6 +260,7 @@ export class SSEConnectionManager {
241
260
  createdAt: s.createdAt,
242
261
  updatedAt: s.updatedAt ?? s.createdAt,
243
262
  status: s.status ?? 'pending',
263
+ toolPolicy: s.toolPolicy,
244
264
  })),
245
265
  };
246
266
  }
@@ -315,8 +335,8 @@ export class SSEConnectionManager {
315
335
  // Attempt connection
316
336
  await client.connect();
317
337
 
318
- // Fetch tools
319
- await client.listTools();
338
+ // Fetch policy-filtered tools for agent-facing discovery
339
+ await this.listPolicyFilteredTools(sessionId);
320
340
 
321
341
  return {
322
342
  sessionId,
@@ -348,6 +368,87 @@ export class SSEConnectionManager {
348
368
  }
349
369
  }
350
370
 
371
+ /**
372
+ * Reconnect to an MCP server — tears down the active client transport/connection
373
+ * and creates a fresh connection while reusing the existing session credentials in a single RPC call.
374
+ */
375
+ private async reconnect(params: ReconnectParams): Promise<ConnectResult> {
376
+ const { serverId: rawServerId, serverName, serverUrl, callbackUrl, transportType } = params;
377
+ const headers = normalizeHeaders(params.headers);
378
+
379
+ // Normalize serverId the same way connect() does
380
+ const serverId = rawServerId && rawServerId.length <= 12
381
+ ? rawServerId
382
+ : generateServerId();
383
+
384
+ // Find existing session for the same server to reuse its session ID
385
+ const existingSessions = await sessions.list(this.userId);
386
+ const duplicate = existingSessions.find(s =>
387
+ s.serverId === serverId || s.serverUrl === serverUrl
388
+ );
389
+
390
+ // Reuse the duplicate sessionId if present, otherwise generate a new one
391
+ const sessionId = duplicate ? duplicate.sessionId : await sessions.generateSessionId();
392
+
393
+ if (duplicate) {
394
+ // Disconnect any active in-memory client transport without deleting the database session
395
+ const existingClient = this.clients.get(duplicate.sessionId);
396
+ if (existingClient) {
397
+ await existingClient.disconnect();
398
+ this.clients.delete(duplicate.sessionId);
399
+ }
400
+ }
401
+
402
+ try {
403
+ const clientMetadata = await this.getResolvedClientMetadata();
404
+
405
+ // Create a new client instantiating the reused session ID (which preserves DCR credentials and tokens)
406
+ const client = new MCPClient({
407
+ userId: this.userId,
408
+ sessionId,
409
+ serverId,
410
+ serverName,
411
+ serverUrl,
412
+ callbackUrl,
413
+ transportType,
414
+ headers,
415
+ ...clientMetadata,
416
+ });
417
+
418
+ this.clients.set(sessionId, client);
419
+
420
+ client.onConnectionEvent((event) => {
421
+ this.emitConnectionEvent(event);
422
+ });
423
+
424
+ client.onObservabilityEvent((event) => {
425
+ this.sendEvent(event);
426
+ });
427
+
428
+ await client.connect();
429
+ await this.listPolicyFilteredTools(sessionId);
430
+
431
+ return { sessionId, success: true };
432
+ } catch (error) {
433
+ if (error instanceof UnauthorizedError) {
434
+ this.clients.delete(sessionId);
435
+ return { sessionId, success: true };
436
+ }
437
+
438
+ this.emitConnectionEvent({
439
+ type: 'error',
440
+ sessionId,
441
+ serverId,
442
+ error: error instanceof Error ? error.message : 'Connection failed',
443
+ errorType: 'connection',
444
+ timestamp: Date.now(),
445
+ });
446
+
447
+ this.clients.delete(sessionId);
448
+ throw error;
449
+ }
450
+ }
451
+
351
452
  /**
352
453
  * Disconnect from an MCP server
353
454
  */
@@ -407,23 +508,109 @@ export class SSEConnectionManager {
407
508
  return client;
408
509
  }
409
510
 
511
+ private async listPolicyFilteredTools(sessionId: string): Promise<{ session: Session; result: ListToolsRpcResult }> {
512
+ const session = await sessions.get(this.userId, sessionId);
513
+ if (!session) {
514
+ throw new Error('Session not found');
515
+ }
516
+
517
+ const client = await this.getOrCreateClient(sessionId);
518
+ const gateway = createToolPolicyGateway(this.userId, sessionId, client);
519
+ const result = await gateway.listTools();
520
+
521
+ this.emitConnectionEvent({
522
+ type: 'tools_discovered',
523
+ sessionId,
524
+ serverId: session.serverId ?? 'unknown',
525
+ toolCount: result.tools.length,
526
+ tools: result.tools,
527
+ timestamp: Date.now(),
528
+ });
529
+
530
+ return { session, result };
531
+ }
410
532
  /**
411
533
  * List tools from a session
412
534
  */
413
535
  private async listTools(params: SessionParams): Promise<ListToolsRpcResult> {
414
536
  const { sessionId } = params;
415
- const client = await this.getOrCreateClient(sessionId);
416
- const result = await client.listTools();
537
+ const { result } = await this.listPolicyFilteredTools(sessionId);
417
538
  return { tools: result.tools };
418
539
  }
419
540
 
541
+ /**
542
+ * Get all raw tools with effective access state for management UI.
543
+ */
544
+ private async getToolPolicy(params: GetToolPolicyParams): Promise<GetToolPolicyResult> {
545
+ const { sessionId } = params;
546
+ const session = await sessions.get(this.userId, sessionId);
547
+ if (!session) {
548
+ throw new Error('Session not found');
549
+ }
550
+
551
+ const client = await this.getOrCreateClient(sessionId);
552
+ const allTools = await client.fetchTools();
553
+ const toolPolicy = session.toolPolicy ?? {
554
+ mode: 'all' as const,
555
+ toolIds: [],
556
+ updatedAt: session.updatedAt ?? session.createdAt,
557
+ };
558
+ const serverId = session.serverId ?? 'unknown';
559
+ const tools = allTools.tools.map((tool) => ({
560
+ ...tool,
561
+ toolId: createToolId(serverId, tool.name),
562
+ allowed: isToolAllowed(toolPolicy, tool.name, session.serverId),
563
+ }));
564
+
565
+ return {
566
+ toolPolicy,
567
+ tools,
568
+ toolCount: tools.length,
569
+ allowedToolCount: tools.filter((tool) => tool.allowed).length,
570
+ };
571
+ }
572
+
573
+ /**
574
+ * Update per-session tool access policy.
575
+ */
576
+ private async setToolPolicy(params: SetToolPolicyParams): Promise<SetToolPolicyResult> {
577
+ const { sessionId } = params;
578
+ const session = await sessions.get(this.userId, sessionId);
579
+ if (!session) {
580
+ throw new Error('Session not found');
581
+ }
582
+
583
+ const client = await this.getOrCreateClient(sessionId);
584
+ const allTools = await client.fetchTools();
585
+ const toolPolicy = normalizeToolPolicyForUpdate(params.toolPolicy);
586
+ validateToolPolicyAgainstTools(toolPolicy, allTools.tools, session.serverId);
587
+
588
+ await sessions.update(this.userId, sessionId, { toolPolicy });
589
+
590
+ const filteredTools = createToolPolicyGateway(this.userId, sessionId, client).filterTools({ ...session, toolPolicy }, allTools.tools);
591
+ this.emitConnectionEvent({
592
+ type: 'tools_discovered',
593
+ sessionId,
594
+ serverId: session.serverId ?? 'unknown',
595
+ toolCount: filteredTools.length,
596
+ tools: filteredTools,
597
+ timestamp: Date.now(),
598
+ });
599
+
600
+ return {
601
+ success: true,
602
+ toolPolicy,
603
+ tools: filteredTools,
604
+ toolCount: filteredTools.length,
605
+ };
606
+ }
420
607
  /**
421
608
  * Call a tool on the MCP server
422
609
  */
423
610
  private async callTool(params: CallToolParams): Promise<CallToolResult> {
424
611
  const { sessionId, toolName, toolArgs } = params;
425
612
  const client = await this.getOrCreateClient(sessionId);
426
- const result = await client.callTool(toolName, toolArgs);
613
+ const result = await createToolPolicyGateway(this.userId, sessionId, client).callTool(toolName, toolArgs);
427
614
 
428
615
  // Inject sessionId into meta so client knows who handled it
429
616
  // This allows AppHost to auto-launch without scanning all sessions
@@ -485,7 +672,7 @@ export class SSEConnectionManager {
485
672
  await client.connect();
486
673
  this.clients.set(sessionId, client);
487
674
 
488
- const tools = await client.listTools();
675
+ const { result: tools } = await this.listPolicyFilteredTools(sessionId);
489
676
 
490
677
  return { success: true, toolCount: tools.tools.length };
491
678
  } catch (error) {
@@ -541,7 +728,7 @@ export class SSEConnectionManager {
541
728
  await client.finishAuth(code, oauthState);
542
729
  this.clients.set(sessionId, client);
543
730
 
544
- const tools = await client.listTools();
731
+ const { result: tools } = await this.listPolicyFilteredTools(sessionId);
545
732
 
546
733
  return { success: true, toolCount: tools.tools.length };
547
734
  } catch (error) {
@@ -687,3 +874,9 @@ function writeSSEEvent(res: { write: Function }, event: string, data: unknown):
687
874
  res.write(`event: ${event}\n`);
688
875
  res.write(`data: ${JSON.stringify(data)}\n\n`);
689
876
  }
877
+
878
+
879
+
880
+
881
+
882
+
@@ -1,6 +1,7 @@
1
1
  import { MCPClient } from './oauth-client.js';
2
2
  import { sessions, type Session } from '../storage/index.js';
3
- import type { ToolClientProvider } from '../../shared/types.js';
3
+ import type { ToolClient, ToolClientProvider } from '../../shared/types.js';
4
+ import { createToolPolicyGateway } from './tool-policy-gateway.js';
4
5
 
5
6
  // ---------------------------------------------------------------------------
6
7
  // Constants
@@ -148,8 +149,10 @@ export class MultiSessionClient implements ToolClientProvider {
148
149
  * Use this to enumerate available tools across all connected servers,
149
150
  * or to route a tool call to the right client by `serverId`.
150
151
  */
151
- getClients(): MCPClient[] {
152
- return this.clients;
152
+ getClients(): ToolClient[] {
153
+ return this.clients.map((client) =>
154
+ createToolPolicyGateway(this.userId, client.getSessionId(), client)
155
+ );
153
156
  }
154
157
 
155
158
  /**
@@ -321,3 +324,4 @@ export class MultiSessionClient implements ToolClientProvider {
321
324
  );
322
325
  }
323
326
  }
327
+
@@ -781,6 +781,21 @@ export class MCPClient {
781
781
  }
782
782
  }
783
783
 
784
+ /**
785
+ * Lists all available tools from the connected MCP server without emitting discovery events.
786
+ * Gateways use this to apply policy before publishing tools to agents or UI state.
787
+ */
788
+ async fetchTools(): Promise<ListToolsResult> {
789
+ const request: ListToolsRequest = {
790
+ method: 'tools/list',
791
+ params: {},
792
+ };
793
+
794
+ return await this.withRetry(() =>
795
+ this.client!.request(request, ListToolsResultSchema)
796
+ );
797
+ }
798
+
784
799
  /**
785
800
  * Lists all available tools from the connected MCP server
786
801
  * @returns List of tools with their schemas and descriptions
@@ -790,14 +805,7 @@ export class MCPClient {
790
805
  this.emitStateChange('DISCOVERING');
791
806
 
792
807
  try {
793
- const request: ListToolsRequest = {
794
- method: 'tools/list',
795
- params: {},
796
- };
797
-
798
- const result = await this.withRetry(() =>
799
- this.client!.request(request, ListToolsResultSchema)
800
- );
808
+ const result = await this.fetchTools();
801
809
 
802
810
  if (this.serverId) {
803
811
  this._onConnectionEvent.fire({
@@ -821,7 +829,6 @@ export class MCPClient {
821
829
  throw error;
822
830
  }
823
831
  }
824
-
825
832
  /**
826
833
  * Executes a tool on the connected MCP server
827
834
  * @param toolName - Name of the tool to execute
@@ -830,7 +837,6 @@ export class MCPClient {
830
837
  * @throws {Error} When client is not connected
831
838
  */
832
839
  async callTool(toolName: string, toolArgs: Record<string, unknown>): Promise<CallToolResult> {
833
-
834
840
  const request: CallToolRequest = {
835
841
  method: 'tools/call',
836
842
  params: {
@@ -1173,3 +1179,12 @@ export class MCPClient {
1173
1179
  }
1174
1180
 
1175
1181
  }
1182
+
1183
+
1184
+
1185
+
1186
+
1187
+
1188
+
1189
+
1190
+
@@ -0,0 +1,90 @@
1
+ import type { Tool, ListToolsResult, CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ import type { ToolClient } from '../../shared/types.js';
3
+ import { sessions } from '../storage/index.js';
4
+ import type { Session } from '../storage/types.js';
5
+ import { assertToolAllowed, filterToolsByPolicy } from '../storage/tool-policy.js';
6
+
7
+ type RawToolClient = ToolClient & {
8
+ fetchTools(): Promise<{ tools: Tool[] }>;
9
+ listTools(): Promise<{ tools: Tool[] }>;
10
+ callTool(name: string, args: Record<string, unknown>): Promise<CallToolResult>;
11
+ };
12
+
13
+ export class ToolPolicyGateway implements ToolClient {
14
+ constructor(
15
+ private readonly userId: string,
16
+ private readonly sessionId: string,
17
+ private readonly client: RawToolClient
18
+ ) {}
19
+
20
+ isConnected(): boolean {
21
+ return this.client.isConnected();
22
+ }
23
+
24
+ getServerId(): string | undefined {
25
+ return this.client.getServerId?.();
26
+ }
27
+
28
+ getServerName(): string | undefined {
29
+ return this.client.getServerName?.();
30
+ }
31
+
32
+ getServerUrl(): string | undefined {
33
+ return this.client.getServerUrl?.();
34
+ }
35
+
36
+ getSessionId(): string {
37
+ return this.client.getSessionId?.() ?? this.sessionId;
38
+ }
39
+
40
+ async listTools(): Promise<ListToolsResult> {
41
+ const session = await this.getSession();
42
+ const result = await this.client.fetchTools();
43
+ const tools = this.filterTools(session, result.tools);
44
+
45
+ return {
46
+ ...result,
47
+ tools,
48
+ } as ListToolsResult;
49
+ }
50
+
51
+ async listAllTools(): Promise<ListToolsResult> {
52
+ return await this.client.fetchTools() as ListToolsResult;
53
+ }
54
+
55
+ async callTool(name: string, args: Record<string, unknown>): Promise<CallToolResult> {
56
+ const session = await this.getSession();
57
+ this.assertAllowed(session, name);
58
+ return await this.client.callTool(name, args);
59
+ }
60
+
61
+ filterTools(session: Session, tools: Tool[]): Tool[] {
62
+ return filterToolsByPolicy(tools, session.toolPolicy, this.getPolicyServerId(session));
63
+ }
64
+
65
+ assertAllowed(session: Session, toolName: string): void {
66
+ assertToolAllowed(session.toolPolicy, toolName, this.getPolicyServerId(session));
67
+ }
68
+
69
+ private async getSession(): Promise<Session> {
70
+ const session = await sessions.get(this.userId, this.sessionId);
71
+ if (!session) {
72
+ throw new Error('Session not found');
73
+ }
74
+ return session;
75
+ }
76
+
77
+ private getPolicyServerId(session: Session): string | undefined {
78
+ return this.client.getServerId?.() ?? session.serverId;
79
+ }
80
+ }
81
+
82
+ export function createToolPolicyGateway(
83
+ userId: string,
84
+ sessionId: string,
85
+ client: RawToolClient
86
+ ): ToolPolicyGateway {
87
+ return new ToolPolicyGateway(userId, sessionId, client);
88
+ }
89
+
90
+
@@ -4,6 +4,7 @@ import { DORMANT_SESSION_EXPIRATION_MS } from '../../shared/constants.js';
4
4
  import { generateSessionId } from '../../shared/utils.js';
5
5
  import { encryptObject, decryptObject } from './crypto.js';
6
6
  import { resolveSessionExpiresAt } from './session-lifecycle.js';
7
+ import { normalizeToolPolicy } from './tool-policy.js';
7
8
 
8
9
  export interface NeonStorageOptions {
9
10
  schema?: string;
@@ -30,6 +31,7 @@ type NeonSessionRow = {
30
31
  headers?: unknown;
31
32
  auth_url?: string | null;
32
33
  status?: SessionStatus | null;
34
+ tool_policy?: unknown;
33
35
  };
34
36
 
35
37
  type NeonCredentialsRow = {
@@ -103,6 +105,7 @@ export class NeonStorageBackend implements SessionStore {
103
105
  headers: decryptObject(row.headers),
104
106
  authUrl: row.auth_url ?? undefined,
105
107
  status: row.status ?? 'pending',
108
+ toolPolicy: normalizeToolPolicy(row.tool_policy as Parameters<typeof normalizeToolPolicy>[0]),
106
109
  };
107
110
  }
108
111
 
@@ -135,43 +138,35 @@ export class NeonStorageBackend implements SessionStore {
135
138
  const status = session.status ?? 'pending';
136
139
  const createdAt = new Date(session.createdAt || Date.now()).toISOString();
137
140
  const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
138
- const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
141
+ const createdAtMs = new Date(createdAt).getTime();
142
+ const expiresAt = resolveSessionExpiresAt(status, createdAtMs);
143
+ const toolPolicy = normalizeToolPolicy(session.toolPolicy, createdAtMs);
144
+
145
+ const columns: string[] = [
146
+ 'session_id', 'user_id', 'server_id', 'server_name',
147
+ 'server_url', 'transport_type', 'callback_url',
148
+ 'created_at', 'updated_at', 'headers', 'auth_url',
149
+ 'status', 'expires_at',
150
+ ];
151
+ const values: unknown[] = [
152
+ sessionId, userId, session.serverId, session.serverName,
153
+ session.serverUrl, session.transportType, session.callbackUrl,
154
+ createdAt, updatedAt, encryptObject(session.headers),
155
+ session.authUrl ?? null, status,
156
+ expiresAt === null ? null : new Date(expiresAt).toISOString(),
157
+ ];
158
+
159
+ if (toolPolicy) {
160
+ columns.push('tool_policy');
161
+ values.push(toolPolicy);
162
+ }
163
+
164
+ const placeholders = values.map((_, i) => `$${i + 1}`);
139
165
 
140
166
  try {
141
167
  await this.sql.query(
142
- `INSERT INTO ${this.tableName} (
143
- session_id,
144
- user_id,
145
- server_id,
146
- server_name,
147
- server_url,
148
- transport_type,
149
- callback_url,
150
- created_at,
151
- updated_at,
152
- headers,
153
- auth_url,
154
- status,
155
- expires_at
156
- ) VALUES (
157
- $1, $2, $3, $4, $5, $6, $7, $8,
158
- $9, $10, $11, $12, $13
159
- )`,
160
- [
161
- sessionId,
162
- userId,
163
- session.serverId,
164
- session.serverName,
165
- session.serverUrl,
166
- session.transportType,
167
- session.callbackUrl,
168
- createdAt,
169
- updatedAt,
170
- encryptObject(session.headers),
171
- session.authUrl ?? null,
172
- status,
173
- expiresAt === null ? null : new Date(expiresAt).toISOString(),
174
- ]
168
+ `INSERT INTO ${this.tableName} (${columns.join(', ')}) VALUES (${placeholders.join(', ')})`,
169
+ values
175
170
  );
176
171
  } catch (error: any) {
177
172
  if (error.code === '23505') {
@@ -200,38 +195,44 @@ export class NeonStorageBackend implements SessionStore {
200
195
  'callbackUrl' in data ||
201
196
  'status' in data ||
202
197
  'headers' in data ||
203
- 'authUrl' in data
198
+ 'authUrl' in data ||
199
+ 'toolPolicy' in data
204
200
  );
205
201
 
206
202
  if (shouldUpdateSession) {
203
+ const setClauses: string[] = [];
204
+ const values: unknown[] = [];
205
+ let paramIndex = 1;
206
+
207
+ const addSet = (column: string, value: unknown) => {
208
+ setClauses.push(`${column} = $${paramIndex++}`);
209
+ values.push(value);
210
+ };
211
+
212
+ addSet('server_id', updatedSession.serverId);
213
+ addSet('server_name', updatedSession.serverName);
214
+ addSet('server_url', updatedSession.serverUrl);
215
+ addSet('transport_type', updatedSession.transportType);
216
+ addSet('callback_url', updatedSession.callbackUrl);
217
+ addSet('status', status);
218
+ addSet('headers', encryptObject(updatedSession.headers));
219
+ addSet('auth_url', updatedSession.authUrl ?? null);
220
+ addSet('expires_at', expiresAt === null ? null : new Date(expiresAt).toISOString());
221
+
222
+ if ('toolPolicy' in data) {
223
+ const policyUpdatedAt = updatedSession.updatedAt ?? Date.now();
224
+ const toolPolicy = normalizeToolPolicy(updatedSession.toolPolicy, policyUpdatedAt) ?? { mode: 'all' as const, toolIds: [], updatedAt: policyUpdatedAt };
225
+ addSet('tool_policy', toolPolicy);
226
+ }
227
+
228
+ setClauses.push('updated_at = now()');
229
+
207
230
  const updatedRows = await this.sql.query(
208
231
  `UPDATE ${this.tableName}
209
- SET
210
- server_id = $1,
211
- server_name = $2,
212
- server_url = $3,
213
- transport_type = $4,
214
- callback_url = $5,
215
- status = $6,
216
- headers = $7,
217
- auth_url = $8,
218
- expires_at = $9,
219
- updated_at = now()
220
- WHERE user_id = $10 AND session_id = $11
232
+ SET ${setClauses.join(', ')}
233
+ WHERE user_id = $${paramIndex++} AND session_id = $${paramIndex++}
221
234
  RETURNING id`,
222
- [
223
- updatedSession.serverId,
224
- updatedSession.serverName,
225
- updatedSession.serverUrl,
226
- updatedSession.transportType,
227
- updatedSession.callbackUrl,
228
- status,
229
- encryptObject(updatedSession.headers),
230
- updatedSession.authUrl ?? null,
231
- expiresAt === null ? null : new Date(expiresAt).toISOString(),
232
- userId,
233
- sessionId,
234
- ]
235
+ [...values, userId, sessionId]
235
236
  ) as Array<{ id: string }>;
236
237
 
237
238
  if (updatedRows.length === 0) {
@@ -412,3 +413,7 @@ export class NeonStorageBackend implements SessionStore {
412
413
  // Neon HTTP queries do not hold a persistent connection.
413
414
  }
414
415
  }
416
+
417
+
418
+
419
+
@@ -4,6 +4,7 @@ import {
4
4
  STATE_EXPIRATION_MS,
5
5
  } from '../../shared/constants.js';
6
6
  import type { Session, SessionStatus } from './types.js';
7
+ import { normalizeToolPolicy } from './tool-policy.js';
7
8
 
8
9
  export function resolveSessionExpiresAt(status: SessionStatus = 'pending', referenceTime = Date.now()): number | null {
9
10
  return status === 'active' ? null : referenceTime + STATE_EXPIRATION_MS;
@@ -26,6 +27,7 @@ export function normalizeNewSession(session: Session, now = Date.now()): Session
26
27
  createdAt,
27
28
  updatedAt,
28
29
  expiresAt: resolveSessionExpiresAt(status, status === 'active' ? updatedAt : createdAt),
30
+ toolPolicy: normalizeToolPolicy(session.toolPolicy, updatedAt),
29
31
  };
30
32
  }
31
33
 
@@ -46,6 +48,7 @@ export function mergeSessionUpdate(
46
48
  ...updated,
47
49
  status,
48
50
  expiresAt: resolveSessionExpiresAt(status, updatedAt),
51
+ toolPolicy: normalizeToolPolicy(updated.toolPolicy, updatedAt),
49
52
  };
50
53
  }
51
54
 
@@ -63,6 +66,7 @@ export function normalizeStoredSession(session: Session): Session {
63
66
  createdAt,
64
67
  updatedAt,
65
68
  expiresAt,
69
+ toolPolicy: normalizeToolPolicy(session.toolPolicy, updatedAt),
66
70
  };
67
71
  }
68
72
 
@@ -76,3 +80,4 @@ export function isSessionExpired(session: Session, now = Date.now()): boolean {
76
80
 
77
81
  return typeof hydrated.expiresAt === 'number' && hydrated.expiresAt < now;
78
82
  }
83
+