@mcp-ts/sdk 2.4.5 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) 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 +6 -0
  28. package/dist/client/index.js.map +1 -1
  29. package/dist/client/index.mjs +6 -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 +34 -0
  34. package/dist/client/react.js.map +1 -1
  35. package/dist/client/react.mjs +34 -0
  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 +6 -0
  40. package/dist/client/vue.js.map +1 -1
  41. package/dist/client/vue.mjs +6 -0
  42. package/dist/client/vue.mjs.map +1 -1
  43. package/dist/{index-CkM3p0eE.d.mts → index-CTURVnom.d.mts} +5 -1
  44. package/dist/{index-CZk2gu2E.d.ts → index-sVcqrhf7.d.ts} +5 -1
  45. package/dist/index.d.mts +4 -4
  46. package/dist/index.d.ts +4 -4
  47. package/dist/index.js +314 -82
  48. package/dist/index.js.map +1 -1
  49. package/dist/index.mjs +310 -82
  50. package/dist/index.mjs.map +1 -1
  51. package/dist/{multi-session-client-ChQrBZhF.d.mts → multi-session-client-CSPSHHla.d.ts} +14 -2
  52. package/dist/{multi-session-client-QOOPdEVp.d.ts → multi-session-client-DMZGVABI.d.mts} +14 -2
  53. package/dist/server/index.d.mts +13 -4
  54. package/dist/server/index.d.ts +13 -4
  55. package/dist/server/index.js +308 -82
  56. package/dist/server/index.js.map +1 -1
  57. package/dist/server/index.mjs +304 -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-DQ-HrD7W.d.ts → tool-router-CS9l0w4a.d.ts} +1 -1
  64. package/dist/{tool-router-CGs3IDOJ.d.mts → tool-router-CVLBaCwH.d.mts} +1 -1
  65. package/dist/{types-Bf-7GOLW.d.mts → types-DK_NGWd4.d.mts} +37 -3
  66. package/dist/{types-Bf-7GOLW.d.ts → types-DK_NGWd4.d.ts} +37 -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 +1 -1
  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 +17 -0
  75. package/src/client/react/use-mcp.ts +63 -0
  76. package/src/server/handlers/sse-handler.ts +116 -9
  77. package/src/server/mcp/multi-session-client.ts +7 -3
  78. package/src/server/mcp/oauth-client.ts +25 -10
  79. package/src/server/mcp/tool-policy-gateway.ts +90 -0
  80. package/src/server/storage/neon-backend.ts +65 -60
  81. package/src/server/storage/session-lifecycle.ts +5 -0
  82. package/src/server/storage/supabase-backend.ts +27 -15
  83. package/src/server/storage/tool-policy.ts +89 -0
  84. package/src/server/storage/types.ts +11 -0
  85. package/src/shared/types.ts +48 -1
@@ -32,13 +32,19 @@ import type {
32
32
  ListPromptsResult,
33
33
  ListResourcesResult,
34
34
  CallToolResult,
35
+ SetToolPolicyParams,
36
+ SetToolPolicyResult,
37
+ GetToolPolicyParams,
38
+ GetToolPolicyResult,
35
39
  } from '../../shared/types.js';
36
40
  import { RpcErrorCodes } from '../../shared/errors.js';
37
41
  import { UnauthorizedError } from '../../shared/errors.js';
38
42
  import { isConnectionEvent, isRpcResponseEvent } from '../../shared/event-routing.js';
39
43
  import { parseOAuthState } from '../../shared/utils.js';
40
44
  import { MCPClient } from '../mcp/oauth-client.js';
41
- import { sessions, generateServerId } from '../storage/index.js';
45
+ import { sessions, generateServerId, type Session } from '../storage/index.js';
46
+ import { createToolId, isToolAllowed, normalizeToolPolicyForUpdate, validateToolPolicyAgainstTools } from '../storage/tool-policy.js';
47
+ import { createToolPolicyGateway } from '../mcp/tool-policy-gateway.js';
42
48
 
