@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.
- package/dist/adapters/agui-adapter.d.mts +3 -3
- package/dist/adapters/agui-adapter.d.ts +3 -3
- package/dist/adapters/agui-adapter.js.map +1 -1
- package/dist/adapters/agui-adapter.mjs.map +1 -1
- package/dist/adapters/agui-middleware.d.mts +3 -3
- package/dist/adapters/agui-middleware.d.ts +3 -3
- package/dist/adapters/agui-middleware.js.map +1 -1
- package/dist/adapters/agui-middleware.mjs.map +1 -1
- package/dist/adapters/ai-adapter.d.mts +3 -3
- package/dist/adapters/ai-adapter.d.ts +3 -3
- package/dist/adapters/ai-adapter.js.map +1 -1
- package/dist/adapters/ai-adapter.mjs.map +1 -1
- package/dist/adapters/langchain-adapter.d.mts +3 -3
- package/dist/adapters/langchain-adapter.d.ts +3 -3
- package/dist/adapters/langchain-adapter.js +2 -2
- package/dist/adapters/langchain-adapter.js.map +1 -1
- package/dist/adapters/langchain-adapter.mjs +2 -2
- package/dist/adapters/langchain-adapter.mjs.map +1 -1
- package/dist/adapters/mastra-adapter.d.mts +2 -2
- package/dist/adapters/mastra-adapter.d.ts +2 -2
- package/dist/adapters/mastra-adapter.js +2 -2
- package/dist/adapters/mastra-adapter.js.map +1 -1
- package/dist/adapters/mastra-adapter.mjs +2 -2
- package/dist/adapters/mastra-adapter.mjs.map +1 -1
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +6 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +6 -0
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +15 -4
- package/dist/client/react.d.ts +15 -4
- package/dist/client/react.js +34 -0
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +34 -0
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +4 -4
- package/dist/client/vue.d.ts +4 -4
- package/dist/client/vue.js +6 -0
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +6 -0
- package/dist/client/vue.mjs.map +1 -1
- package/dist/{index-CkM3p0eE.d.mts → index-CTURVnom.d.mts} +5 -1
- package/dist/{index-CZk2gu2E.d.ts → index-sVcqrhf7.d.ts} +5 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +314 -82
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +310 -82
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-ChQrBZhF.d.mts → multi-session-client-CSPSHHla.d.ts} +14 -2
- package/dist/{multi-session-client-QOOPdEVp.d.ts → multi-session-client-DMZGVABI.d.mts} +14 -2
- package/dist/server/index.d.mts +13 -4
- package/dist/server/index.d.ts +13 -4
- package/dist/server/index.js +308 -82
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +304 -82
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +4 -4
- package/dist/shared/index.d.ts +4 -4
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs.map +1 -1
- package/dist/{tool-router-DQ-HrD7W.d.ts → tool-router-CS9l0w4a.d.ts} +1 -1
- package/dist/{tool-router-CGs3IDOJ.d.mts → tool-router-CVLBaCwH.d.mts} +1 -1
- package/dist/{types-Bf-7GOLW.d.mts → types-DK_NGWd4.d.mts} +37 -3
- package/dist/{types-Bf-7GOLW.d.ts → types-DK_NGWd4.d.ts} +37 -3
- package/migrations/neon/20260513010000_install_mcp_sessions.sql +1 -0
- package/migrations/supabase/20260330195700_install_mcp_sessions.sql +1 -0
- package/package.json +1 -1
- package/src/adapters/agui-adapter.ts +6 -2
- package/src/adapters/ai-adapter.ts +3 -1
- package/src/adapters/langchain-adapter.ts +5 -3
- package/src/adapters/mastra-adapter.ts +5 -3
- package/src/client/core/sse-client.ts +17 -0
- package/src/client/react/use-mcp.ts +63 -0
- package/src/server/handlers/sse-handler.ts +116 -9
- package/src/server/mcp/multi-session-client.ts +7 -3
- package/src/server/mcp/oauth-client.ts +25 -10
- package/src/server/mcp/tool-policy-gateway.ts +90 -0
- package/src/server/storage/neon-backend.ts +65 -60
- package/src/server/storage/session-lifecycle.ts +5 -0
- package/src/server/storage/supabase-backend.ts +27 -15
- package/src/server/storage/tool-policy.ts +89 -0
- package/src/server/storage/types.ts +11 -0
- 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
|
|
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
|
|
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
|
|
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
|
|
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():
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
|