@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
@@ -159,13 +159,9 @@ init_cjs_shims();
159
159
  // src/shared/utils.ts
160
160
  init_cjs_shims();
161
161
  var OAUTH_STATE_SEPARATOR = ".";
162
- var firstChar = nanoid.customAlphabet(
163
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
164
- 1
165
- );
166
- var rest = nanoid.customAlphabet(
167
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
168
- 11
162
+ var serverIdAlphabet = nanoid.customAlphabet(
163
+ "abcdefghijklmnopqrstuvwxyz0123456789",
164
+ 12
169
165
  );
170
166
  function sanitizeServerLabel(name) {
171
167
  let sanitized = name.replace(/[^a-zA-Z0-9-_]/g, "_").replace(/_{2,}/g, "_").toLowerCase();
@@ -175,7 +171,10 @@ function sanitizeServerLabel(name) {
175
171
  return sanitized;
176
172
  }
177
173
  function generateSessionId() {
178
- return firstChar() + rest();
174
+ return "sess_" + nanoid.nanoid(21);
175
+ }
176
+ function generateServerId() {
177
+ return serverIdAlphabet();
179
178
  }
180
179
  function formatOAuthState(nonce, sessionId) {
181
180
  return `${nonce}${OAUTH_STATE_SEPARATOR}${sessionId}`;
@@ -210,6 +209,60 @@ var SOFTWARE_VERSION = "2.3.4";
210
209
  var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
211
210
  var MCP_CLIENT_VERSION = "2.0";
212
211
 
212
+ // src/server/storage/tool-policy.ts
213
+ init_cjs_shims();
214
+ function createToolId(serverId, toolName) {
215
+ return `${serverId}::${toolName}`;
216
+ }
217
+ function normalizeToolIds(input) {
218
+ if (!Array.isArray(input)) return [];
219
+ return Array.from(new Set(
220
+ input.filter((id) => typeof id === "string").map((id) => id.trim()).filter(Boolean)
221
+ ));
222
+ }
223
+ function normalizeToolPolicy(input, now = Date.now()) {
224
+ if (!input || typeof input !== "object") {
225
+ return void 0;
226
+ }
227
+ const mode = input.mode === "allowlist" || input.mode === "denylist" ? input.mode : "all";
228
+ const updatedAt = typeof input.updatedAt === "number" && Number.isFinite(input.updatedAt) ? input.updatedAt : now;
229
+ if (mode === "all") {
230
+ return { mode: "all", toolIds: [], updatedAt };
231
+ }
232
+ return { mode, toolIds: normalizeToolIds(input.toolIds), updatedAt };
233
+ }
234
+ function normalizeToolPolicyForUpdate(input, now = Date.now()) {
235
+ return normalizeToolPolicy(input, now) ?? { mode: "all", toolIds: [], updatedAt: now };
236
+ }
237
+ function isToolAllowed(policy, toolName, serverId) {
238
+ if (!policy || policy.mode === "all") return true;
239
+ if (!serverId) return false;
240
+ const toolId = createToolId(serverId, toolName);
241
+ if (policy.mode === "allowlist") {
242
+ return policy.toolIds.includes(toolId);
243
+ }
244
+ return !policy.toolIds.includes(toolId);
245
+ }
246
+ function assertToolAllowed(policy, toolName, serverId) {
247
+ if (isToolAllowed(policy, toolName, serverId)) return;
248
+ throw new Error(`Tool "${toolName}" is not allowed for this MCP session`);
249
+ }
250
+ function filterToolsByPolicy(tools, policy, serverId) {
251
+ if (!policy || policy.mode === "all") return tools;
252
+ return tools.filter((tool) => isToolAllowed(policy, tool.name, serverId));
253
+ }
254
+ function validateToolPolicyAgainstTools(policy, tools, serverId) {
255
+ if (policy.mode === "all") return;
256
+ if (!serverId) {
257
+ throw new Error("Cannot validate MCP tool policy without a serverId");
258
+ }
259
+ const availableIds = new Set(tools.map((tool) => createToolId(serverId, tool.name)));
260
+ const unknownIds = policy.toolIds.filter((id) => !availableIds.has(id));
261
+ if (unknownIds.length > 0) {
262
+ throw new Error(`Unknown tool id(s) for this MCP session: ${unknownIds.join(", ")}`);
263
+ }
264
+ }
265
+
213
266
  // src/server/storage/session-lifecycle.ts
214
267
  function resolveSessionExpiresAt(status = "pending", referenceTime = Date.now()) {
215
268
  return status === "active" ? null : referenceTime + STATE_EXPIRATION_MS;
@@ -226,7 +279,8 @@ function normalizeNewSession(session, now = Date.now()) {
226
279
  status,
227
280
  createdAt,
228
281
  updatedAt,
229
- expiresAt: resolveSessionExpiresAt(status, status === "active" ? updatedAt : createdAt)
282
+ expiresAt: resolveSessionExpiresAt(status, status === "active" ? updatedAt : createdAt),
283
+ toolPolicy: normalizeToolPolicy(session.toolPolicy, updatedAt)
230
284
  };
231
285
  }
232
286
  function mergeSessionUpdate(current, data, now = Date.now()) {
@@ -240,7 +294,8 @@ function mergeSessionUpdate(current, data, now = Date.now()) {
240
294
  return {
241
295
  ...updated,
242
296
  status,
243
- expiresAt: resolveSessionExpiresAt(status, updatedAt)
297
+ expiresAt: resolveSessionExpiresAt(status, updatedAt),
298
+ toolPolicy: normalizeToolPolicy(updated.toolPolicy, updatedAt)
244
299
  };
245
300
  }
246
301
  function normalizeStoredSession(session) {
@@ -253,7 +308,8 @@ function normalizeStoredSession(session) {
253
308
  status,
254
309
  createdAt,
255
310
  updatedAt,
256
- expiresAt
311
+ expiresAt,
312
+ toolPolicy: normalizeToolPolicy(session.toolPolicy, updatedAt)
257
313
  };
258
314
  }
259
315
  function isSessionExpired(session, now = Date.now()) {
@@ -1124,7 +1180,8 @@ var SupabaseStorageBackend = class {
1124
1180
  userId: row.user_id,
1125
1181
  headers: decryptObject(row.headers),
1126
1182
  authUrl: row.auth_url,
1127
- status: row.status ?? "pending"
1183
+ status: row.status ?? "pending",
1184
+ toolPolicy: normalizeToolPolicy(row.tool_policy)
1128
1185
  };
1129
1186
  }
1130
1187
  mapRowToCredentials(row, userId, sessionId) {
@@ -1148,7 +1205,7 @@ var SupabaseStorageBackend = class {
1148
1205
  const createdAt = new Date(session.createdAt || Date.now()).toISOString();
1149
1206
  const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
1150
1207
  const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
1151
- const { error } = await this.supabase.from("mcp_sessions").insert({
1208
+ const insertData = {
1152
1209
  session_id: sessionId,
1153
1210
  user_id: userId,
1154
1211
  server_id: session.serverId,
@@ -1162,7 +1219,12 @@ var SupabaseStorageBackend = class {
1162
1219
  auth_url: session.authUrl ?? null,
1163
1220
  status,
1164
1221
  expires_at: expiresAt === null ? null : new Date(expiresAt).toISOString()
1165
- });
1222
+ };
1223
+ const toolPolicy = normalizeToolPolicy(session.toolPolicy);
1224
+ if (toolPolicy) {
1225
+ insertData.tool_policy = toolPolicy;
1226
+ }
1227
+ const { error } = await this.supabase.from("mcp_sessions").insert(insertData);
1166
1228
  if (error) {
1167
1229
  if (error.code === "23505") {
1168
1230
  throw new Error(`Session ${sessionId} already exists`);
@@ -1187,6 +1249,7 @@ var SupabaseStorageBackend = class {
1187
1249
  }
1188
1250
  if ("headers" in data) updateData.headers = encryptObject(data.headers);
1189
1251
  if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
1252
+ if ("toolPolicy" in data) updateData.tool_policy = normalizeToolPolicy(data.toolPolicy);
1190
1253
  const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
1191
1254
  let updatedRows = null;
1192
1255
  if (shouldUpdateSession) {
@@ -1360,7 +1423,8 @@ var NeonStorageBackend = class {
1360
1423
  userId: row.user_id,
1361
1424
  headers: decryptObject(row.headers),
1362
1425
  authUrl: row.auth_url ?? void 0,
1363
- status: row.status ?? "pending"
1426
+ status: row.status ?? "pending",
1427
+ toolPolicy: normalizeToolPolicy(row.tool_policy)
1364
1428
  };
1365
1429
  }
1366
1430
  mapRowToCredentials(row, userId, sessionId) {
@@ -1383,42 +1447,48 @@ var NeonStorageBackend = class {
1383
1447
  const status = session.status ?? "pending";
1384
1448
  const createdAt = new Date(session.createdAt || Date.now()).toISOString();
1385
1449
  const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
1386
- const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
1450
+ const createdAtMs = new Date(createdAt).getTime();
1451
+ const expiresAt = resolveSessionExpiresAt(status, createdAtMs);
1452
+ const toolPolicy = normalizeToolPolicy(session.toolPolicy, createdAtMs);
1453
+ const columns = [
1454
+ "session_id",
1455
+ "user_id",
1456
+ "server_id",
1457
+ "server_name",
1458
+ "server_url",
1459
+ "transport_type",
1460
+ "callback_url",
1461
+ "created_at",
1462
+ "updated_at",
1463
+ "headers",
1464
+ "auth_url",
1465
+ "status",
1466
+ "expires_at"
1467
+ ];
1468
+ const values = [
1469
+ sessionId,
1470
+ userId,
1471
+ session.serverId,
1472
+ session.serverName,
1473
+ session.serverUrl,
1474
+ session.transportType,
1475
+ session.callbackUrl,
1476
+ createdAt,
1477
+ updatedAt,
1478
+ encryptObject(session.headers),
1479
+ session.authUrl ?? null,
1480
+ status,
1481
+ expiresAt === null ? null : new Date(expiresAt).toISOString()
1482
+ ];
1483
+ if (toolPolicy) {
1484
+ columns.push("tool_policy");
1485
+ values.push(toolPolicy);
1486
+ }
1487
+ const placeholders = values.map((_, i) => `$${i + 1}`);
1387
1488
  try {
1388
1489
  await this.sql.query(
1389
- `INSERT INTO ${this.tableName} (
1390
- session_id,
1391
- user_id,
1392
- server_id,
1393
- server_name,
1394
- server_url,
1395
- transport_type,
1396
- callback_url,
1397
- created_at,
1398
- updated_at,
1399
- headers,
1400
- auth_url,
1401
- status,
1402
- expires_at
1403
- ) VALUES (
1404
- $1, $2, $3, $4, $5, $6, $7, $8,
1405
- $9, $10, $11, $12, $13
1406
- )`,
1407
- [
1408
- sessionId,
1409
- userId,
1410
- session.serverId,
1411
- session.serverName,
1412
- session.serverUrl,
1413
- session.transportType,
1414
- session.callbackUrl,
1415
- createdAt,
1416
- updatedAt,
1417
- encryptObject(session.headers),
1418
- session.authUrl ?? null,
1419
- status,
1420
- expiresAt === null ? null : new Date(expiresAt).toISOString()
1421
- ]
1490
+ `INSERT INTO ${this.tableName} (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`,
1491
+ values
1422
1492
  );
1423
1493
  } catch (error) {
1424
1494
  if (error.code === "23505") {
@@ -1435,36 +1505,36 @@ var NeonStorageBackend = class {
1435
1505
  const updatedSession = { ...currentSession, ...data };
1436
1506
  const status = updatedSession.status ?? "pending";
1437
1507
  const expiresAt = resolveSessionExpiresAt(status);
1438
- 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;
1508
+ 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;
1439
1509
  if (shouldUpdateSession) {
1510
+ const setClauses = [];
1511
+ const values = [];
1512
+ let paramIndex = 1;
1513
+ const addSet = (column, value) => {
1514
+ setClauses.push(`${column} = $${paramIndex++}`);
1515
+ values.push(value);
1516
+ };
1517
+ addSet("server_id", updatedSession.serverId);
1518
+ addSet("server_name", updatedSession.serverName);
1519
+ addSet("server_url", updatedSession.serverUrl);
1520
+ addSet("transport_type", updatedSession.transportType);
1521
+ addSet("callback_url", updatedSession.callbackUrl);
1522
+ addSet("status", status);
1523
+ addSet("headers", encryptObject(updatedSession.headers));
1524
+ addSet("auth_url", updatedSession.authUrl ?? null);
1525
+ addSet("expires_at", expiresAt === null ? null : new Date(expiresAt).toISOString());
1526
+ if ("toolPolicy" in data) {
1527
+ const policyUpdatedAt = updatedSession.updatedAt ?? Date.now();
1528
+ const toolPolicy = normalizeToolPolicy(updatedSession.toolPolicy, policyUpdatedAt) ?? { mode: "all", toolIds: [], updatedAt: policyUpdatedAt };
1529
+ addSet("tool_policy", toolPolicy);
1530
+ }
1531
+ setClauses.push("updated_at = now()");
1440
1532
  const updatedRows = await this.sql.query(
1441
1533
  `UPDATE ${this.tableName}
1442
- SET
1443
- server_id = $1,
1444
- server_name = $2,
1445
- server_url = $3,
1446
- transport_type = $4,
1447
- callback_url = $5,
1448
- status = $6,
1449
- headers = $7,
1450
- auth_url = $8,
1451
- expires_at = $9,
1452
- updated_at = now()
1453
- WHERE user_id = $10 AND session_id = $11
1534
+ SET ${setClauses.join(", ")}
1535
+ WHERE user_id = $${paramIndex++} AND session_id = $${paramIndex++}
1454
1536
  RETURNING id`,
1455
- [
1456
- updatedSession.serverId,
1457
- updatedSession.serverName,
1458
- updatedSession.serverUrl,
1459
- updatedSession.transportType,
1460
- updatedSession.callbackUrl,
1461
- status,
1462
- encryptObject(updatedSession.headers),
1463
- updatedSession.authUrl ?? null,
1464
- expiresAt === null ? null : new Date(expiresAt).toISOString(),
1465
- userId,
1466
- sessionId
1467
- ]
1537
+ [...values, userId, sessionId]
1468
1538
  );
1469
1539
  if (updatedRows.length === 0) {
1470
1540
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
@@ -2384,7 +2454,15 @@ var MCPClient = class {
2384
2454
  if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
2385
2455
  this.createdAt = Date.now();
2386
2456
  const updatedAt = this.createdAt;
2387
- console.log(`[MCPClient] Creating pending session ${this.sessionId} for connection setup`);
2457
+ this._onObservabilityEvent.fire({
2458
+ type: "mcp:client:session_created",
2459
+ level: "info",
2460
+ message: `Creating pending session ${this.sessionId} for connection setup`,
2461
+ sessionId: this.sessionId,
2462
+ serverId: this.serverId,
2463
+ timestamp: Date.now(),
2464
+ id: nanoid.nanoid()
2465
+ });
2388
2466
  await sessions.create({
2389
2467
  sessionId: this.sessionId,
2390
2468
  userId: this.userId,
@@ -2514,7 +2592,15 @@ var MCPClient = class {
2514
2592
  this.transportType = transportType;
2515
2593
  this.emitStateChange("CONNECTED");
2516
2594
  this.emitProgress("Connected successfully");
2517
- console.log(`[MCPClient] Saving active session ${this.sessionId} (connect success)`);
2595
+ this._onObservabilityEvent.fire({
2596
+ type: "mcp:client:session_saved",
2597
+ level: "info",
2598
+ message: `Saving active session ${this.sessionId} (connect success)`,
2599
+ sessionId: this.sessionId,
2600
+ serverId: this.serverId,
2601
+ timestamp: Date.now(),
2602
+ id: nanoid.nanoid()
2603
+ });
2518
2604
  await this.saveSession("active");
2519
2605
  } catch (error) {
2520
2606
  if (error instanceof auth_js.UnauthorizedError || error instanceof Error && error.message.toLowerCase().includes("unauthorized")) {
@@ -2531,7 +2617,15 @@ var MCPClient = class {
2531
2617
  throw new Error(message);
2532
2618
  }
2533
2619
  this.emitStateChange("AUTHENTICATING");
2534
- console.log(`[MCPClient] Saving pending OAuth session ${this.sessionId}`);
2620
+ this._onObservabilityEvent.fire({
2621
+ type: "mcp:client:session_saved",
2622
+ level: "info",
2623
+ message: `Saving pending OAuth session ${this.sessionId}`,
2624
+ sessionId: this.sessionId,
2625
+ serverId: this.serverId,
2626
+ timestamp: Date.now(),
2627
+ id: nanoid.nanoid()
2628
+ });
2535
2629
  await this.saveSession("pending");
2536
2630
  if (this.serverId) {
2537
2631
  this._onConnectionEvent.fire({
@@ -2606,27 +2700,25 @@ var MCPClient = class {
2606
2700
  this.emitStateChange("AUTHENTICATED");
2607
2701
  authenticatedStateEmitted = true;
2608
2702
  }
2609
- this.emitProgress("Creating authenticated client...");
2610
- this.client = new index_js.Client(
2611
- {
2612
- name: MCP_CLIENT_NAME,
2613
- version: MCP_CLIENT_VERSION
2614
- },
2615
- {
2616
- capabilities: {
2617
- extensions: {
2618
- "io.modelcontextprotocol/ui": {
2619
- mimeTypes: ["text/html+mcp"]
2620
- }
2621
- }
2622
- }
2623
- }
2624
- );
2625
2703
  this.emitStateChange("CONNECTING");
2704
+ if (this.client.transport) {
2705
+ try {
2706
+ await this.client.close();
2707
+ } catch {
2708
+ }
2709
+ }
2626
2710
  await this.client.connect(this.transport);
2627
2711
  this.transportType = currentType;
2628
2712
  this.emitStateChange("CONNECTED");
2629
- console.log(`[MCPClient] Saving active session ${this.sessionId} (OAuth complete)`);
2713
+ this._onObservabilityEvent.fire({
2714
+ type: "mcp:client:session_saved",
2715
+ level: "info",
2716
+ message: `Saving active session ${this.sessionId} (OAuth complete)`,
2717
+ sessionId: this.sessionId,
2718
+ serverId: this.serverId,
2719
+ timestamp: Date.now(),
2720
+ id: nanoid.nanoid()
2721
+ });
2630
2722
  await this.saveSession("active");
2631
2723
  return;
2632
2724
  } catch (error) {
@@ -2661,24 +2753,28 @@ var MCPClient = class {
2661
2753
  throw lastError;
2662
2754
  }
2663
2755
  }
2756
+ /**
2757
+ * Lists all available tools from the connected MCP server without emitting discovery events.
2758
+ * Gateways use this to apply policy before publishing tools to agents or UI state.
2759
+ */
2760
+ async fetchTools() {
2761
+ const request = {
2762
+ method: "tools/list",
2763
+ params: {}
2764
+ };
2765
+ return await this.withRetry(
2766
+ () => this.client.request(request, types_js.ListToolsResultSchema)
2767
+ );
2768
+ }
2664
2769
  /**
2665
2770
  * Lists all available tools from the connected MCP server
2666
2771
  * @returns List of tools with their schemas and descriptions
2667
2772
  * @throws {Error} When client is not connected
2668
2773
  */
2669
2774
  async listTools() {
2670
- if (!this.client) {
2671
- throw new Error("Not connected to server");
2672
- }
2673
2775
  this.emitStateChange("DISCOVERING");
2674
2776
  try {
2675
- const request = {
2676
- method: "tools/list",
2677
- params: {}
2678
- };
2679
- const result = await this.withRetry(
2680
- () => this.client.request(request, types_js.ListToolsResultSchema)
2681
- );
2777
+ const result = await this.fetchTools();
2682
2778
  if (this.serverId) {
2683
2779
  this._onConnectionEvent.fire({
2684
2780
  type: "tools_discovered",
@@ -2707,9 +2803,6 @@ var MCPClient = class {
2707
2803
  * @throws {Error} When client is not connected
2708
2804
  */
2709
2805
  async callTool(toolName, toolArgs) {
2710
- if (!this.client) {
2711
- throw new Error("Not connected to server");
2712
- }
2713
2806
  const request = {
2714
2807
  method: "tools/call",
2715
2808
  params: {
@@ -2763,9 +2856,6 @@ var MCPClient = class {
2763
2856
  * @throws {Error} When client is not connected
2764
2857
  */
2765
2858
  async listPrompts() {
2766
- if (!this.client) {
2767
- throw new Error("Not connected to server");
2768
- }
2769
2859
  this.emitStateChange("DISCOVERING");
2770
2860
  try {
2771
2861
  const request = {
@@ -2793,9 +2883,6 @@ var MCPClient = class {
2793
2883
  * @throws {Error} When client is not connected
2794
2884
  */
2795
2885
  async getPrompt(name, args) {
2796
- if (!this.client) {
2797
- throw new Error("Not connected to server");
2798
- }
2799
2886
  const request = {
2800
2887
  method: "prompts/get",
2801
2888
  params: {
@@ -2813,9 +2900,6 @@ var MCPClient = class {
2813
2900
  * @throws {Error} When client is not connected
2814
2901
  */
2815
2902
  async listResources() {
2816
- if (!this.client) {
2817
- throw new Error("Not connected to server");
2818
- }
2819
2903
  this.emitStateChange("DISCOVERING");
2820
2904
  try {
2821
2905
  const request = {
@@ -2842,9 +2926,6 @@ var MCPClient = class {
2842
2926
  * @throws {Error} When client is not connected
2843
2927
  */
2844
2928
  async readResource(uri) {
2845
- if (!this.client) {
2846
- throw new Error("Not connected to server");
2847
- }
2848
2929
  const request = {
2849
2930
  method: "resources/read",
2850
2931
  params: {
@@ -2900,7 +2981,16 @@ var MCPClient = class {
2900
2981
  try {
2901
2982
  await this.ensureSession();
2902
2983
  } catch (error) {
2903
- console.warn("[MCPClient] Initialization failed during clearSession:", error);
2984
+ this._onObservabilityEvent.fire({
2985
+ type: "mcp:client:error",
2986
+ level: "warn",
2987
+ message: "Initialization failed during clearSession",
2988
+ sessionId: this.sessionId,
2989
+ serverId: this.serverId,
2990
+ payload: { error: String(error) },
2991
+ timestamp: Date.now(),
2992
+ id: nanoid.nanoid()
2993
+ });
2904
2994
  }
2905
2995
  if (this.oauthProvider) {
2906
2996
  await this.oauthProvider.invalidateCredentials("all");
@@ -3013,6 +3103,69 @@ var MCPClient = class {
3013
3103
 
3014
3104
  // src/server/mcp/multi-session-client.ts
3015
3105
  init_cjs_shims();
3106
+
3107
+ // src/server/mcp/tool-policy-gateway.ts
3108
+ init_cjs_shims();
3109
+ var ToolPolicyGateway = class {
3110
+ constructor(userId, sessionId, client) {
3111
+ this.userId = userId;
3112
+ this.sessionId = sessionId;
3113
+ this.client = client;
3114
+ }
3115
+ isConnected() {
3116
+ return this.client.isConnected();
3117
+ }
3118
+ getServerId() {
3119
+ return this.client.getServerId?.();
3120
+ }
3121
+ getServerName() {
3122
+ return this.client.getServerName?.();
3123
+ }
3124
+ getServerUrl() {
3125
+ return this.client.getServerUrl?.();
3126
+ }
3127
+ getSessionId() {
3128
+ return this.client.getSessionId?.() ?? this.sessionId;
3129
+ }
3130
+ async listTools() {
3131
+ const session = await this.getSession();
3132
+ const result = await this.client.fetchTools();
3133
+ const tools = this.filterTools(session, result.tools);
3134
+ return {
3135
+ ...result,
3136
+ tools
3137
+ };
3138
+ }
3139
+ async listAllTools() {
3140
+ return await this.client.fetchTools();
3141
+ }
3142
+ async callTool(name, args) {
3143
+ const session = await this.getSession();
3144
+ this.assertAllowed(session, name);
3145
+ return await this.client.callTool(name, args);
3146
+ }
3147
+ filterTools(session, tools) {
3148
+ return filterToolsByPolicy(tools, session.toolPolicy, this.getPolicyServerId(session));
3149
+ }
3150
+ assertAllowed(session, toolName) {
3151
+ assertToolAllowed(session.toolPolicy, toolName, this.getPolicyServerId(session));
3152
+ }
3153
+ async getSession() {
3154
+ const session = await sessions.get(this.userId, this.sessionId);
3155
+ if (!session) {
3156
+ throw new Error("Session not found");
3157
+ }
3158
+ return session;
3159
+ }
3160
+ getPolicyServerId(session) {
3161
+ return this.client.getServerId?.() ?? session.serverId;
3162
+ }
3163
+ };
3164
+ function createToolPolicyGateway(userId, sessionId, client) {
3165
+ return new ToolPolicyGateway(userId, sessionId, client);
3166
+ }
3167
+
3168
+ // src/server/mcp/multi-session-client.ts
3016
3169
  var DEFAULT_TIMEOUT_MS = 15e3;
3017
3170
  var DEFAULT_MAX_RETRIES = 2;
3018
3171
  var DEFAULT_RETRY_DELAY_MS = 1e3;
@@ -3077,7 +3230,9 @@ var MultiSessionClient = class {
3077
3230
  * or to route a tool call to the right client by `serverId`.
3078
3231
  */
3079
3232
  getClients() {
3080
- return this.clients;
3233
+ return this.clients.map(
3234
+ (client) => createToolPolicyGateway(this.userId, client.getSessionId(), client)
3235
+ );
3081
3236
  }
3082
3237
  /**
3083
3238
  * Removes and disconnects a single session by ID.
@@ -3316,6 +3471,12 @@ var SSEConnectionManager = class {
3316
3471
  case "listTools":
3317
3472
  result = await this.listTools(request.params);
3318
3473
  break;
3474
+ case "setToolPolicy":
3475
+ result = await this.setToolPolicy(request.params);
3476
+ break;
3477
+ case "getToolPolicy":
3478
+ result = await this.getToolPolicy(request.params);
3479
+ break;
3319
3480
  case "callTool":
3320
3481
  result = await this.callTool(request.params);
3321
3482
  break;
@@ -3372,7 +3533,8 @@ var SSEConnectionManager = class {
3372
3533
  transport: s.transportType,
3373
3534
  createdAt: s.createdAt,
3374
3535
  updatedAt: s.updatedAt ?? s.createdAt,
3375
- status: s.status ?? "pending"
3536
+ status: s.status ?? "pending",
3537
+ toolPolicy: s.toolPolicy
3376
3538
  }))
3377
3539
  };
3378
3540
  }
@@ -3382,7 +3544,7 @@ var SSEConnectionManager = class {
3382
3544
  async connect(params) {
3383
3545
  const { serverName, serverUrl, callbackUrl, transportType } = params;
3384
3546
  const headers = normalizeHeaders(params.headers);
3385
- const serverId = params.serverId && params.serverId.length <= 12 ? params.serverId : await sessions.generateSessionId();
3547
+ const serverId = params.serverId && params.serverId.length <= 12 ? params.serverId : generateServerId();
3386
3548
  const existingSessions = await sessions.list(this.userId);
3387
3549
  const duplicate = existingSessions.find(
3388
3550
  (s) => s.serverId === serverId || s.serverUrl === serverUrl
@@ -3420,7 +3582,7 @@ var SSEConnectionManager = class {
3420
3582
  this.sendEvent(event);
3421
3583
  });
3422
3584
  await client.connect();
3423
- await client.listTools();
3585
+ await this.listPolicyFilteredTools(sessionId);
3424
3586
  return {
3425
3587
  sessionId,
3426
3588
  success: true
@@ -3491,22 +3653,98 @@ var SSEConnectionManager = class {
3491
3653
  this.clients.set(sessionId, client);
3492
3654
  return client;
3493
3655
  }
3656
+ async listPolicyFilteredTools(sessionId) {
3657
+ const session = await sessions.get(this.userId, sessionId);
3658
+ if (!session) {
3659
+ throw new Error("Session not found");
3660
+ }
3661
+ const client = await this.getOrCreateClient(sessionId);
3662
+ const gateway = createToolPolicyGateway(this.userId, sessionId, client);
3663
+ const result = await gateway.listTools();
3664
+ this.emitConnectionEvent({
3665
+ type: "tools_discovered",
3666
+ sessionId,
3667
+ serverId: session.serverId ?? "unknown",
3668
+ toolCount: result.tools.length,
3669
+ tools: result.tools,
3670
+ timestamp: Date.now()
3671
+ });
3672
+ return { session, result };
3673
+ }
3494
3674
  /**
3495
3675
  * List tools from a session
3496
3676
  */
3497
3677
  async listTools(params) {
3498
3678
  const { sessionId } = params;
3499
- const client = await this.getOrCreateClient(sessionId);
3500
- const result = await client.listTools();
3679
+ const { result } = await this.listPolicyFilteredTools(sessionId);
3501
3680
  return { tools: result.tools };
3502
3681
  }
3682
+ /**
3683
+ * Get all raw tools with effective access state for management UI.
3684
+ */
3685
+ async getToolPolicy(params) {
3686
+ const { sessionId } = params;
3687
+ const session = await sessions.get(this.userId, sessionId);
3688
+ if (!session) {
3689
+ throw new Error("Session not found");
3690
+ }
3691
+ const client = await this.getOrCreateClient(sessionId);
3692
+ const allTools = await client.fetchTools();
3693
+ const toolPolicy = session.toolPolicy ?? {
3694
+ mode: "all",
3695
+ toolIds: [],
3696
+ updatedAt: session.updatedAt ?? session.createdAt
3697
+ };
3698
+ const serverId = session.serverId ?? "unknown";
3699
+ const tools = allTools.tools.map((tool) => ({
3700
+ ...tool,
3701
+ toolId: createToolId(serverId, tool.name),
3702
+ allowed: isToolAllowed(toolPolicy, tool.name, session.serverId)
3703
+ }));
3704
+ return {
3705
+ toolPolicy,
3706
+ tools,
3707
+ toolCount: tools.length,
3708
+ allowedToolCount: tools.filter((tool) => tool.allowed).length
3709
+ };
3710
+ }
3711
+ /**
3712
+ * Update per-session tool access policy.
3713
+ */
3714
+ async setToolPolicy(params) {
3715
+ const { sessionId } = params;
3716
+ const session = await sessions.get(this.userId, sessionId);
3717
+ if (!session) {
3718
+ throw new Error("Session not found");
3719
+ }
3720
+ const client = await this.getOrCreateClient(sessionId);
3721
+ const allTools = await client.fetchTools();
3722
+ const toolPolicy = normalizeToolPolicyForUpdate(params.toolPolicy);
3723
+ validateToolPolicyAgainstTools(toolPolicy, allTools.tools, session.serverId);
3724
+ await sessions.update(this.userId, sessionId, { toolPolicy });
3725
+ const filteredTools = createToolPolicyGateway(this.userId, sessionId, client).filterTools({ ...session, toolPolicy }, allTools.tools);
3726
+ this.emitConnectionEvent({
3727
+ type: "tools_discovered",
3728
+ sessionId,
3729
+ serverId: session.serverId ?? "unknown",
3730
+ toolCount: filteredTools.length,
3731
+ tools: filteredTools,
3732
+ timestamp: Date.now()
3733
+ });
3734
+ return {
3735
+ success: true,
3736
+ toolPolicy,
3737
+ tools: filteredTools,
3738
+ toolCount: filteredTools.length
3739
+ };
3740
+ }
3503
3741
  /**
3504
3742
  * Call a tool on the MCP server
3505
3743
  */
3506
3744
  async callTool(params) {
3507
3745
  const { sessionId, toolName, toolArgs } = params;
3508
3746
  const client = await this.getOrCreateClient(sessionId);
3509
- const result = await client.callTool(toolName, toolArgs);
3747
+ const result = await createToolPolicyGateway(this.userId, sessionId, client).callTool(toolName, toolArgs);
3510
3748
  const meta = result._meta || {};
3511
3749
  return {
3512
3750
  ...result,
@@ -3556,7 +3794,7 @@ var SSEConnectionManager = class {
3556
3794
  client.onObservabilityEvent((event) => this.sendEvent(event));
3557
3795
  await client.connect();
3558
3796
  this.clients.set(sessionId, client);
3559
- const tools = await client.listTools();
3797
+ const { result: tools } = await this.listPolicyFilteredTools(sessionId);
3560
3798
  return { success: true, toolCount: tools.tools.length };
3561
3799
  } catch (error) {
3562
3800
  this.emitConnectionEvent({
@@ -3604,7 +3842,7 @@ var SSEConnectionManager = class {
3604
3842
  client.onConnectionEvent((event) => this.emitConnectionEvent(event));
3605
3843
  await client.finishAuth(code, oauthState);
3606
3844
  this.clients.set(sessionId, client);
3607
- const tools = await client.listTools();
3845
+ const { result: tools } = await this.listPolicyFilteredTools(sessionId);
3608
3846
  return { success: true, toolCount: tools.tools.length };
3609
3847
  } catch (error) {
3610
3848
  this.emitConnectionEvent({