@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
|
@@ -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
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import type { ToolPolicy } from './types.js';
|
|
3
|
+
|
|
4
|
+
export type ToolPolicyInput = {
|
|
5
|
+
mode?: unknown;
|
|
6
|
+
toolIds?: unknown;
|
|
7
|
+
updatedAt?: unknown;
|
|
8
|
+
} | null | undefined;
|
|
9
|
+
|
|
10
|
+
export function createToolId(serverId: string, toolName: string): string {
|
|
11
|
+
return `${serverId}::${toolName}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeToolIds(input?: unknown): string[] {
|
|
15
|
+
if (!Array.isArray(input)) return [];
|
|
16
|
+
|
|
17
|
+
return Array.from(new Set(
|
|
18
|
+
input
|
|
19
|
+
.filter((id): id is string => typeof id === 'string')
|
|
20
|
+
.map((id) => id.trim())
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function normalizeToolPolicy(input?: ToolPolicyInput, now = Date.now()): ToolPolicy | undefined {
|
|
26
|
+
if (!input || typeof input !== 'object') {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const mode = input.mode === 'allowlist' || input.mode === 'denylist'
|
|
31
|
+
? input.mode
|
|
32
|
+
: 'all';
|
|
33
|
+
const updatedAt = typeof input.updatedAt === 'number' && Number.isFinite(input.updatedAt)
|
|
34
|
+
? input.updatedAt
|
|
35
|
+
: now;
|
|
36
|
+
|
|
37
|
+
if (mode === 'all') {
|
|
38
|
+
return { mode: 'all', toolIds: [], updatedAt };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { mode, toolIds: normalizeToolIds(input.toolIds), updatedAt };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function normalizeToolPolicyForUpdate(input: ToolPolicyInput, now = Date.now()): ToolPolicy {
|
|
45
|
+
return normalizeToolPolicy(input, now) ?? { mode: 'all', toolIds: [], updatedAt: now };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isToolAllowed(policy: ToolPolicy | undefined, toolName: string, serverId?: string): boolean {
|
|
49
|
+
if (!policy || policy.mode === 'all') return true;
|
|
50
|
+
if (!serverId) return false;
|
|
51
|
+
|
|
52
|
+
const toolId = createToolId(serverId, toolName);
|
|
53
|
+
if (policy.mode === 'allowlist') {
|
|
54
|
+
return policy.toolIds.includes(toolId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return !policy.toolIds.includes(toolId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function assertToolAllowed(policy: ToolPolicy | undefined, toolName: string, serverId?: string): void {
|
|
61
|
+
if (isToolAllowed(policy, toolName, serverId)) return;
|
|
62
|
+
throw new Error(`Tool "${toolName}" is not allowed for this MCP session`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function filterToolsByPolicy<T extends Pick<Tool, 'name'>>(
|
|
66
|
+
tools: T[],
|
|
67
|
+
policy: ToolPolicy | undefined,
|
|
68
|
+
serverId?: string
|
|
69
|
+
): T[] {
|
|
70
|
+
if (!policy || policy.mode === 'all') return tools;
|
|
71
|
+
return tools.filter((tool) => isToolAllowed(policy, tool.name, serverId));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function validateToolPolicyAgainstTools(
|
|
75
|
+
policy: ToolPolicy,
|
|
76
|
+
tools: Array<Pick<Tool, 'name'>>,
|
|
77
|
+
serverId?: string
|
|
78
|
+
): void {
|
|
79
|
+
if (policy.mode === 'all') return;
|
|
80
|
+
if (!serverId) {
|
|
81
|
+
throw new Error('Cannot validate MCP tool policy without a serverId');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const availableIds = new Set(tools.map((tool) => createToolId(serverId, tool.name)));
|
|
85
|
+
const unknownIds = policy.toolIds.filter((id) => !availableIds.has(id));
|
|
86
|
+
if (unknownIds.length > 0) {
|
|
87
|
+
throw new Error(`Unknown tool id(s) for this MCP session: ${unknownIds.join(', ')}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -14,6 +14,14 @@ export interface OAuthState {
|
|
|
14
14
|
|
|
15
15
|
export type SessionStatus = 'pending' | 'active';
|
|
16
16
|
|
|
17
|
+
export type ToolPolicyMode = 'all' | 'allowlist' | 'denylist';
|
|
18
|
+
|
|
19
|
+
export interface ToolPolicy {
|
|
20
|
+
mode: ToolPolicyMode;
|
|
21
|
+
toolIds: string[];
|
|
22
|
+
updatedAt: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
export interface Session {
|
|
18
26
|
sessionId: string;
|
|
19
27
|
serverId?: string; // Database server ID for mapping
|
|
@@ -37,6 +45,7 @@ export interface Session {
|
|
|
37
45
|
* - active: restorable session after successful connection/auth completion
|
|
38
46
|
*/
|
|
39
47
|
status?: SessionStatus;
|
|
48
|
+
toolPolicy?: ToolPolicy;
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
export interface SessionCredentials {
|
|
@@ -158,3 +167,5 @@ export interface SessionStore {
|
|
|
158
167
|
*/
|
|
159
168
|
disconnect(): Promise<void>;
|
|
160
169
|
}
|
|
170
|
+
|
|
171
|
+
|
package/src/shared/types.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface ToolClient {
|
|
|
21
21
|
callTool(name: string, args: Record<string, unknown>): Promise<any>;
|
|
22
22
|
getServerId?(): string | undefined;
|
|
23
23
|
getServerName?(): string | undefined;
|
|
24
|
+
getServerUrl?(): string | undefined;
|
|
24
25
|
getSessionId?(): string;
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -169,6 +170,13 @@ export type ToolInfo = {
|
|
|
169
170
|
// Transport type
|
|
170
171
|
export type TransportType = 'sse' | 'streamable-http';
|
|
171
172
|
export type SessionStatus = 'pending' | 'active';
|
|
173
|
+
export type ToolPolicyMode = 'all' | 'allowlist' | 'denylist';
|
|
174
|
+
|
|
175
|
+
export interface ToolPolicy {
|
|
176
|
+
mode: ToolPolicyMode;
|
|
177
|
+
toolIds: string[];
|
|
178
|
+
updatedAt: number;
|
|
179
|
+
}
|
|
172
180
|
|
|
173
181
|
// SSE/RPC types
|
|
174
182
|
export type McpRpcMethod =
|
|
@@ -182,7 +190,9 @@ export type McpRpcMethod =
|
|
|
182
190
|
| 'listPrompts'
|
|
183
191
|
| 'getPrompt'
|
|
184
192
|
| 'listResources'
|
|
185
|
-
| 'readResource'
|
|
193
|
+
| 'readResource'
|
|
194
|
+
| 'setToolPolicy'
|
|
195
|
+
| 'getToolPolicy';
|
|
186
196
|
|
|
187
197
|
export interface McpRpcRequest {
|
|
188
198
|
id: string;
|
|
@@ -239,6 +249,18 @@ export interface FinishAuthParams {
|
|
|
239
249
|
code: string;
|
|
240
250
|
}
|
|
241
251
|
|
|
252
|
+
export interface SetToolPolicyParams {
|
|
253
|
+
sessionId: string;
|
|
254
|
+
toolPolicy: {
|
|
255
|
+
mode: ToolPolicyMode;
|
|
256
|
+
toolIds?: string[];
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface GetToolPolicyParams {
|
|
261
|
+
sessionId: string;
|
|
262
|
+
}
|
|
263
|
+
|
|
242
264
|
export type McpRpcParams =
|
|
243
265
|
| ConnectParams
|
|
244
266
|
| DisconnectParams
|
|
@@ -247,6 +269,8 @@ export type McpRpcParams =
|
|
|
247
269
|
| GetPromptParams
|
|
248
270
|
| ReadResourceParams
|
|
249
271
|
| FinishAuthParams
|
|
272
|
+
| SetToolPolicyParams
|
|
273
|
+
| GetToolPolicyParams
|
|
250
274
|
| undefined;
|
|
251
275
|
|
|
252
276
|
// RPC Result Types
|
|
@@ -263,6 +287,7 @@ export interface SessionInfo {
|
|
|
263
287
|
* `pending` means auth is in progress and should be resumed explicitly by user action.
|
|
264
288
|
*/
|
|
265
289
|
status: SessionStatus;
|
|
290
|
+
toolPolicy?: ToolPolicy;
|
|
266
291
|
}
|
|
267
292
|
|
|
268
293
|
export interface SessionListResult {
|
|
@@ -292,6 +317,25 @@ export interface ListToolsRpcResult {
|
|
|
292
317
|
tools: Tool[];
|
|
293
318
|
}
|
|
294
319
|
|
|
320
|
+
export interface SetToolPolicyResult {
|
|
321
|
+
success: boolean;
|
|
322
|
+
toolPolicy: ToolPolicy;
|
|
323
|
+
tools: Tool[];
|
|
324
|
+
toolCount: number;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export type ToolAccessInfo = Tool & {
|
|
328
|
+
toolId: string;
|
|
329
|
+
allowed: boolean;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
export interface GetToolPolicyResult {
|
|
333
|
+
toolPolicy: ToolPolicy;
|
|
334
|
+
tools: ToolAccessInfo[];
|
|
335
|
+
toolCount: number;
|
|
336
|
+
allowedToolCount: number;
|
|
337
|
+
}
|
|
338
|
+
|
|
295
339
|
export interface ListPromptsResult {
|
|
296
340
|
prompts: Array<{
|
|
297
341
|
name: string;
|
|
@@ -314,3 +358,6 @@ export interface ListResourcesResult {
|
|
|
314
358
|
}
|
|
315
359
|
|
|
316
360
|
export type { CallToolResult };
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
|
package/src/shared/utils.ts
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
import { customAlphabet } from 'nanoid';
|
|
1
|
+
import { customAlphabet, nanoid } from 'nanoid';
|
|
2
2
|
|
|
3
3
|
const OAUTH_STATE_SEPARATOR = '.';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
1
|
|
9
|
-
);
|
|
10
|
-
|
|
11
|
-
/** remaining chars: alphanumeric */
|
|
12
|
-
const rest = customAlphabet(
|
|
13
|
-
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
|
|
14
|
-
11
|
|
5
|
+
const serverIdAlphabet = customAlphabet(
|
|
6
|
+
'abcdefghijklmnopqrstuvwxyz0123456789',
|
|
7
|
+
12
|
|
15
8
|
);
|
|
16
9
|
|
|
17
10
|
/**
|
|
@@ -32,11 +25,20 @@ export function sanitizeServerLabel(name: string): string {
|
|
|
32
25
|
}
|
|
33
26
|
|
|
34
27
|
/**
|
|
35
|
-
* Generates a
|
|
36
|
-
*
|
|
28
|
+
* Generates a session ID with a human-readable prefix for easy identification.
|
|
29
|
+
* Format: sess_<21-char nanoid> (26 chars total).
|
|
30
|
+
* Not used in tool names, so length is not constrained.
|
|
37
31
|
*/
|
|
38
32
|
export function generateSessionId(): string {
|
|
39
|
-
return
|
|
33
|
+
return 'sess_' + nanoid(21);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generates a short server ID suitable for use in tool names.
|
|
38
|
+
* 12-char alphanumeric string that keeps tool_<serverId>_<toolName> under 64 chars.
|
|
39
|
+
*/
|
|
40
|
+
export function generateServerId(): string {
|
|
41
|
+
return serverIdAlphabet();
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export interface ParsedOAuthState {
|