@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.
- 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 +36 -6
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +36 -6
- 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 +374 -130
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +370 -130
- 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 +368 -130
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +364 -130
- 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 +2 -6
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +2 -6
- 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 +117 -10
- package/src/server/mcp/multi-session-client.ts +7 -3
- package/src/server/mcp/oauth-client.ts +78 -55
- package/src/server/mcp/tool-policy-gateway.ts +90 -0
- package/src/server/storage/index.ts +1 -1
- 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
- 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
|
-
:
|
|
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
|
|
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
|
+
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|