@mcp-ts/sdk 2.4.4 → 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 (89) 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 +36 -6
  34. package/dist/client/react.js.map +1 -1
  35. package/dist/client/react.mjs +36 -6
  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 +374 -130
  48. package/dist/index.js.map +1 -1
  49. package/dist/index.mjs +370 -130
  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 +368 -130
  56. package/dist/server/index.js.map +1 -1
  57. package/dist/server/index.mjs +364 -130
  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 +2 -6
  62. package/dist/shared/index.js.map +1 -1
  63. package/dist/shared/index.mjs +2 -6
  64. package/dist/shared/index.mjs.map +1 -1
  65. package/dist/{tool-router-DQ-HrD7W.d.ts → tool-router-CS9l0w4a.d.ts} +1 -1
  66. package/dist/{tool-router-CGs3IDOJ.d.mts → tool-router-CVLBaCwH.d.mts} +1 -1
  67. package/dist/{types-Bf-7GOLW.d.mts → types-DK_NGWd4.d.mts} +37 -3
  68. package/dist/{types-Bf-7GOLW.d.ts → types-DK_NGWd4.d.ts} +37 -3
  69. package/migrations/neon/20260513010000_install_mcp_sessions.sql +1 -0
  70. package/migrations/supabase/20260330195700_install_mcp_sessions.sql +1 -0
  71. package/package.json +1 -1
  72. package/src/adapters/agui-adapter.ts +6 -2
  73. package/src/adapters/ai-adapter.ts +3 -1
  74. package/src/adapters/langchain-adapter.ts +5 -3
  75. package/src/adapters/mastra-adapter.ts +5 -3
  76. package/src/client/core/sse-client.ts +17 -0
  77. package/src/client/react/use-mcp.ts +63 -0
  78. package/src/server/handlers/sse-handler.ts +117 -10
  79. package/src/server/mcp/multi-session-client.ts +7 -3
  80. package/src/server/mcp/oauth-client.ts +78 -55
  81. package/src/server/mcp/tool-policy-gateway.ts +90 -0
  82. package/src/server/storage/index.ts +1 -1
  83. package/src/server/storage/neon-backend.ts +65 -60
  84. package/src/server/storage/session-lifecycle.ts +5 -0
  85. package/src/server/storage/supabase-backend.ts +27 -15
  86. package/src/server/storage/tool-policy.ts +89 -0
  87. package/src/server/storage/types.ts +11 -0
  88. package/src/shared/types.ts +48 -1
  89. package/src/shared/utils.ts +16 -14
@@ -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 } 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
  }
