@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.
Files changed (89) hide show
  1. package/dist/adapters/agui-adapter.d.mts +3 -3
  2. package/dist/adapters/agui-adapter.d.ts +3 -3
  3. package/dist/adapters/agui-adapter.js.map +1 -1
  4. package/dist/adapters/agui-adapter.mjs.map +1 -1
  5. package/dist/adapters/agui-middleware.d.mts +3 -3
  6. package/dist/adapters/agui-middleware.d.ts +3 -3
  7. package/dist/adapters/agui-middleware.js.map +1 -1
  8. package/dist/adapters/agui-middleware.mjs.map +1 -1
  9. package/dist/adapters/ai-adapter.d.mts +3 -3
  10. package/dist/adapters/ai-adapter.d.ts +3 -3
  11. package/dist/adapters/ai-adapter.js.map +1 -1
  12. package/dist/adapters/ai-adapter.mjs.map +1 -1
  13. package/dist/adapters/langchain-adapter.d.mts +3 -3
  14. package/dist/adapters/langchain-adapter.d.ts +3 -3
  15. package/dist/adapters/langchain-adapter.js +2 -2
  16. package/dist/adapters/langchain-adapter.js.map +1 -1
  17. package/dist/adapters/langchain-adapter.mjs +2 -2
  18. package/dist/adapters/langchain-adapter.mjs.map +1 -1
  19. package/dist/adapters/mastra-adapter.d.mts +2 -2
  20. package/dist/adapters/mastra-adapter.d.ts +2 -2
  21. package/dist/adapters/mastra-adapter.js +2 -2
  22. package/dist/adapters/mastra-adapter.js.map +1 -1
  23. package/dist/adapters/mastra-adapter.mjs +2 -2
  24. package/dist/adapters/mastra-adapter.mjs.map +1 -1
  25. package/dist/client/index.d.mts +2 -2
  26. package/dist/client/index.d.ts +2 -2
  27. package/dist/client/index.js +6 -0
  28. package/dist/client/index.js.map +1 -1
  29. package/dist/client/index.mjs +6 -0
  30. package/dist/client/index.mjs.map +1 -1
  31. package/dist/client/react.d.mts +15 -4
  32. package/dist/client/react.d.ts +15 -4
  33. package/dist/client/react.js +36 -6
  34. package/dist/client/react.js.map +1 -1
  35. package/dist/client/react.mjs +36 -6
  36. package/dist/client/react.mjs.map +1 -1
  37. package/dist/client/vue.d.mts +4 -4
  38. package/dist/client/vue.d.ts +4 -4
  39. package/dist/client/vue.js +6 -0
  40. package/dist/client/vue.js.map +1 -1
  41. package/dist/client/vue.mjs +6 -0
  42. package/dist/client/vue.mjs.map +1 -1
  43. package/dist/{index-CkM3p0eE.d.mts → index-CTURVnom.d.mts} +5 -1
  44. package/dist/{index-CZk2gu2E.d.ts → index-sVcqrhf7.d.ts} +5 -1
  45. package/dist/index.d.mts +4 -4
  46. package/dist/index.d.ts +4 -4
  47. package/dist/index.js +374 -130
  48. package/dist/index.js.map +1 -1
  49. package/dist/index.mjs +370 -130
  50. package/dist/index.mjs.map +1 -1
  51. package/dist/{multi-session-client-ChQrBZhF.d.mts → multi-session-client-CSPSHHla.d.ts} +14 -2
  52. package/dist/{multi-session-client-QOOPdEVp.d.ts → multi-session-client-DMZGVABI.d.mts} +14 -2
  53. package/dist/server/index.d.mts +13 -4
  54. package/dist/server/index.d.ts +13 -4
  55. package/dist/server/index.js +368 -130
  56. package/dist/server/index.js.map +1 -1
  57. package/dist/server/index.mjs +364 -130
  58. package/dist/server/index.mjs.map +1 -1
  59. package/dist/shared/index.d.mts +4 -4
  60. package/dist/shared/index.d.ts +4 -4
  61. package/dist/shared/index.js +2 -6
  62. package/dist/shared/index.js.map +1 -1
  63. package/dist/shared/index.mjs +2 -6
  64. package/dist/shared/index.mjs.map +1 -1
  65. package/dist/{tool-router-DQ-HrD7W.d.ts → tool-router-CS9l0w4a.d.ts} +1 -1
  66. package/dist/{tool-router-CGs3IDOJ.d.mts → tool-router-CVLBaCwH.d.mts} +1 -1
  67. package/dist/{types-Bf-7GOLW.d.mts → types-DK_NGWd4.d.mts} +37 -3
  68. package/dist/{types-Bf-7GOLW.d.ts → types-DK_NGWd4.d.ts} +37 -3
  69. package/migrations/neon/20260513010000_install_mcp_sessions.sql +1 -0
  70. package/migrations/supabase/20260330195700_install_mcp_sessions.sql +1 -0
  71. package/package.json +1 -1
  72. package/src/adapters/agui-adapter.ts +6 -2
  73. package/src/adapters/ai-adapter.ts +3 -1
  74. package/src/adapters/langchain-adapter.ts +5 -3
  75. package/src/adapters/mastra-adapter.ts +5 -3
  76. package/src/client/core/sse-client.ts +17 -0
  77. package/src/client/react/use-mcp.ts +63 -0
  78. package/src/server/handlers/sse-handler.ts +117 -10
  79. package/src/server/mcp/multi-session-client.ts +7 -3
  80. package/src/server/mcp/oauth-client.ts +78 -55
  81. package/src/server/mcp/tool-policy-gateway.ts +90 -0
  82. package/src/server/storage/index.ts +1 -1
  83. package/src/server/storage/neon-backend.ts +65 -60
  84. package/src/server/storage/session-lifecycle.ts +5 -0
  85. package/src/server/storage/supabase-backend.ts +27 -15
  86. package/src/server/storage/tool-policy.ts +89 -0
  87. package/src/server/storage/types.ts +11 -0
  88. package/src/shared/types.ts +48 -1
  89. package/src/shared/utils.ts +16 -14
