@mcp-ts/sdk 2.5.1 → 2.5.2
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-middleware.d.mts +3 -3
- package/dist/adapters/agui-middleware.d.ts +3 -3
- package/dist/adapters/ai-adapter.d.mts +3 -3
- package/dist/adapters/ai-adapter.d.ts +3 -3
- package/dist/adapters/langchain-adapter.d.mts +3 -3
- package/dist/adapters/langchain-adapter.d.ts +3 -3
- package/dist/adapters/mastra-adapter.d.mts +2 -2
- package/dist/adapters/mastra-adapter.d.ts +2 -2
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/react.d.mts +4 -4
- package/dist/client/react.d.ts +4 -4
- package/dist/client/react.js +1 -1
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +1 -1
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +5 -4
- package/dist/client/vue.d.ts +5 -4
- package/dist/client/vue.js +1 -1
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +1 -1
- package/dist/client/vue.mjs.map +1 -1
- package/dist/{index-C99wE2Zf.d.mts → index-B3GPsPsi.d.mts} +1 -1
- package/dist/{index-BEdyuz1M.d.ts → index-gNdSQTSz.d.ts} +1 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +148 -13
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +148 -13
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-CclWRxTD.d.mts → multi-session-client-CB4oDrQX.d.ts} +36 -10
- package/dist/{multi-session-client-DzvZD6Tt.d.ts → multi-session-client-xj3iQIv6.d.mts} +36 -10
- package/dist/server/index.d.mts +34 -7
- package/dist/server/index.d.ts +34 -7
- package/dist/server/index.js +148 -13
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +148 -13
- 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-CUSSk6lT.d.ts → tool-router-C-Mw1_BQ.d.ts} +1 -1
- package/dist/{tool-router-C9ECn3FX.d.mts → tool-router-CsKVXbQB.d.mts} +1 -1
- package/dist/{types-CbFbPkfQ.d.mts → types-6SmXege4.d.mts} +1 -0
- package/dist/{types-CbFbPkfQ.d.ts → types-6SmXege4.d.ts} +1 -0
- package/package.json +1 -1
- package/src/client/react/use-mcp.ts +1 -1
- package/src/client/vue/use-mcp.ts +2 -1
- package/src/server/handlers/sse-handler.ts +35 -3
- package/src/server/mcp/oauth-client.ts +44 -20
- package/src/server/mcp/tool-policy-gateway.ts +98 -2
- package/src/server/storage/tool-policy.ts +102 -0
- package/src/shared/events.ts +1 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import { A as ToolClientProvider, T as ToolClient } from './types-
|
|
2
|
+
import { A as ToolClientProvider, T as ToolClient } from './types-6SmXege4.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* ToolIndex — Lightweight in-memory search index for MCP tool discovery.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import { A as ToolClientProvider, T as ToolClient } from './types-
|
|
2
|
+
import { A as ToolClientProvider, T as ToolClient } from './types-6SmXege4.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* ToolIndex — Lightweight in-memory search index for MCP tool discovery.
|
package/package.json
CHANGED
|
@@ -371,7 +371,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
371
371
|
}
|
|
372
372
|
|
|
373
373
|
return prev.map((c: McpConnection) =>
|
|
374
|
-
c.sessionId === event.sessionId ? { ...c, tools: event.tools, state: 'READY', updatedAt: new Date() } : c
|
|
374
|
+
c.sessionId === event.sessionId ? { ...c, tools: event.tools, allTools: (event as any).allTools, state: 'READY', updatedAt: new Date() } : c
|
|
375
375
|
);
|
|
376
376
|
}
|
|
377
377
|
|
|
@@ -84,6 +84,7 @@ export interface McpConnection {
|
|
|
84
84
|
transport?: string;
|
|
85
85
|
state: McpConnectionState;
|
|
86
86
|
tools: ToolInfo[];
|
|
87
|
+
allTools?: ToolInfo[];
|
|
87
88
|
authUrl?: string;
|
|
88
89
|
error?: string;
|
|
89
90
|
createdAt?: Date;
|
|
@@ -294,7 +295,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
294
295
|
case 'tools_discovered': {
|
|
295
296
|
const index = connections.value.findIndex((c) => c.sessionId === event.sessionId);
|
|
296
297
|
if (index !== -1) {
|
|
297
|
-
connections.value[index] = { ...connections.value[index], tools: event.tools, state: 'READY', updatedAt: new Date() };
|
|
298
|
+
connections.value[index] = { ...connections.value[index], tools: event.tools, allTools: (event as any).allTools, state: 'READY', updatedAt: new Date() };
|
|
298
299
|
}
|
|
299
300
|
break;
|
|
300
301
|
}
|
|
@@ -508,6 +508,22 @@ export class SSEConnectionManager {
|
|
|
508
508
|
return client;
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
+
/**
|
|
512
|
+
* Fetches all tools from the remote MCP server and emits a `tools_discovered` event.
|
|
513
|
+
*
|
|
514
|
+
* Two lists are always published together:
|
|
515
|
+
* - `tools` — policy-filtered list that agents are allowed to call.
|
|
516
|
+
* - `allTools` — the complete, unfiltered list used by the management UI so
|
|
517
|
+
* that blocked tools still appear as checkboxes in the dialog.
|
|
518
|
+
*
|
|
519
|
+
* `fetchTools()` is called first (populates the in-memory cache), then
|
|
520
|
+
* `gateway.listTools()` re-uses that cache internally — so only one remote
|
|
521
|
+
* network round-trip is made regardless of how many callers follow.
|
|
522
|
+
*
|
|
523
|
+
* @param sessionId - The session whose tools should be discovered.
|
|
524
|
+
* @returns The session record and the policy-filtered tool list.
|
|
525
|
+
* @throws {Error} When the session does not exist in the store.
|
|
526
|
+
*/
|
|
511
527
|
private async listPolicyFilteredTools(sessionId: string): Promise<{ session: Session; result: ListToolsRpcResult }> {
|
|
512
528
|
const session = await sessions.get(this.userId, sessionId);
|
|
513
529
|
if (!session) {
|
|
@@ -515,6 +531,7 @@ export class SSEConnectionManager {
|
|
|
515
531
|
}
|
|
516
532
|
|
|
517
533
|
const client = await this.getOrCreateClient(sessionId);
|
|
534
|
+
const allTools = await client.fetchTools().catch(() => ({ tools: [] }));
|
|
518
535
|
const gateway = createToolPolicyGateway(this.userId, sessionId, client);
|
|
519
536
|
const result = await gateway.listTools();
|
|
520
537
|
|
|
@@ -524,13 +541,17 @@ export class SSEConnectionManager {
|
|
|
524
541
|
serverId: session.serverId ?? 'unknown',
|
|
525
542
|
toolCount: result.tools.length,
|
|
526
543
|
tools: result.tools,
|
|
544
|
+
allTools: allTools.tools,
|
|
527
545
|
timestamp: Date.now(),
|
|
528
546
|
});
|
|
529
547
|
|
|
530
548
|
return { session, result };
|
|
531
549
|
}
|
|
550
|
+
|
|
532
551
|
/**
|
|
533
|
-
*
|
|
552
|
+
* Returns the policy-filtered tool list for a session (agent-facing).
|
|
553
|
+
* Internally re-uses `listPolicyFilteredTools` which also emits a
|
|
554
|
+
* `tools_discovered` SSE event to keep client state up to date.
|
|
534
555
|
*/
|
|
535
556
|
private async listTools(params: SessionParams): Promise<ListToolsRpcResult> {
|
|
536
557
|
const { sessionId } = params;
|
|
@@ -571,7 +592,15 @@ export class SSEConnectionManager {
|
|
|
571
592
|
}
|
|
572
593
|
|
|
573
594
|
/**
|
|
574
|
-
*
|
|
595
|
+
* Persists a new tool access policy for a session and broadcasts the updated
|
|
596
|
+
* filtered tool list to all connected browser clients via a `tools_discovered` event.
|
|
597
|
+
*
|
|
598
|
+
* Both `tools` (policy-filtered) and `allTools` (complete list) are emitted
|
|
599
|
+
* so the management UI can immediately reflect the new checkbox states without
|
|
600
|
+
* an additional round-trip to the server.
|
|
601
|
+
*
|
|
602
|
+
* @param params - Session ID and the new `{ mode, toolIds }` policy to apply.
|
|
603
|
+
* @throws {Error} When the session does not exist or the policy references unknown tool IDs.
|
|
575
604
|
*/
|
|
576
605
|
private async setToolPolicy(params: SetToolPolicyParams): Promise<SetToolPolicyResult> {
|
|
577
606
|
const { sessionId } = params;
|
|
@@ -594,6 +623,7 @@ export class SSEConnectionManager {
|
|
|
594
623
|
serverId: session.serverId ?? 'unknown',
|
|
595
624
|
toolCount: filteredTools.length,
|
|
596
625
|
tools: filteredTools,
|
|
626
|
+
allTools: allTools.tools,
|
|
597
627
|
timestamp: Date.now(),
|
|
598
628
|
});
|
|
599
629
|
|
|
@@ -604,8 +634,10 @@ export class SSEConnectionManager {
|
|
|
604
634
|
toolCount: filteredTools.length,
|
|
605
635
|
};
|
|
606
636
|
}
|
|
637
|
+
|
|
607
638
|
/**
|
|
608
|
-
*
|
|
639
|
+
* Proxies a tool invocation to the remote MCP server.
|
|
640
|
+
* Resolves the client for the given session and delegates to the tool router.
|
|
609
641
|
*/
|
|
610
642
|
private async callTool(params: CallToolParams): Promise<CallToolResult> {
|
|
611
643
|
const { sessionId, toolName, toolArgs } = params;
|
|
@@ -509,13 +509,20 @@ export class MCPClient {
|
|
|
509
509
|
}
|
|
510
510
|
|
|
511
511
|
/**
|
|
512
|
-
* Connects to the MCP server
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
512
|
+
* Connects to the MCP server.
|
|
513
|
+
*
|
|
514
|
+
* Automatically validates and refreshes OAuth tokens if needed.
|
|
515
|
+
* Saves the session to Redis on first successful connection.
|
|
516
|
+
*
|
|
517
|
+
* The in-memory tools cache (`cachedTools`) is cleared at the start of every
|
|
518
|
+
* call so that a reconnection always fetches a fresh tool list from the remote
|
|
519
|
+
* server — even if the same `MCPClient` instance is reused.
|
|
520
|
+
*
|
|
521
|
+
* @throws {UnauthorizedError} When OAuth authorization is required.
|
|
522
|
+
* @throws {Error} When connection fails for other reasons.
|
|
517
523
|
*/
|
|
518
524
|
async connect(): Promise<void> {
|
|
525
|
+
this.cachedTools = null;
|
|
519
526
|
// Close any existing transport so we can negotiate a fresh session.
|
|
520
527
|
// The SDK Client throws if asked to connect() while a transport is
|
|
521
528
|
// already attached; close() detaches it cleanly so the same Client
|
|
@@ -782,18 +789,41 @@ export class MCPClient {
|
|
|
782
789
|
}
|
|
783
790
|
|
|
784
791
|
/**
|
|
785
|
-
*
|
|
786
|
-
*
|
|
792
|
+
* In-memory cache for the remote server's full tools list.
|
|
793
|
+
*
|
|
794
|
+
* Populated on the first `fetchTools()` call and reused for the lifetime of
|
|
795
|
+
* the connection. Cleared to `null` at the start of `connect()` so that a
|
|
796
|
+
* reconnect always retrieves a fresh list, and also in `dispose()` to release
|
|
797
|
+
* the memory when the client is no longer needed.
|
|
798
|
+
*/
|
|
799
|
+
private cachedTools: ListToolsResult | null = null;
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Lists all available tools from the connected MCP server without emitting
|
|
803
|
+
* discovery events. The result is cached in memory for the lifetime of the
|
|
804
|
+
* connection — subsequent callers (e.g. `gateway.listTools()` running right
|
|
805
|
+
* after `fetchTools()`) pay zero extra network cost.
|
|
806
|
+
*
|
|
807
|
+
* Gateways use this to apply a tool-access policy before publishing the
|
|
808
|
+
* filtered list to agents or UI state.
|
|
809
|
+
*
|
|
810
|
+
* @returns The full `ListToolsResult` from the remote server.
|
|
811
|
+
* @throws {Error} When the client is not connected or the request times out.
|
|
787
812
|
*/
|
|
788
813
|
async fetchTools(): Promise<ListToolsResult> {
|
|
814
|
+
if (this.cachedTools) {
|
|
815
|
+
return this.cachedTools;
|
|
816
|
+
}
|
|
789
817
|
const request: ListToolsRequest = {
|
|
790
818
|
method: 'tools/list',
|
|
791
819
|
params: {},
|
|
792
820
|
};
|
|
793
821
|
|
|
794
|
-
|
|
822
|
+
const result = await this.withRetry(() =>
|
|
795
823
|
this.client!.request(request, ListToolsResultSchema)
|
|
796
824
|
);
|
|
825
|
+
this.cachedTools = result;
|
|
826
|
+
return result;
|
|
797
827
|
}
|
|
798
828
|
|
|
799
829
|
/**
|
|
@@ -1119,10 +1149,14 @@ export class MCPClient {
|
|
|
1119
1149
|
}
|
|
1120
1150
|
|
|
1121
1151
|
/**
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
1152
|
+
* Disposes all event emitters and releases cached state.
|
|
1153
|
+
*
|
|
1154
|
+
* Clears `cachedTools` to free memory, and disposes the connection and
|
|
1155
|
+
* observability event emitters so downstream listeners are unsubscribed.
|
|
1156
|
+
* Call this when the client is permanently shut down (not just disconnected).
|
|
1124
1157
|
*/
|
|
1125
1158
|
dispose(): void {
|
|
1159
|
+
this.cachedTools = null;
|
|
1126
1160
|
this._onConnectionEvent.dispose();
|
|
1127
1161
|
this._onObservabilityEvent.dispose();
|
|
1128
1162
|
}
|
|
@@ -1177,14 +1211,4 @@ export class MCPClient {
|
|
|
1177
1211
|
getSessionId(): string {
|
|
1178
1212
|
return this.sessionId;
|
|
1179
1213
|
}
|
|
1180
|
-
|
|
1181
1214
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
@@ -4,12 +4,27 @@ import { sessions } from '../storage/index.js';
|
|
|
4
4
|
import type { Session } from '../storage/types.js';
|
|
5
5
|
import { assertToolAllowed, filterToolsByPolicy } from '../storage/tool-policy.js';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Internal shape expected from the underlying MCP client.
|
|
9
|
+
* Extends `ToolClient` with the raw `fetchTools` / `listTools` / `callTool`
|
|
10
|
+
* methods so the gateway can fetch unfiltered results and apply policy on top.
|
|
11
|
+
*/
|
|
7
12
|
type RawToolClient = ToolClient & {
|
|
8
13
|
fetchTools(): Promise<{ tools: Tool[] }>;
|
|
9
14
|
listTools(): Promise<{ tools: Tool[] }>;
|
|
10
15
|
callTool(name: string, args: Record<string, unknown>): Promise<CallToolResult>;
|
|
11
16
|
};
|
|
12
17
|
|
|
18
|
+
/**
|
|
19
|
+
* A thin policy-enforcement layer that wraps a raw `MCPClient`.
|
|
20
|
+
*
|
|
21
|
+
* Every public method on this class loads the current session's `toolPolicy`
|
|
22
|
+
* from the session store and uses it to filter or guard tool access before
|
|
23
|
+
* delegating to the underlying client.
|
|
24
|
+
*
|
|
25
|
+
* This keeps security logic in one place and prevents agents from calling
|
|
26
|
+
* tools that a user has explicitly blocked via the management UI.
|
|
27
|
+
*/
|
|
13
28
|
export class ToolPolicyGateway implements ToolClient {
|
|
14
29
|
constructor(
|
|
15
30
|
private readonly userId: string,
|
|
@@ -17,26 +32,52 @@ export class ToolPolicyGateway implements ToolClient {
|
|
|
17
32
|
private readonly client: RawToolClient
|
|
18
33
|
) {}
|
|
19
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Returns whether the underlying MCP client transport is currently connected.
|
|
37
|
+
*/
|
|
20
38
|
isConnected(): boolean {
|
|
21
39
|
return this.client.isConnected();
|
|
22
40
|
}
|
|
23
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Returns the server ID from the underlying client, if available.
|
|
44
|
+
*/
|
|
24
45
|
getServerId(): string | undefined {
|
|
25
46
|
return this.client.getServerId?.();
|
|
26
47
|
}
|
|
27
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Returns the human-readable server name from the underlying client, if available.
|
|
51
|
+
*/
|
|
28
52
|
getServerName(): string | undefined {
|
|
29
53
|
return this.client.getServerName?.();
|
|
30
54
|
}
|
|
31
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Returns the server URL from the underlying client, if available.
|
|
58
|
+
*/
|
|
32
59
|
getServerUrl(): string | undefined {
|
|
33
60
|
return this.client.getServerUrl?.();
|
|
34
61
|
}
|
|
35
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Returns the session ID — prefers the value reported by the underlying
|
|
65
|
+
* client, falling back to the one injected at construction time.
|
|
66
|
+
*/
|
|
36
67
|
getSessionId(): string {
|
|
37
68
|
return this.client.getSessionId?.() ?? this.sessionId;
|
|
38
69
|
}
|
|
39
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Returns the **policy-filtered** list of tools that the current session
|
|
73
|
+
* is allowed to call.
|
|
74
|
+
*
|
|
75
|
+
* Internally calls `client.fetchTools()` (which is cache-backed) so no
|
|
76
|
+
* extra network round-trip is incurred when called after `fetchTools()`.
|
|
77
|
+
*
|
|
78
|
+
* @returns A `ListToolsResult` containing only the permitted tools.
|
|
79
|
+
* @throws {Error} When the session does not exist in the store.
|
|
80
|
+
*/
|
|
40
81
|
async listTools(): Promise<ListToolsResult> {
|
|
41
82
|
const session = await this.getSession();
|
|
42
83
|
const result = await this.client.fetchTools();
|
|
@@ -48,24 +89,65 @@ export class ToolPolicyGateway implements ToolClient {
|
|
|
48
89
|
} as ListToolsResult;
|
|
49
90
|
}
|
|
50
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Returns the **complete, unfiltered** list of tools from the remote server,
|
|
94
|
+
* bypassing any tool-access policy.
|
|
95
|
+
*
|
|
96
|
+
* Used by the management UI to show all available tools (including blocked
|
|
97
|
+
* ones) so users can toggle individual tool access in the dialog.
|
|
98
|
+
*
|
|
99
|
+
* @returns The raw `ListToolsResult` from the remote server.
|
|
100
|
+
*/
|
|
51
101
|
async listAllTools(): Promise<ListToolsResult> {
|
|
52
102
|
return await this.client.fetchTools() as ListToolsResult;
|
|
53
103
|
}
|
|
54
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Executes a tool call on the remote server after verifying that the tool
|
|
107
|
+
* is permitted by the current session's policy.
|
|
108
|
+
*
|
|
109
|
+
* @param name - The exact tool name to invoke.
|
|
110
|
+
* @param args - Key/value arguments to pass to the tool.
|
|
111
|
+
* @returns The tool's `CallToolResult`.
|
|
112
|
+
* @throws {Error} When the tool is blocked by the session's policy.
|
|
113
|
+
* @throws {Error} When the session does not exist in the store.
|
|
114
|
+
*/
|
|
55
115
|
async callTool(name: string, args: Record<string, unknown>): Promise<CallToolResult> {
|
|
56
116
|
const session = await this.getSession();
|
|
57
117
|
this.assertAllowed(session, name);
|
|
58
118
|
return await this.client.callTool(name, args);
|
|
59
119
|
}
|
|
60
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Filters a raw tools array down to only those permitted by the session's
|
|
123
|
+
* `toolPolicy`.
|
|
124
|
+
*
|
|
125
|
+
* @param session - The session whose policy should be applied.
|
|
126
|
+
* @param tools - The unfiltered list of tools from the remote server.
|
|
127
|
+
* @returns A subset of `tools` that the policy allows.
|
|
128
|
+
*/
|
|
61
129
|
filterTools(session: Session, tools: Tool[]): Tool[] {
|
|
62
130
|
return filterToolsByPolicy(tools, session.toolPolicy, this.getPolicyServerId(session));
|
|
63
131
|
}
|
|
64
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Throws if `toolName` is blocked by the session's policy.
|
|
135
|
+
* Call this before proxying a `callTool` request to the remote server.
|
|
136
|
+
*
|
|
137
|
+
* @param session - The session whose policy should be enforced.
|
|
138
|
+
* @param toolName - The tool being invoked.
|
|
139
|
+
* @throws {Error} When the tool is not permitted.
|
|
140
|
+
*/
|
|
65
141
|
assertAllowed(session: Session, toolName: string): void {
|
|
66
142
|
assertToolAllowed(session.toolPolicy, toolName, this.getPolicyServerId(session));
|
|
67
143
|
}
|
|
68
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Loads the session from the store and throws if it does not exist.
|
|
147
|
+
*
|
|
148
|
+
* @returns The fully-hydrated `Session` record.
|
|
149
|
+
* @throws {Error} When the session cannot be found.
|
|
150
|
+
*/
|
|
69
151
|
private async getSession(): Promise<Session> {
|
|
70
152
|
const session = await sessions.get(this.userId, this.sessionId);
|
|
71
153
|
if (!session) {
|
|
@@ -74,11 +156,27 @@ export class ToolPolicyGateway implements ToolClient {
|
|
|
74
156
|
return session;
|
|
75
157
|
}
|
|
76
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Resolves the server ID to use when evaluating tool policy.
|
|
161
|
+
* Prefers the value from the live client (most accurate) and falls back
|
|
162
|
+
* to the server ID stored on the session record.
|
|
163
|
+
*/
|
|
77
164
|
private getPolicyServerId(session: Session): string | undefined {
|
|
78
165
|
return this.client.getServerId?.() ?? session.serverId;
|
|
79
166
|
}
|
|
80
167
|
}
|
|
81
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Factory function that creates a `ToolPolicyGateway` for a specific session.
|
|
171
|
+
*
|
|
172
|
+
* Prefer this over constructing `ToolPolicyGateway` directly — it keeps call
|
|
173
|
+
* sites concise and makes it easier to swap the implementation in tests.
|
|
174
|
+
*
|
|
175
|
+
* @param userId - The owner of the session (used for storage lookups).
|
|
176
|
+
* @param sessionId - The session to enforce policy for.
|
|
177
|
+
* @param client - The raw MCP client that the gateway wraps.
|
|
178
|
+
* @returns A fully configured `ToolPolicyGateway` instance.
|
|
179
|
+
*/
|
|
82
180
|
export function createToolPolicyGateway(
|
|
83
181
|
userId: string,
|
|
84
182
|
sessionId: string,
|
|
@@ -86,5 +184,3 @@ export function createToolPolicyGateway(
|
|
|
86
184
|
): ToolPolicyGateway {
|
|
87
185
|
return new ToolPolicyGateway(userId, sessionId, client);
|
|
88
186
|
}
|
|
89
|
-
|
|
90
|
-
|
|
@@ -1,16 +1,42 @@
|
|
|
1
1
|
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
import type { ToolPolicy } from './types.js';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Raw, unvalidated shape of a tool policy as it may arrive from an external
|
|
6
|
+
* source (HTTP request body, database read, etc.). All fields are typed as
|
|
7
|
+
* `unknown` so callers are forced to pass through one of the `normalize*`
|
|
8
|
+
* helpers before using the value as a `ToolPolicy`.
|
|
9
|
+
*/
|
|
4
10
|
export type ToolPolicyInput = {
|
|
5
11
|
mode?: unknown;
|
|
6
12
|
toolIds?: unknown;
|
|
7
13
|
updatedAt?: unknown;
|
|
8
14
|
} | null | undefined;
|
|
9
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Constructs the canonical tool ID string used throughout the policy system.
|
|
18
|
+
*
|
|
19
|
+
* Tool IDs combine the server ID and the tool name with a `::` separator so
|
|
20
|
+
* they are globally unique across all connected MCP servers:
|
|
21
|
+
* ```
|
|
22
|
+
* "mn0v3pfwwbxv::delete_mcp_server"
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @param serverId - The short server identifier (e.g. `"mn0v3pfwwbxv"`).
|
|
26
|
+
* @param toolName - The exact tool name as reported by the remote server.
|
|
27
|
+
* @returns A namespaced tool ID string.
|
|
28
|
+
*/
|
|
10
29
|
export function createToolId(serverId: string, toolName: string): string {
|
|
11
30
|
return `${serverId}::${toolName}`;
|
|
12
31
|
}
|
|
13
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Coerces an arbitrary value into a deduplicated array of trimmed, non-empty
|
|
35
|
+
* strings. Returns an empty array for any non-array or invalid input.
|
|
36
|
+
*
|
|
37
|
+
* @param input - Raw value to normalise (typically from user input or a DB read).
|
|
38
|
+
* @returns A clean `string[]` with duplicates and blank entries removed.
|
|
39
|
+
*/
|
|
14
40
|
function normalizeToolIds(input?: unknown): string[] {
|
|
15
41
|
if (!Array.isArray(input)) return [];
|
|
16
42
|
|
|
@@ -22,6 +48,18 @@ function normalizeToolIds(input?: unknown): string[] {
|
|
|
22
48
|
));
|
|
23
49
|
}
|
|
24
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Converts a raw, potentially untrusted `ToolPolicyInput` into a validated
|
|
53
|
+
* `ToolPolicy` object, or `undefined` if the input is absent / malformed.
|
|
54
|
+
*
|
|
55
|
+
* - Invalid `mode` values fall back to `"all"` (no restrictions).
|
|
56
|
+
* - Invalid `updatedAt` values fall back to `now`.
|
|
57
|
+
* - `"all"` mode always produces an empty `toolIds` array regardless of input.
|
|
58
|
+
*
|
|
59
|
+
* @param input - Raw policy data to normalise (from request body, DB, etc.).
|
|
60
|
+
* @param now - Unix timestamp used as the fallback for `updatedAt`. Defaults to `Date.now()`.
|
|
61
|
+
* @returns A well-formed `ToolPolicy`, or `undefined` if `input` is absent.
|
|
62
|
+
*/
|
|
25
63
|
export function normalizeToolPolicy(input?: ToolPolicyInput, now = Date.now()): ToolPolicy | undefined {
|
|
26
64
|
if (!input || typeof input !== 'object') {
|
|
27
65
|
return undefined;
|
|
@@ -41,10 +79,39 @@ export function normalizeToolPolicy(input?: ToolPolicyInput, now = Date.now()):
|
|
|
41
79
|
return { mode, toolIds: normalizeToolIds(input.toolIds), updatedAt };
|
|
42
80
|
}
|
|
43
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Same as `normalizeToolPolicy` but guaranteed to return a `ToolPolicy`.
|
|
84
|
+
* Falls back to an unrestricted `{ mode: "all" }` policy when the input is
|
|
85
|
+
* absent or invalid.
|
|
86
|
+
*
|
|
87
|
+
* Use this variant when a policy is required (e.g. when persisting an update)
|
|
88
|
+
* rather than when reading an optional policy from the session store.
|
|
89
|
+
*
|
|
90
|
+
* @param input - Raw policy data to normalise.
|
|
91
|
+
* @param now - Unix timestamp used as the fallback for `updatedAt`. Defaults to `Date.now()`.
|
|
92
|
+
* @returns A well-formed `ToolPolicy`, never `undefined`.
|
|
93
|
+
*/
|
|
44
94
|
export function normalizeToolPolicyForUpdate(input: ToolPolicyInput, now = Date.now()): ToolPolicy {
|
|
45
95
|
return normalizeToolPolicy(input, now) ?? { mode: 'all', toolIds: [], updatedAt: now };
|
|
46
96
|
}
|
|
47
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Determines whether a specific tool is permitted under the given policy.
|
|
100
|
+
*
|
|
101
|
+
* | Policy mode | Allowed when… |
|
|
102
|
+
* |--------------|--------------------------------------------------|
|
|
103
|
+
* | `"all"` | Always (no restrictions). |
|
|
104
|
+
* | `"allowlist"`| The tool's ID is present in `policy.toolIds`. |
|
|
105
|
+
* | `"denylist"` | The tool's ID is **not** in `policy.toolIds`. |
|
|
106
|
+
*
|
|
107
|
+
* Returns `false` when the policy requires a `serverId` for ID construction
|
|
108
|
+
* but none is provided (fail-safe / deny by default).
|
|
109
|
+
*
|
|
110
|
+
* @param policy - The active tool policy, or `undefined` for unrestricted access.
|
|
111
|
+
* @param toolName - The tool name to check (as reported by the remote server).
|
|
112
|
+
* @param serverId - The server that owns the tool (required for `allowlist`/`denylist`).
|
|
113
|
+
* @returns `true` if the tool is allowed, `false` otherwise.
|
|
114
|
+
*/
|
|
48
115
|
export function isToolAllowed(policy: ToolPolicy | undefined, toolName: string, serverId?: string): boolean {
|
|
49
116
|
if (!policy || policy.mode === 'all') return true;
|
|
50
117
|
if (!serverId) return false;
|
|
@@ -57,11 +124,33 @@ export function isToolAllowed(policy: ToolPolicy | undefined, toolName: string,
|
|
|
57
124
|
return !policy.toolIds.includes(toolId);
|
|
58
125
|
}
|
|
59
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Asserts that a tool call is permitted under the current session policy.
|
|
129
|
+
* Throws a descriptive error if the tool is blocked — call this inside
|
|
130
|
+
* `ToolPolicyGateway.callTool` before proxying the request to the remote server.
|
|
131
|
+
*
|
|
132
|
+
* @param policy - The active tool policy, or `undefined` for unrestricted access.
|
|
133
|
+
* @param toolName - The tool being invoked.
|
|
134
|
+
* @param serverId - The server that owns the tool.
|
|
135
|
+
* @throws {Error} When the tool is not permitted by the policy.
|
|
136
|
+
*/
|
|
60
137
|
export function assertToolAllowed(policy: ToolPolicy | undefined, toolName: string, serverId?: string): void {
|
|
61
138
|
if (isToolAllowed(policy, toolName, serverId)) return;
|
|
62
139
|
throw new Error(`Tool "${toolName}" is not allowed for this MCP session`);
|
|
63
140
|
}
|
|
64
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Returns the subset of `tools` that are permitted under the given policy.
|
|
144
|
+
*
|
|
145
|
+
* When no policy is set (or mode is `"all"`), the original array is returned
|
|
146
|
+
* unchanged (no copy is made). Otherwise each tool is tested with
|
|
147
|
+
* `isToolAllowed` and only the passing tools are included.
|
|
148
|
+
*
|
|
149
|
+
* @param tools - The full, unfiltered list of tools from the remote server.
|
|
150
|
+
* @param policy - The active tool policy, or `undefined` for unrestricted access.
|
|
151
|
+
* @param serverId - The server that owns the tools (required for `allowlist`/`denylist`).
|
|
152
|
+
* @returns A filtered array containing only the permitted tools.
|
|
153
|
+
*/
|
|
65
154
|
export function filterToolsByPolicy<T extends Pick<Tool, 'name'>>(
|
|
66
155
|
tools: T[],
|
|
67
156
|
policy: ToolPolicy | undefined,
|
|
@@ -71,6 +160,19 @@ export function filterToolsByPolicy<T extends Pick<Tool, 'name'>>(
|
|
|
71
160
|
return tools.filter((tool) => isToolAllowed(policy, tool.name, serverId));
|
|
72
161
|
}
|
|
73
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Validates that every tool ID referenced in a policy actually exists on the
|
|
165
|
+
* connected MCP server. Throws if any ID is unknown.
|
|
166
|
+
*
|
|
167
|
+
* This prevents silent misconfigurations where a user accidentally saves a
|
|
168
|
+
* policy referencing a tool that no longer exists (e.g. after a server update).
|
|
169
|
+
*
|
|
170
|
+
* @param policy - The policy to validate (skipped for `"all"` mode).
|
|
171
|
+
* @param tools - The authoritative list of tools from the remote server.
|
|
172
|
+
* @param serverId - Required to construct tool IDs for comparison.
|
|
173
|
+
* @throws {Error} When `serverId` is missing and the policy is not `"all"`.
|
|
174
|
+
* @throws {Error} When the policy contains tool IDs not present in `tools`.
|
|
175
|
+
*/
|
|
74
176
|
export function validateToolPolicyAgainstTools(
|
|
75
177
|
policy: ToolPolicy,
|
|
76
178
|
tools: Array<Pick<Tool, 'name'>>,
|