@@ -256,7 +271,7 @@ export class SSEConnectionManager {
256
271
  // Tool name format: tool_<serverId>_<toolName> - with 12 char serverId leaves 46 chars for tool name
257
272
  const serverId = params.serverId && params.serverId.length <= 12
258
273
  ? params.serverId
259
- : await sessions.generateSessionId();
274
+ : generateServerId();
260
275
 
261
276
  // Check for existing connections
262
277
  const existingSessions = await sessions.list(this.userId);
@@ -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
+
@@ -360,7 +360,15 @@ export class MCPClient {
360
360
  if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
361
361
  this.createdAt = Date.now();
362
362
  const updatedAt = this.createdAt;
363
- console.log(`[MCPClient] Creating pending session ${this.sessionId} for connection setup`);
363
+ this._onObservabilityEvent.fire({
364
+ type: 'mcp:client:session_created',
365
+ level: 'info',
366
+ message: `Creating pending session ${this.sessionId} for connection setup`,
367
+ sessionId: this.sessionId,
368
+ serverId: this.serverId,
369
+ timestamp: Date.now(),
370
+ id: nanoid(),
371
+ });
364
372
  await sessions.create({
365
373
  sessionId: this.sessionId,
366
374
  userId: this.userId,
@@ -542,7 +550,15 @@ export class MCPClient {
542
550
 
543
551
  // Refresh session metadata on every successful connect so active sessions
544
552
  // record ongoing usage and don't look dormant to session cleanup jobs.
545
- console.log(`[MCPClient] Saving active session ${this.sessionId} (connect success)`);
553
+ this._onObservabilityEvent.fire({
554
+ type: 'mcp:client:session_saved',
555
+ level: 'info',
556
+ message: `Saving active session ${this.sessionId} (connect success)`,
557
+ sessionId: this.sessionId,
558
+ serverId: this.serverId,
559
+ timestamp: Date.now(),
560
+ id: nanoid(),
561
+ });
546
562
  await this.saveSession('active');
547
563
  } catch (error) {
548
564
  /** Handle Authentication Errors */
@@ -580,7 +596,15 @@ export class MCPClient {
580
596
  }
581
597
 
582
598
  this.emitStateChange('AUTHENTICATING');
583
- console.log(`[MCPClient] Saving pending OAuth session ${this.sessionId}`);
599
+ this._onObservabilityEvent.fire({
600
+ type: 'mcp:client:session_saved',
601
+ level: 'info',
602
+ message: `Saving pending OAuth session ${this.sessionId}`,
603
+ sessionId: this.sessionId,
604
+ serverId: this.serverId,
605
+ timestamp: Date.now(),
606
+ id: nanoid(),
607
+ });
584
608
  await this.saveSession('pending');
585
609
 
586
610
  if (this.serverId) {
@@ -686,34 +710,30 @@ export class MCPClient {
686
710
  authenticatedStateEmitted = true;
687
711
  }
688
712
 
689
- this.emitProgress('Creating authenticated client...');
690
-
691
- this.client = new Client(
692
- {
693
- name: MCP_CLIENT_NAME,
694
- version: MCP_CLIENT_VERSION,
695
- },
696
- {
697
- capabilities: {
698
- extensions: {
699
- 'io.modelcontextprotocol/ui': {
700
- mimeTypes: ['text/html+mcp'],
701
- },
702
- },
703
- } as McpAppClientCapabilities
704
- }
705
- );
706
-
707
713
  this.emitStateChange('CONNECTING');
708
714
 
709
- /** We explicitly try to connect with the transport we just auth'd with first */
715
+ // The SDK Client may still have a transport attached from a prior
716
+ // connect() that failed with UnauthorizedError; close it first so
717
+ // we can negotiate a fresh session with the newly-exchanged tokens.
718
+ if (this.client.transport) {
719
+ try { await this.client.close(); } catch {}
720
+ }
721
+
710
722
  await this.client.connect(this.transport);
711
723
 
712
724
  /** Connection succeeded — lock in the transport type */
713
725
  this.transportType = currentType;
714
726
 
715
727
  this.emitStateChange('CONNECTED');
716
- console.log(`[MCPClient] Saving active session ${this.sessionId} (OAuth complete)`);
728
+ this._onObservabilityEvent.fire({
729
+ type: 'mcp:client:session_saved',
730
+ level: 'info',
731
+ message: `Saving active session ${this.sessionId} (OAuth complete)`,
732
+ sessionId: this.sessionId,
733
+ serverId: this.serverId,
734
+ timestamp: Date.now(),
735
+ id: nanoid(),
736
+ });
717
737
  await this.saveSession('active');
718
738
 
719
739
  return; // Success, exit function
@@ -761,27 +781,31 @@ export class MCPClient {
761
781
  }
762
782
  }
763
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
+
764
799
  /**
765
800
  * Lists all available tools from the connected MCP server
766
801
  * @returns List of tools with their schemas and descriptions
767
802
  * @throws {Error} When client is not connected
768
803
  */
769
804
  async listTools(): Promise<ListToolsResult> {
770
- if (!this.client) {
771
- throw new Error('Not connected to server');
772
- }
773
-
774
805
  this.emitStateChange('DISCOVERING');
775
806
 
776
807
  try {
777
- const request: ListToolsRequest = {
778
- method: 'tools/list',
779
- params: {},
780
- };
781
-
782
- const result = await this.withRetry(() =>
783
- this.client!.request(request, ListToolsResultSchema)
784
- );
808
+ const result = await this.fetchTools();
785
809
 
786
810
  if (this.serverId) {
787
811
  this._onConnectionEvent.fire({
@@ -805,7 +829,6 @@ export class MCPClient {
805
829
  throw error;
806
830
  }
807
831
  }
808
-
809
832
  /**
810
833
  * Executes a tool on the connected MCP server
811
834
  * @param toolName - Name of the tool to execute
@@ -814,10 +837,6 @@ export class MCPClient {
814
837
  * @throws {Error} When client is not connected
815
838
  */
816
839
  async callTool(toolName: string, toolArgs: Record<string, unknown>): Promise<CallToolResult> {
817
- if (!this.client) {
818
- throw new Error('Not connected to server');
819
- }
820
-
821
840
  const request: CallToolRequest = {
822
841
  method: 'tools/call',
823
842
  params: {
@@ -877,10 +896,6 @@ export class MCPClient {
877
896
  * @throws {Error} When client is not connected
878
897
  */
879
898
  async listPrompts(): Promise<ListPromptsResult> {
880
- if (!this.client) {
881
- throw new Error('Not connected to server');
882
- }
883
-
884
899
  this.emitStateChange('DISCOVERING');
885
900
 
886
901
  try {
@@ -913,9 +928,6 @@ export class MCPClient {
913
928
  * @throws {Error} When client is not connected
914
929
  */
915
930
  async getPrompt(name: string, args?: Record<string, string>): Promise<GetPromptResult> {
916
- if (!this.client) {
917
- throw new Error('Not connected to server');
918
- }
919
931
 
920
932
  const request: GetPromptRequest = {
921
933
  method: 'prompts/get',
@@ -936,10 +948,6 @@ export class MCPClient {
936
948
  * @throws {Error} When client is not connected
937
949
  */
938
950
  async listResources(): Promise<ListResourcesResult> {
939
- if (!this.client) {
940
- throw new Error('Not connected to server');
941
- }
942
-
943
951
  this.emitStateChange('DISCOVERING');
944
952
 
945
953
  try {
@@ -971,9 +979,6 @@ export class MCPClient {
971
979
  * @throws {Error} When client is not connected
972
980
  */
973
981
  async readResource(uri: string): Promise<ReadResourceResult> {
974
- if (!this.client) {
975
- throw new Error('Not connected to server');
976
- }
977
982
 
978
983
  const request: ReadResourceRequest = {
979
984
  method: 'resources/read',
@@ -1031,7 +1036,16 @@ export class MCPClient {
1031
1036
  try {
1032
1037
  await this.ensureSession();
1033
1038
  } catch (error) {
1034
- console.warn('[MCPClient] Initialization failed during clearSession:', error);
1039
+ this._onObservabilityEvent.fire({
1040
+ type: 'mcp:client:error',
1041
+ level: 'warn',
1042
+ message: 'Initialization failed during clearSession',
1043
+ sessionId: this.sessionId,
1044
+ serverId: this.serverId,
1045
+ payload: { error: String(error) },
1046
+ timestamp: Date.now(),
1047
+ id: nanoid(),
1048
+ });
1035
1049
  }
1036
1050
 
1037
1051
  if (this.oauthProvider) {
@@ -1165,3 +1179,12 @@ export class MCPClient {
1165
1179
  }
1166
1180
 
1167
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
+
@@ -9,7 +9,7 @@ import type { SessionStore, Session, SessionMutationEvent, SessionMutationListen
9
9
 
10
10
  // Re-export types
11
11
  export * from './types.js';
12
- export { generateSessionId } from '../../shared/utils.js';
12
+ export { generateSessionId, generateServerId } from '../../shared/utils.js';
13
13
  export { RedisStorageBackend, MemoryStorageBackend, FileStorageBackend, SqliteStorage, SupabaseStorageBackend, NeonStorageBackend };
14
14
 
15
15
  export function createSupabaseStorageBackend(client: any): SupabaseStorageBackend {