43
49
  // ============================================
44
50
  // Types & Interfaces
@@ -149,7 +155,7 @@ export class SSEConnectionManager {
149
155
  */
150
156
  async handleRequest(request: McpRpcRequest): Promise<McpRpcResponse> {
151
157
  try {
152
- let result: SessionListResult | ConnectResult | DisconnectResult | GetSessionResult | FinishAuthResult | ListToolsRpcResult | ListPromptsResult | ListResourcesResult | unknown;
158
+ let result: SessionListResult | ConnectResult | DisconnectResult | GetSessionResult | FinishAuthResult | ListToolsRpcResult | SetToolPolicyResult | GetToolPolicyResult | ListPromptsResult | ListResourcesResult | unknown;
153
159
 
154
160
  switch (request.method) {
155
161
  case 'listSessions':
@@ -168,6 +174,14 @@ export class SSEConnectionManager {
168
174
  result = await this.listTools(request.params as SessionParams);
169
175
  break;
170
176
 
177
+ case 'setToolPolicy':
178
+ result = await this.setToolPolicy(request.params as SetToolPolicyParams);
179
+ break;
180
+
181
+ case 'getToolPolicy':
182
+ result = await this.getToolPolicy(request.params as GetToolPolicyParams);
183
+ break;
184
+
171
185
  case 'callTool':
172
186
  result = await this.callTool(request.params as CallToolParams);
173
187
  break;
@@ -241,6 +255,7 @@ export class SSEConnectionManager {
241
255
  createdAt: s.createdAt,
242
256
  updatedAt: s.updatedAt ?? s.createdAt,
243
257
  status: s.status ?? 'pending',
258
+ toolPolicy: s.toolPolicy,
244
259
  })),
245
260
  };
246
261
  }
@@ -315,8 +330,8 @@ export class SSEConnectionManager {
315
330
  // Attempt connection
316
331
  await client.connect();
317
332
 
318
- // Fetch tools
319
- await client.listTools();
333
+ // Fetch policy-filtered tools for agent-facing discovery
334
+ await this.listPolicyFilteredTools(sessionId);
320
335
 
321
336
  return {
322
337
  sessionId,
@@ -407,23 +422,109 @@ export class SSEConnectionManager {
407
422
  return client;
408
423
  }
409
424
 
425
+ private async listPolicyFilteredTools(sessionId: string): Promise<{ session: Session; result: ListToolsRpcResult }> {
426
+ const session = await sessions.get(this.userId, sessionId);
427
+ if (!session) {
428
+ throw new Error('Session not found');
429
+ }
430
+
431
+ const client = await this.getOrCreateClient(sessionId);
432
+ const gateway = createToolPolicyGateway(this.userId, sessionId, client);
433
+ const result = await gateway.listTools();
434
+
435
+ this.emitConnectionEvent({
436
+ type: 'tools_discovered',
437
+ sessionId,
438
+ serverId: session.serverId ?? 'unknown',
439
+ toolCount: result.tools.length,
440
+ tools: result.tools,
441
+ timestamp: Date.now(),
442
+ });
443
+
444
+ return { session, result };
445
+ }
410
446
  /**
411
447
  * List tools from a session
412
448
  */
413
449
  private async listTools(params: SessionParams): Promise<ListToolsRpcResult> {
414
450
  const { sessionId } = params;
415
- const client = await this.getOrCreateClient(sessionId);
416
- const result = await client.listTools();
451
+ const { result } = await this.listPolicyFilteredTools(sessionId);
417
452
  return { tools: result.tools };
418
453
  }
419
454
 
455
+ /**
456
+ * Get all raw tools with effective access state for management UI.
457
+ */
458
+ private async getToolPolicy(params: GetToolPolicyParams): Promise<GetToolPolicyResult> {
459
+ const { sessionId } = params;
460
+ const session = await sessions.get(this.userId, sessionId);
461
+ if (!session) {
462
+ throw new Error('Session not found');
463
+ }
464
+
465
+ const client = await this.getOrCreateClient(sessionId);
466
+ const allTools = await client.fetchTools();
467
+ const toolPolicy = session.toolPolicy ?? {
468
+ mode: 'all' as const,
469
+ toolIds: [],
470
+ updatedAt: session.updatedAt ?? session.createdAt,
471
+ };
472
+ const serverId = session.serverId ?? 'unknown';
473
+ const tools = allTools.tools.map((tool) => ({
474
+ ...tool,
475
+ toolId: createToolId(serverId, tool.name),
476
+ allowed: isToolAllowed(toolPolicy, tool.name, session.serverId),
477
+ }));
478
+
479
+ return {
480
+ toolPolicy,
481
+ tools,
482
+ toolCount: tools.length,
483
+ allowedToolCount: tools.filter((tool) => tool.allowed).length,
484
+ };
485
+ }
486
+
487
+ /**
488
+ * Update per-session tool access policy.
489
+ */
490
+ private async setToolPolicy(params: SetToolPolicyParams): Promise<SetToolPolicyResult> {
491
+ const { sessionId } = params;
492
+ const session = await sessions.get(this.userId, sessionId);
493
+ if (!session) {
494
+ throw new Error('Session not found');
495
+ }
496
+
497
+ const client = await this.getOrCreateClient(sessionId);
498
+ const allTools = await client.fetchTools();
499
+ const toolPolicy = normalizeToolPolicyForUpdate(params.toolPolicy);
500
+ validateToolPolicyAgainstTools(toolPolicy, allTools.tools, session.serverId);
501
+
502
+ await sessions.update(this.userId, sessionId, { toolPolicy });
503
+
504
+ const filteredTools = createToolPolicyGateway(this.userId, sessionId, client).filterTools({ ...session, toolPolicy }, allTools.tools);
505
+ this.emitConnectionEvent({
506
+ type: 'tools_discovered',
507
+ sessionId,
508
+ serverId: session.serverId ?? 'unknown',
509
+ toolCount: filteredTools.length,
510
+ tools: filteredTools,
511
+ timestamp: Date.now(),
512
+ });
513
+
514
+ return {
515
+ success: true,
516
+ toolPolicy,
517
+ tools: filteredTools,
518
+ toolCount: filteredTools.length,
519
+ };
520
+ }
420
521
  /**
421
522
  * Call a tool on the MCP server
422
523
  */
423
524
  private async callTool(params: CallToolParams): Promise<CallToolResult> {
424
525
  const { sessionId, toolName, toolArgs } = params;
425
526
  const client = await this.getOrCreateClient(sessionId);
426
- const result = await client.callTool(toolName, toolArgs);
527
+ const result = await createToolPolicyGateway(this.userId, sessionId, client).callTool(toolName, toolArgs);
427
528
 
428
529
  // Inject sessionId into meta so client knows who handled it
429
530
  // This allows AppHost to auto-launch without scanning all sessions
@@ -485,7 +586,7 @@ export class SSEConnectionManager {
485
586
  await client.connect();
486
587
  this.clients.set(sessionId, client);
487
588
 
488
- const tools = await client.listTools();
589
+ const { result: tools } = await this.listPolicyFilteredTools(sessionId);
489
590
 
490
591
  return { success: true, toolCount: tools.tools.length };
491
592
  } catch (error) {
@@ -541,7 +642,7 @@ export class SSEConnectionManager {
541
642
  await client.finishAuth(code, oauthState);
542
643
  this.clients.set(sessionId, client);
543
644
 
544
- const tools = await client.listTools();
645
+ const { result: tools } = await this.listPolicyFilteredTools(sessionId);
545
646
 
546
647
  return { success: true, toolCount: tools.tools.length };
547
648
  } catch (error) {
@@ -687,3 +788,9 @@ function writeSSEEvent(res: { write: Function }, event: string, data: unknown):
687
788
  res.write(`event: ${event}\n`);
688
789
  res.write(`data: ${JSON.stringify(data)}\n\n`);
689
790
  }
791
+
792
+
793
+
794
+
795
+
796
+
@@ -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
+
@@ -3,6 +3,7 @@ import type { SessionStore, Session, SessionCredentials } from './types.js';
3
3
  import { generateSessionId } from '../../shared/utils.js';
4
4
  import { encryptObject, decryptObject } from './crypto.js';
5
5
  import { resolveSessionExpiresAt } from './session-lifecycle.js';
6
+ import { normalizeToolPolicy } from './tool-policy.js';
6
7
  import { DORMANT_SESSION_EXPIRATION_MS } from '../../shared/constants.js';
7
8
 
8
9
  export class SupabaseStorageBackend implements SessionStore {
@@ -51,6 +52,7 @@ export class SupabaseStorageBackend implements SessionStore {
51
52
  headers: decryptObject(row.headers),
52
53
  authUrl: row.auth_url,
53
54
  status: row.status ?? 'pending',
55
+ toolPolicy: normalizeToolPolicy(row.tool_policy),
54
56
  };
55
57
  }
56
58
 
@@ -85,23 +87,30 @@ export class SupabaseStorageBackend implements SessionStore {
85
87
  const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
86
88
  const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
87
89
 
90
+ const insertData: Record<string, unknown> = {
91
+ session_id: sessionId,
92
+ user_id: userId,
93
+ server_id: session.serverId,
94
+ server_name: session.serverName,
95
+ server_url: session.serverUrl,
96
+ transport_type: session.transportType,
97
+ callback_url: session.callbackUrl,
98
+ created_at: createdAt,
99
+ updated_at: updatedAt,
100
+ headers: encryptObject(session.headers),
101
+ auth_url: session.authUrl ?? null,
102
+ status,
103
+ expires_at: expiresAt === null ? null : new Date(expiresAt).toISOString(),
104
+ };
105
+
106
+ const toolPolicy = normalizeToolPolicy(session.toolPolicy);
107
+ if (toolPolicy) {
108
+ insertData.tool_policy = toolPolicy;
109
+ }
110
+
88
111
  const { error } = await this.supabase
89
112
  .from('mcp_sessions')
90
- .insert({
91
- session_id: sessionId,
92
- user_id: userId,
93
- server_id: session.serverId,
94
- server_name: session.serverName,
95
- server_url: session.serverUrl,
96
- transport_type: session.transportType,
97
- callback_url: session.callbackUrl,
98
- created_at: createdAt,
99
- updated_at: updatedAt,
100
- headers: encryptObject(session.headers),
101
- auth_url: session.authUrl ?? null,
102
- status,
103
- expires_at: expiresAt === null ? null : new Date(expiresAt).toISOString(),
104
- });
113
+ .insert(insertData);
105
114
 
106
115
  if (error) {
107
116
  if (error.code === '23505') {
@@ -130,6 +139,7 @@ export class SupabaseStorageBackend implements SessionStore {
130
139
  }
131
140
  if ('headers' in data) updateData.headers = encryptObject(data.headers);
132
141
  if ('authUrl' in data) updateData.auth_url = data.authUrl ?? null;
142
+ if ('toolPolicy' in data) updateData.tool_policy = normalizeToolPolicy(data.toolPolicy);
133
143
 
134
144
  const shouldUpdateSession = Object.keys(updateData).some((key) => key !== 'updated_at');
135
145
 
@@ -351,3 +361,5 @@ export class SupabaseStorageBackend implements SessionStore {
351
361
  // Supabase client handles its own connection pooling over HTTP.
352
362
  }
353
363
  }
364
+
365
+