package/dist/index.mjs CHANGED
@@ -113,13 +113,9 @@ var init_redis = __esm({
113
113
  }
114
114
  });
115
115
  var OAUTH_STATE_SEPARATOR = ".";
116
- var firstChar = customAlphabet(
117
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
118
- 1
119
- );
120
- var rest = customAlphabet(
121
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
122
- 11
116
+ var serverIdAlphabet = customAlphabet(
117
+ "abcdefghijklmnopqrstuvwxyz0123456789",
118
+ 12
123
119
  );
124
120
  function sanitizeServerLabel(name) {
125
121
  let sanitized = name.replace(/[^a-zA-Z0-9-_]/g, "_").replace(/_{2,}/g, "_").toLowerCase();
@@ -129,7 +125,10 @@ function sanitizeServerLabel(name) {
129
125
  return sanitized;
130
126
  }
131
127
  function generateSessionId() {
132
- return firstChar() + rest();
128
+ return "sess_" + nanoid(21);
129
+ }
130
+ function generateServerId() {
131
+ return serverIdAlphabet();
133
132
  }
134
133
  function formatOAuthState(nonce, sessionId) {
135
134
  return `${nonce}${OAUTH_STATE_SEPARATOR}${sessionId}`;
@@ -163,6 +162,59 @@ var SOFTWARE_VERSION = "2.3.4";
163
162
  var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
164
163
  var MCP_CLIENT_VERSION = "2.0";
165
164
 
165
+ // src/server/storage/tool-policy.ts
166
+ function createToolId(serverId, toolName) {
167
+ return `${serverId}::${toolName}`;
168
+ }
169
+ function normalizeToolIds(input) {
170
+ if (!Array.isArray(input)) return [];
171
+ return Array.from(new Set(
172
+ input.filter((id) => typeof id === "string").map((id) => id.trim()).filter(Boolean)
173
+ ));
174
+ }
175
+ function normalizeToolPolicy(input, now = Date.now()) {
176
+ if (!input || typeof input !== "object") {
177
+ return void 0;
178
+ }
179
+ const mode = input.mode === "allowlist" || input.mode === "denylist" ? input.mode : "all";
180
+ const updatedAt = typeof input.updatedAt === "number" && Number.isFinite(input.updatedAt) ? input.updatedAt : now;
181
+ if (mode === "all") {
182
+ return { mode: "all", toolIds: [], updatedAt };
183
+ }
184
+ return { mode, toolIds: normalizeToolIds(input.toolIds), updatedAt };
185
+ }
186
+ function normalizeToolPolicyForUpdate(input, now = Date.now()) {
187
+ return normalizeToolPolicy(input, now) ?? { mode: "all", toolIds: [], updatedAt: now };
188
+ }
189
+ function isToolAllowed(policy, toolName, serverId) {
190
+ if (!policy || policy.mode === "all") return true;
191
+ if (!serverId) return false;
192
+ const toolId = createToolId(serverId, toolName);
193
+ if (policy.mode === "allowlist") {
194
+ return policy.toolIds.includes(toolId);
195
+ }
196
+ return !policy.toolIds.includes(toolId);
197
+ }
198
+ function assertToolAllowed(policy, toolName, serverId) {
199
+ if (isToolAllowed(policy, toolName, serverId)) return;
200
+ throw new Error(`Tool "${toolName}" is not allowed for this MCP session`);
201
+ }
202
+ function filterToolsByPolicy(tools, policy, serverId) {
203
+ if (!policy || policy.mode === "all") return tools;
204
+ return tools.filter((tool) => isToolAllowed(policy, tool.name, serverId));
205
+ }
206
+ function validateToolPolicyAgainstTools(policy, tools, serverId) {
207
+ if (policy.mode === "all") return;
208
+ if (!serverId) {
209
+ throw new Error("Cannot validate MCP tool policy without a serverId");
210
+ }
211
+ const availableIds = new Set(tools.map((tool) => createToolId(serverId, tool.name)));
212
+ const unknownIds = policy.toolIds.filter((id) => !availableIds.has(id));
213
+ if (unknownIds.length > 0) {
214
+ throw new Error(`Unknown tool id(s) for this MCP session: ${unknownIds.join(", ")}`);
215
+ }
216
+ }
217
+
166
218
  // src/server/storage/session-lifecycle.ts
167
219
  function resolveSessionExpiresAt(status = "pending", referenceTime = Date.now()) {
168
220
  return status === "active" ? null : referenceTime + STATE_EXPIRATION_MS;
@@ -179,7 +231,8 @@ function normalizeNewSession(session, now = Date.now()) {
179
231
  status,
180
232
  createdAt,
181
233
  updatedAt,
182
- expiresAt: resolveSessionExpiresAt(status, status === "active" ? updatedAt : createdAt)
234
+ expiresAt: resolveSessionExpiresAt(status, status === "active" ? updatedAt : createdAt),
235
+ toolPolicy: normalizeToolPolicy(session.toolPolicy, updatedAt)
183
236
  };
184
237
  }
185
238
  function mergeSessionUpdate(current, data, now = Date.now()) {
@@ -193,7 +246,8 @@ function mergeSessionUpdate(current, data, now = Date.now()) {
193
246
  return {
194
247
  ...updated,
195
248
  status,
196
- expiresAt: resolveSessionExpiresAt(status, updatedAt)
249
+ expiresAt: resolveSessionExpiresAt(status, updatedAt),
250
+ toolPolicy: normalizeToolPolicy(updated.toolPolicy, updatedAt)
197
251
  };
198
252
  }
199
253
  function normalizeStoredSession(session) {
@@ -206,7 +260,8 @@ function normalizeStoredSession(session) {
206
260
  status,
207
261
  createdAt,
208
262
  updatedAt,
209
- expiresAt
263
+ expiresAt,
264
+ toolPolicy: normalizeToolPolicy(session.toolPolicy, updatedAt)
210
265
  };
211
266
  }
212
267
  function isSessionExpired(session, now = Date.now()) {
@@ -1064,7 +1119,8 @@ var SupabaseStorageBackend = class {
1064
1119
  userId: row.user_id,
1065
1120
  headers: decryptObject(row.headers),
1066
1121
  authUrl: row.auth_url,
1067
- status: row.status ?? "pending"
1122
+ status: row.status ?? "pending",
1123
+ toolPolicy: normalizeToolPolicy(row.tool_policy)
1068
1124
  };
1069
1125
  }
1070
1126
  mapRowToCredentials(row, userId, sessionId) {
@@ -1088,7 +1144,7 @@ var SupabaseStorageBackend = class {
1088
1144
  const createdAt = new Date(session.createdAt || Date.now()).toISOString();
1089
1145
  const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
1090
1146
  const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
1091
- const { error } = await this.supabase.from("mcp_sessions").insert({
1147
+ const insertData = {
1092
1148
  session_id: sessionId,
1093
1149
  user_id: userId,
1094
1150
  server_id: session.serverId,
@@ -1102,7 +1158,12 @@ var SupabaseStorageBackend = class {
1102
1158
  auth_url: session.authUrl ?? null,
1103
1159
  status,
1104
1160
  expires_at: expiresAt === null ? null : new Date(expiresAt).toISOString()
1105
- });
1161
+ };
1162
+ const toolPolicy = normalizeToolPolicy(session.toolPolicy);
1163
+ if (toolPolicy) {
1164
+ insertData.tool_policy = toolPolicy;
1165
+ }
1166
+ const { error } = await this.supabase.from("mcp_sessions").insert(insertData);
1106
1167
  if (error) {
1107
1168
  if (error.code === "23505") {
1108
1169
  throw new Error(`Session ${sessionId} already exists`);
@@ -1127,6 +1188,7 @@ var SupabaseStorageBackend = class {
1127
1188
  }
1128
1189
  if ("headers" in data) updateData.headers = encryptObject(data.headers);
1129
1190
  if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
1191
+ if ("toolPolicy" in data) updateData.tool_policy = normalizeToolPolicy(data.toolPolicy);
1130
1192
  const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
1131
1193
  let updatedRows = null;
1132
1194
  if (shouldUpdateSession) {
@@ -1299,7 +1361,8 @@ var NeonStorageBackend = class {
1299
1361
  userId: row.user_id,
1300
1362
  headers: decryptObject(row.headers),
1301
1363
  authUrl: row.auth_url ?? void 0,
1302
- status: row.status ?? "pending"
1364
+ status: row.status ?? "pending",
1365
+ toolPolicy: normalizeToolPolicy(row.tool_policy)
1303
1366
  };
1304
1367
  }
1305
1368
  mapRowToCredentials(row, userId, sessionId) {
@@ -1322,42 +1385,48 @@ var NeonStorageBackend = class {
1322
1385
  const status = session.status ?? "pending";
1323
1386
  const createdAt = new Date(session.createdAt || Date.now()).toISOString();
1324
1387
  const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
1325
- const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
1388
+ const createdAtMs = new Date(createdAt).getTime();
1389
+ const expiresAt = resolveSessionExpiresAt(status, createdAtMs);
1390
+ const toolPolicy = normalizeToolPolicy(session.toolPolicy, createdAtMs);
1391
+ const columns = [
1392
+ "session_id",
1393
+ "user_id",
1394
+ "server_id",
1395
+ "server_name",
1396
+ "server_url",
1397
+ "transport_type",
1398
+ "callback_url",
1399
+ "created_at",
1400
+ "updated_at",
1401
+ "headers",
1402
+ "auth_url",
1403
+ "status",
1404
+ "expires_at"
1405
+ ];
1406
+ const values = [
1407
+ sessionId,
1408
+ userId,
1409
+ session.serverId,
1410
+ session.serverName,
1411
+ session.serverUrl,
1412
+ session.transportType,
1413
+ session.callbackUrl,
1414
+ createdAt,
1415
+ updatedAt,
1416
+ encryptObject(session.headers),
1417
+ session.authUrl ?? null,
1418
+ status,
1419
+ expiresAt === null ? null : new Date(expiresAt).toISOString()
1420
+ ];
1421
+ if (toolPolicy) {
1422
+ columns.push("tool_policy");
1423
+ values.push(toolPolicy);
1424
+ }
1425
+ const placeholders = values.map((_, i) => `$${i + 1}`);
1326
1426
  try {
1327
1427
  await this.sql.query(
1328
- `INSERT INTO ${this.tableName} (
1329
- session_id,
1330
- user_id,
1331
- server_id,
1332
- server_name,
1333
- server_url,
1334
- transport_type,
1335
- callback_url,
1336
- created_at,
1337
- updated_at,
1338
- headers,
1339
- auth_url,
1340
- status,
1341
- expires_at
1342
- ) VALUES (
1343
- $1, $2, $3, $4, $5, $6, $7, $8,
1344
- $9, $10, $11, $12, $13
1345
- )`,
1346
- [
1347
- sessionId,
1348
- userId,
1349
- session.serverId,
1350
- session.serverName,
1351
- session.serverUrl,
1352
- session.transportType,
1353
- session.callbackUrl,
1354
- createdAt,
1355
- updatedAt,
1356
- encryptObject(session.headers),
1357
- session.authUrl ?? null,
1358
- status,
1359
- expiresAt === null ? null : new Date(expiresAt).toISOString()
1360
- ]
1428
+ `INSERT INTO ${this.tableName} (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`,
1429
+ values
1361
1430
  );
1362
1431
  } catch (error) {
1363
1432
  if (error.code === "23505") {
@@ -1374,36 +1443,36 @@ var NeonStorageBackend = class {
1374
1443
  const updatedSession = { ...currentSession, ...data };
1375
1444
  const status = updatedSession.status ?? "pending";
1376
1445
  const expiresAt = resolveSessionExpiresAt(status);
1377
- const shouldUpdateSession = "serverId" in data || "serverName" in data || "serverUrl" in data || "transportType" in data || "callbackUrl" in data || "status" in data || "headers" in data || "authUrl" in data;
1446
+ const shouldUpdateSession = "serverId" in data || "serverName" in data || "serverUrl" in data || "transportType" in data || "callbackUrl" in data || "status" in data || "headers" in data || "authUrl" in data || "toolPolicy" in data;
1378
1447
  if (shouldUpdateSession) {
1448
+ const setClauses = [];
1449
+ const values = [];
1450
+ let paramIndex = 1;
1451
+ const addSet = (column, value) => {
1452
+ setClauses.push(`${column} = $${paramIndex++}`);
1453
+ values.push(value);
1454
+ };
1455
+ addSet("server_id", updatedSession.serverId);
1456
+ addSet("server_name", updatedSession.serverName);
1457
+ addSet("server_url", updatedSession.serverUrl);
1458
+ addSet("transport_type", updatedSession.transportType);
1459
+ addSet("callback_url", updatedSession.callbackUrl);
1460
+ addSet("status", status);
1461
+ addSet("headers", encryptObject(updatedSession.headers));
1462
+ addSet("auth_url", updatedSession.authUrl ?? null);
1463
+ addSet("expires_at", expiresAt === null ? null : new Date(expiresAt).toISOString());
1464
+ if ("toolPolicy" in data) {
1465
+ const policyUpdatedAt = updatedSession.updatedAt ?? Date.now();
1466
+ const toolPolicy = normalizeToolPolicy(updatedSession.toolPolicy, policyUpdatedAt) ?? { mode: "all", toolIds: [], updatedAt: policyUpdatedAt };
1467
+ addSet("tool_policy", toolPolicy);
1468
+ }
1469
+ setClauses.push("updated_at = now()");
1379
1470
  const updatedRows = await this.sql.query(
1380
1471
  `UPDATE ${this.tableName}
1381
- SET
1382
- server_id = $1,
1383
- server_name = $2,
1384
- server_url = $3,
1385
- transport_type = $4,
1386
- callback_url = $5,
1387
- status = $6,
1388
- headers = $7,
1389
- auth_url = $8,
1390
- expires_at = $9,
1391
- updated_at = now()
1392
- WHERE user_id = $10 AND session_id = $11
1472
+ SET ${setClauses.join(", ")}
1473
+ WHERE user_id = $${paramIndex++} AND session_id = $${paramIndex++}
1393
1474
  RETURNING id`,
1394
- [
1395
- updatedSession.serverId,
1396
- updatedSession.serverName,
1397
- updatedSession.serverUrl,
1398
- updatedSession.transportType,
1399
- updatedSession.callbackUrl,
1400
- status,
1401
- encryptObject(updatedSession.headers),
1402
- updatedSession.authUrl ?? null,
1403
- expiresAt === null ? null : new Date(expiresAt).toISOString(),
1404
- userId,
1405
- sessionId
1406
- ]
1475
+ [...values, userId, sessionId]
1407
1476
  );
1408
1477
  if (updatedRows.length === 0) {
1409
1478
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
@@ -2386,7 +2455,15 @@ var MCPClient = class {
2386
2455
  if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
2387
2456
  this.createdAt = Date.now();
2388
2457
  const updatedAt = this.createdAt;
2389
- console.log(`[MCPClient] Creating pending session ${this.sessionId} for connection setup`);
2458
+ this._onObservabilityEvent.fire({
2459
+ type: "mcp:client:session_created",
2460
+ level: "info",
2461
+ message: `Creating pending session ${this.sessionId} for connection setup`,
2462
+ sessionId: this.sessionId,
2463
+ serverId: this.serverId,
2464
+ timestamp: Date.now(),
2465
+ id: nanoid()
2466
+ });
2390
2467
  await sessions.create({
2391
2468
  sessionId: this.sessionId,
2392
2469
  userId: this.userId,
@@ -2516,7 +2593,15 @@ var MCPClient = class {
2516
2593
  this.transportType = transportType;
2517
2594
  this.emitStateChange("CONNECTED");
2518
2595
  this.emitProgress("Connected successfully");
2519
- console.log(`[MCPClient] Saving active session ${this.sessionId} (connect success)`);
2596
+ this._onObservabilityEvent.fire({
2597
+ type: "mcp:client:session_saved",
2598
+ level: "info",
2599
+ message: `Saving active session ${this.sessionId} (connect success)`,
2600
+ sessionId: this.sessionId,
2601
+ serverId: this.serverId,
2602
+ timestamp: Date.now(),
2603
+ id: nanoid()
2604
+ });
2520
2605
  await this.saveSession("active");
2521
2606
  } catch (error) {
2522
2607
  if (error instanceof UnauthorizedError$1 || error instanceof Error && error.message.toLowerCase().includes("unauthorized")) {
@@ -2533,7 +2618,15 @@ var MCPClient = class {
2533
2618
  throw new Error(message);
2534
2619
  }
2535
2620
  this.emitStateChange("AUTHENTICATING");
2536
- console.log(`[MCPClient] Saving pending OAuth session ${this.sessionId}`);
2621
+ this._onObservabilityEvent.fire({
2622
+ type: "mcp:client:session_saved",
2623
+ level: "info",
2624
+ message: `Saving pending OAuth session ${this.sessionId}`,
2625
+ sessionId: this.sessionId,
2626
+ serverId: this.serverId,
2627
+ timestamp: Date.now(),
2628
+ id: nanoid()
2629
+ });
2537
2630
  await this.saveSession("pending");
2538
2631
  if (this.serverId) {
2539
2632
  this._onConnectionEvent.fire({
@@ -2608,27 +2701,25 @@ var MCPClient = class {
2608
2701
  this.emitStateChange("AUTHENTICATED");
2609
2702
  authenticatedStateEmitted = true;
2610
2703
  }
2611
- this.emitProgress("Creating authenticated client...");
2612
- this.client = new Client(
2613
- {
2614
- name: MCP_CLIENT_NAME,
2615
- version: MCP_CLIENT_VERSION
2616
- },
2617
- {
2618
- capabilities: {
2619
- extensions: {
2620
- "io.modelcontextprotocol/ui": {
2621
- mimeTypes: ["text/html+mcp"]
2622
- }
2623
- }
2624
- }
2625
- }
2626
- );
2627
2704
  this.emitStateChange("CONNECTING");
2705
+ if (this.client.transport) {
2706
+ try {
2707
+ await this.client.close();
2708
+ } catch {
2709
+ }
2710
+ }
2628
2711
  await this.client.connect(this.transport);
2629
2712
  this.transportType = currentType;
2630
2713
  this.emitStateChange("CONNECTED");
2631
- console.log(`[MCPClient] Saving active session ${this.sessionId} (OAuth complete)`);
2714
+ this._onObservabilityEvent.fire({
2715
+ type: "mcp:client:session_saved",
2716
+ level: "info",
2717
+ message: `Saving active session ${this.sessionId} (OAuth complete)`,
2718
+ sessionId: this.sessionId,
2719
+ serverId: this.serverId,
2720
+ timestamp: Date.now(),
2721
+ id: nanoid()
2722
+ });
2632
2723
  await this.saveSession("active");
2633
2724
  return;
2634
2725
  } catch (error) {
@@ -2663,24 +2754,28 @@ var MCPClient = class {
2663
2754
  throw lastError;
2664
2755
  }
2665
2756
  }
2757
+ /**
2758
+ * Lists all available tools from the connected MCP server without emitting discovery events.
2759
+ * Gateways use this to apply policy before publishing tools to agents or UI state.
2760
+ */
2761
+ async fetchTools() {
2762
+ const request = {
2763
+ method: "tools/list",
2764
+ params: {}
2765
+ };
2766
+ return await this.withRetry(
2767
+ () => this.client.request(request, ListToolsResultSchema)
2768
+ );
2769
+ }
2666
2770
  /**
2667
2771
  * Lists all available tools from the connected MCP server
2668
2772
  * @returns List of tools with their schemas and descriptions
2669
2773
  * @throws {Error} When client is not connected
2670
2774
  */
2671
2775
  async listTools() {
2672
- if (!this.client) {
2673
- throw new Error("Not connected to server");
2674
- }
2675
2776
  this.emitStateChange("DISCOVERING");
2676
2777
  try {
2677
- const request = {
2678
- method: "tools/list",
2679
- params: {}
2680
- };
2681
- const result = await this.withRetry(
2682
- () => this.client.request(request, ListToolsResultSchema)
2683
- );
2778
+ const result = await this.fetchTools();
2684
2779
  if (this.serverId) {
2685
2780
  this._onConnectionEvent.fire({
2686
2781
  type: "tools_discovered",
@@ -2709,9 +2804,6 @@ var MCPClient = class {
2709
2804
  * @throws {Error} When client is not connected
2710
2805
  */
2711
2806
  async callTool(toolName, toolArgs) {
2712
- if (!this.client) {
2713
- throw new Error("Not connected to server");
2714
- }
2715
2807
  const request = {
2716
2808
  method: "tools/call",
2717
2809
  params: {
@@ -2765,9 +2857,6 @@ var MCPClient = class {
2765
2857
  * @throws {Error} When client is not connected
2766
2858
  */
2767
2859
  async listPrompts() {
2768
- if (!this.client) {
2769
- throw new Error("Not connected to server");
2770
- }
2771
2860
  this.emitStateChange("DISCOVERING");
2772
2861
  try {
2773
2862
  const request = {
@@ -2795,9 +2884,6 @@ var MCPClient = class {
2795
2884
  * @throws {Error} When client is not connected
2796
2885
  */
2797
2886
  async getPrompt(name, args) {
2798
- if (!this.client) {
2799
- throw new Error("Not connected to server");
2800
- }
2801
2887
  const request = {
2802
2888
  method: "prompts/get",
2803
2889
  params: {
@@ -2815,9 +2901,6 @@ var MCPClient = class {
2815
2901
  * @throws {Error} When client is not connected
2816
2902
  */
2817
2903
  async listResources() {
2818
- if (!this.client) {
2819
- throw new Error("Not connected to server");
2820
- }
2821
2904
  this.emitStateChange("DISCOVERING");
2822
2905
  try {
2823
2906
  const request = {
@@ -2844,9 +2927,6 @@ var MCPClient = class {
2844
2927
  * @throws {Error} When client is not connected
2845
2928
  */
2846
2929
  async readResource(uri) {
2847
- if (!this.client) {
2848
- throw new Error("Not connected to server");
2849
- }
2850
2930
  const request = {
2851
2931
  method: "resources/read",
2852
2932
  params: {
@@ -2902,7 +2982,16 @@ var MCPClient = class {
2902
2982
  try {
2903
2983
  await this.ensureSession();
2904
2984
  } catch (error) {
2905
- console.warn("[MCPClient] Initialization failed during clearSession:", error);
2985
+ this._onObservabilityEvent.fire({
2986
+ type: "mcp:client:error",
2987
+ level: "warn",
2988
+ message: "Initialization failed during clearSession",
2989
+ sessionId: this.sessionId,
2990
+ serverId: this.serverId,
2991
+ payload: { error: String(error) },
2992
+ timestamp: Date.now(),
2993
+ id: nanoid()
2994
+ });
2906
2995
  }
2907
2996
  if (this.oauthProvider) {
2908
2997
  await this.oauthProvider.invalidateCredentials("all");
@@ -3013,6 +3102,66 @@ var MCPClient = class {
3013
3102
  }
3014
3103
  };
3015
3104
 
3105
+ // src/server/mcp/tool-policy-gateway.ts
3106
+ var ToolPolicyGateway = class {
3107
+ constructor(userId, sessionId, client) {
3108
+ this.userId = userId;
3109
+ this.sessionId = sessionId;
3110
+ this.client = client;
3111
+ }
3112
+ isConnected() {
3113
+ return this.client.isConnected();
3114
+ }
3115
+ getServerId() {
3116
+ return this.client.getServerId?.();
3117
+ }
3118
+ getServerName() {
3119
+ return this.client.getServerName?.();
3120
+ }
3121
+ getServerUrl() {
3122
+ return this.client.getServerUrl?.();
3123
+ }
3124
+ getSessionId() {
3125
+ return this.client.getSessionId?.() ?? this.sessionId;
3126
+ }
3127
+ async listTools() {
3128
+ const session = await this.getSession();
3129
+ const result = await this.client.fetchTools();
3130
+ const tools = this.filterTools(session, result.tools);
3131
+ return {
3132
+ ...result,
3133
+ tools
3134
+ };
3135
+ }
3136
+ async listAllTools() {
3137
+ return await this.client.fetchTools();
3138
+ }
3139
+ async callTool(name, args) {
3140
+ const session = await this.getSession();
3141
+ this.assertAllowed(session, name);
3142
+ return await this.client.callTool(name, args);
3143
+ }
3144
+ filterTools(session, tools) {
3145
+ return filterToolsByPolicy(tools, session.toolPolicy, this.getPolicyServerId(session));
3146
+ }
3147
+ assertAllowed(session, toolName) {
3148
+ assertToolAllowed(session.toolPolicy, toolName, this.getPolicyServerId(session));
3149
+ }
3150
+ async getSession() {
3151
+ const session = await sessions.get(this.userId, this.sessionId);
3152
+ if (!session) {
3153
+ throw new Error("Session not found");
3154
+ }
3155
+ return session;
3156
+ }
3157
+ getPolicyServerId(session) {
3158
+ return this.client.getServerId?.() ?? session.serverId;
3159
+ }
3160
+ };
3161
+ function createToolPolicyGateway(userId, sessionId, client) {
3162
+ return new ToolPolicyGateway(userId, sessionId, client);
3163
+ }
3164
+
3016
3165
  // src/server/mcp/multi-session-client.ts
3017
3166
  var DEFAULT_TIMEOUT_MS = 15e3;
3018
3167
  var DEFAULT_MAX_RETRIES = 2;
@@ -3078,7 +3227,9 @@ var MultiSessionClient = class {
3078
3227
  * or to route a tool call to the right client by `serverId`.
3079
3228
  */
3080
3229
  getClients() {
3081
- return this.clients;
3230
+ return this.clients.map(
3231
+ (client) => createToolPolicyGateway(this.userId, client.getSessionId(), client)
3232
+ );
3082
3233
  }
3083
3234
  /**
3084
3235
  * Removes and disconnects a single session by ID.
@@ -3313,6 +3464,12 @@ var SSEConnectionManager = class {
3313
3464
  case "listTools":
3314
3465
  result = await this.listTools(request.params);
3315
3466
  break;
3467
+ case "setToolPolicy":
3468
+ result = await this.setToolPolicy(request.params);
3469
+ break;
3470
+ case "getToolPolicy":
3471
+ result = await this.getToolPolicy(request.params);
3472
+ break;
3316
3473
  case "callTool":
3317
3474
  result = await this.callTool(request.params);
3318
3475
  break;
@@ -3369,7 +3526,8 @@ var SSEConnectionManager = class {
3369
3526
  transport: s.transportType,
3370
3527
  createdAt: s.createdAt,
3371
3528
  updatedAt: s.updatedAt ?? s.createdAt,
3372
- status: s.status ?? "pending"
3529
+ status: s.status ?? "pending",
3530
+ toolPolicy: s.toolPolicy
3373
3531
  }))
3374
3532
  };
3375
3533
  }
@@ -3379,7 +3537,7 @@ var SSEConnectionManager = class {
3379
3537
  async connect(params) {
3380
3538
  const { serverName, serverUrl, callbackUrl, transportType } = params;
3381
3539
  const headers = normalizeHeaders(params.headers);
3382
- const serverId = params.serverId && params.serverId.length <= 12 ? params.serverId : await sessions.generateSessionId();
3540
+ const serverId = params.serverId && params.serverId.length <= 12 ? params.serverId : generateServerId();
3383
3541
  const existingSessions = await sessions.list(this.userId);
3384
3542
  const duplicate = existingSessions.find(
3385
3543
  (s) => s.serverId === serverId || s.serverUrl === serverUrl
@@ -3417,7 +3575,7 @@ var SSEConnectionManager = class {
3417
3575
  this.sendEvent(event);
3418
3576
  });
3419
3577
  await client.connect();
3420
- await client.listTools();
3578
+ await this.listPolicyFilteredTools(sessionId);
3421
3579
  return {
3422
3580
  sessionId,
3423
3581
  success: true
@@ -3488,22 +3646,98 @@ var SSEConnectionManager = class {
3488
3646
  this.clients.set(sessionId, client);
3489
3647
  return client;
3490
3648
  }
3649
+ async listPolicyFilteredTools(sessionId) {
3650
+ const session = await sessions.get(this.userId, sessionId);
3651
+ if (!session) {
3652
+ throw new Error("Session not found");
3653
+ }
3654
+ const client = await this.getOrCreateClient(sessionId);
3655
+ const gateway = createToolPolicyGateway(this.userId, sessionId, client);
3656
+ const result = await gateway.listTools();
3657
+ this.emitConnectionEvent({
3658
+ type: "tools_discovered",
3659
+ sessionId,
3660
+ serverId: session.serverId ?? "unknown",
3661
+ toolCount: result.tools.length,
3662
+ tools: result.tools,
3663
+ timestamp: Date.now()
3664
+ });
3665
+ return { session, result };
3666
+ }
3491
3667
  /**
3492
3668
  * List tools from a session
3493
3669
  */
3494
3670
  async listTools(params) {
3495
3671
  const { sessionId } = params;
3496
- const client = await this.getOrCreateClient(sessionId);
3497
- const result = await client.listTools();
3672
+ const { result } = await this.listPolicyFilteredTools(sessionId);
3498
3673
  return { tools: result.tools };
3499
3674
  }
3675
+ /**
3676
+ * Get all raw tools with effective access state for management UI.
3677
+ */
3678
+ async getToolPolicy(params) {
3679
+ const { sessionId } = params;
3680
+ const session = await sessions.get(this.userId, sessionId);
3681
+ if (!session) {
3682
+ throw new Error("Session not found");
3683
+ }
3684
+ const client = await this.getOrCreateClient(sessionId);
3685
+ const allTools = await client.fetchTools();
3686
+ const toolPolicy = session.toolPolicy ?? {
3687
+ mode: "all",
3688
+ toolIds: [],
3689
+ updatedAt: session.updatedAt ?? session.createdAt
3690
+ };
3691
+ const serverId = session.serverId ?? "unknown";
3692
+ const tools = allTools.tools.map((tool) => ({
3693
+ ...tool,
3694
+ toolId: createToolId(serverId, tool.name),
3695
+ allowed: isToolAllowed(toolPolicy, tool.name, session.serverId)
3696
+ }));
3697
+ return {
3698
+ toolPolicy,
3699
+ tools,
3700
+ toolCount: tools.length,
3701
+ allowedToolCount: tools.filter((tool) => tool.allowed).length
3702
+ };
3703
+ }
3704
+ /**
3705
+ * Update per-session tool access policy.
3706
+ */
3707
+ async setToolPolicy(params) {
3708
+ const { sessionId } = params;
3709
+ const session = await sessions.get(this.userId, sessionId);
3710
+ if (!session) {
3711
+ throw new Error("Session not found");
3712
+ }
3713
+ const client = await this.getOrCreateClient(sessionId);
3714
+ const allTools = await client.fetchTools();
3715
+ const toolPolicy = normalizeToolPolicyForUpdate(params.toolPolicy);
3716
+ validateToolPolicyAgainstTools(toolPolicy, allTools.tools, session.serverId);
3717
+ await sessions.update(this.userId, sessionId, { toolPolicy });
3718
+ const filteredTools = createToolPolicyGateway(this.userId, sessionId, client).filterTools({ ...session, toolPolicy }, allTools.tools);
3719
+ this.emitConnectionEvent({
3720
+ type: "tools_discovered",
3721
+ sessionId,
3722
+ serverId: session.serverId ?? "unknown",
3723
+ toolCount: filteredTools.length,
3724
+ tools: filteredTools,
3725
+ timestamp: Date.now()
3726
+ });
3727
+ return {
3728
+ success: true,
3729
+ toolPolicy,
3730
+ tools: filteredTools,
3731
+ toolCount: filteredTools.length
3732
+ };
3733
+ }
3500
3734
  /**
3501
3735
  * Call a tool on the MCP server
3502
3736
  */
3503
3737
  async callTool(params) {
3504
3738
  const { sessionId, toolName, toolArgs } = params;
3505
3739
  const client = await this.getOrCreateClient(sessionId);
3506
- const result = await client.callTool(toolName, toolArgs);
3740
+ const result = await createToolPolicyGateway(this.userId, sessionId, client).callTool(toolName, toolArgs);
3507
3741
  const meta = result._meta || {};
3508
3742
  return {
3509
3743
  ...result,
@@ -3553,7 +3787,7 @@ var SSEConnectionManager = class {
3553
3787
  client.onObservabilityEvent((event) => this.sendEvent(event));
3554
3788
  await client.connect();
3555
3789
  this.clients.set(sessionId, client);
3556
- const tools = await client.listTools();
3790
+ const { result: tools } = await this.listPolicyFilteredTools(sessionId);
3557
3791
  return { success: true, toolCount: tools.tools.length };
3558
3792
  } catch (error) {
3559
3793
  this.emitConnectionEvent({
@@ -3601,7 +3835,7 @@ var SSEConnectionManager = class {
3601
3835
  client.onConnectionEvent((event) => this.emitConnectionEvent(event));
3602
3836
  await client.finishAuth(code, oauthState);
3603
3837
  this.clients.set(sessionId, client);
3604
- const tools = await client.listTools();
3838
+ const { result: tools } = await this.listPolicyFilteredTools(sessionId);
3605
3839
  return { success: true, toolCount: tools.tools.length };
3606
3840
  } catch (error) {
3607
3841
  this.emitConnectionEvent({
@@ -3893,6 +4127,12 @@ var SSEClient = class {
3893
4127
  async disconnectFromServer(sessionId) {
3894
4128
  return this.sendRequest("disconnect", { sessionId });
3895
4129
  }
4130
+ async setToolPolicy(sessionId, toolPolicy) {
4131
+ return this.sendRequest("setToolPolicy", { sessionId, toolPolicy });
4132
+ }
4133
+ async getToolPolicy(sessionId) {
4134
+ return this.sendRequest("getToolPolicy", { sessionId });
4135
+ }
3896
4136
  async listTools(sessionId) {
3897
4137
  return this.sendRequest("listTools", { sessionId });
3898
4138
  }