@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.
- 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 +9 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +9 -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 +56 -37
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +56 -37
- 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 +9 -0
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +9 -0
- package/dist/client/vue.mjs.map +1 -1
- package/dist/{index-CZk2gu2E.d.ts → index-BEdyuz1M.d.ts} +6 -1
- package/dist/{index-CkM3p0eE.d.mts → index-C99wE2Zf.d.mts} +6 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +380 -82
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +376 -82
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-QOOPdEVp.d.ts → multi-session-client-CclWRxTD.d.mts} +14 -2
- package/dist/{multi-session-client-ChQrBZhF.d.mts → multi-session-client-DzvZD6Tt.d.ts} +14 -2
- package/dist/server/index.d.mts +18 -4
- package/dist/server/index.d.ts +18 -4
- package/dist/server/index.js +371 -82
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +367 -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-CGs3IDOJ.d.mts → tool-router-C9ECn3FX.d.mts} +1 -1
- package/dist/{tool-router-DQ-HrD7W.d.ts → tool-router-CUSSk6lT.d.ts} +1 -1
- package/dist/{types-Bf-7GOLW.d.mts → types-CbFbPkfQ.d.mts} +38 -3
- package/dist/{types-Bf-7GOLW.d.ts → types-CbFbPkfQ.d.ts} +38 -3
- package/migrations/neon/20260513010000_install_mcp_sessions.sql +1 -0
- package/migrations/supabase/20260330195700_install_mcp_sessions.sql +1 -0
- package/package.json +2 -2
- 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 +21 -0
- package/src/client/react/oauth-popup.tsx +17 -24
- package/src/client/react/use-mcp.ts +69 -15
- package/src/server/handlers/sse-handler.ts +202 -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 +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
|
|
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
|
|
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
|
|
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
|
|
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():
|
|
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
|
+
|