@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
@@ -112,13 +112,9 @@ var init_redis = __esm({
112
112
  }
113
113
  });
114
114
  var OAUTH_STATE_SEPARATOR = ".";
115
- var firstChar = customAlphabet(
116
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
117
- 1
118
- );
119
- var rest = customAlphabet(
120
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
121
- 11
115
+ var serverIdAlphabet = customAlphabet(
116
+ "abcdefghijklmnopqrstuvwxyz0123456789",
117
+ 12
122
118
  );
123
119
  function sanitizeServerLabel(name) {
124
120
  let sanitized = name.replace(/[^a-zA-Z0-9-_]/g, "_").replace(/_{2,}/g, "_").toLowerCase();
@@ -128,7 +124,10 @@ function sanitizeServerLabel(name) {
128
124
  return sanitized;
129
125
  }
130
126
  function generateSessionId() {
131
- return firstChar() + rest();
127
+ return "sess_" + nanoid(21);
128
+ }
129
+ function generateServerId() {
130
+ return serverIdAlphabet();
132
131
  }
133
132
  function formatOAuthState(nonce, sessionId) {
134
133
  return `${nonce}${OAUTH_STATE_SEPARATOR}${sessionId}`;
@@ -159,6 +158,59 @@ var SOFTWARE_VERSION = "2.3.4";
159
158
  var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
160
159
  var MCP_CLIENT_VERSION = "2.0";
161
160
 
161
+ // src/server/storage/tool-policy.ts
162
+ function createToolId(serverId, toolName) {
163
+ return `${serverId}::${toolName}`;
164
+ }
165
+ function normalizeToolIds(input) {
166
+ if (!Array.isArray(input)) return [];
167
+ return Array.from(new Set(
168
+ input.filter((id) => typeof id === "string").map((id) => id.trim()).filter(Boolean)
169
+ ));
170
+ }
171
+ function normalizeToolPolicy(input, now = Date.now()) {
172
+ if (!input || typeof input !== "object") {
173
+ return void 0;
174
+ }
175
+ const mode = input.mode === "allowlist" || input.mode === "denylist" ? input.mode : "all";
176
+ const updatedAt = typeof input.updatedAt === "number" && Number.isFinite(input.updatedAt) ? input.updatedAt : now;
177
+ if (mode === "all") {
178
+ return { mode: "all", toolIds: [], updatedAt };
179
+ }
180
+ return { mode, toolIds: normalizeToolIds(input.toolIds), updatedAt };
181
+ }
182
+ function normalizeToolPolicyForUpdate(input, now = Date.now()) {
183
+ return normalizeToolPolicy(input, now) ?? { mode: "all", toolIds: [], updatedAt: now };
184
+ }
185
+ function isToolAllowed(policy, toolName, serverId) {
186
+ if (!policy || policy.mode === "all") return true;
187
+ if (!serverId) return false;
188
+ const toolId = createToolId(serverId, toolName);
189
+ if (policy.mode === "allowlist") {
190
+ return policy.toolIds.includes(toolId);
191
+ }
192
+ return !policy.toolIds.includes(toolId);
193
+ }
194
+ function assertToolAllowed(policy, toolName, serverId) {
195
+ if (isToolAllowed(policy, toolName, serverId)) return;
196
+ throw new Error(`Tool "${toolName}" is not allowed for this MCP session`);
197
+ }
198
+ function filterToolsByPolicy(tools, policy, serverId) {
199
+ if (!policy || policy.mode === "all") return tools;
200
+ return tools.filter((tool) => isToolAllowed(policy, tool.name, serverId));
201
+ }
202
+ function validateToolPolicyAgainstTools(policy, tools, serverId) {
203
+ if (policy.mode === "all") return;
204
+ if (!serverId) {
205
+ throw new Error("Cannot validate MCP tool policy without a serverId");
206
+ }
207
+ const availableIds = new Set(tools.map((tool) => createToolId(serverId, tool.name)));
208
+ const unknownIds = policy.toolIds.filter((id) => !availableIds.has(id));
209
+ if (unknownIds.length > 0) {
210
+ throw new Error(`Unknown tool id(s) for this MCP session: ${unknownIds.join(", ")}`);
211
+ }
212
+ }
213
+
162
214
  // src/server/storage/session-lifecycle.ts
163
215
  function resolveSessionExpiresAt(status = "pending", referenceTime = Date.now()) {
164
216
  return status === "active" ? null : referenceTime + STATE_EXPIRATION_MS;
@@ -175,7 +227,8 @@ function normalizeNewSession(session, now = Date.now()) {
175
227
  status,
176
228
  createdAt,
177
229
  updatedAt,
178
- expiresAt: resolveSessionExpiresAt(status, status === "active" ? updatedAt : createdAt)
230
+ expiresAt: resolveSessionExpiresAt(status, status === "active" ? updatedAt : createdAt),
231
+ toolPolicy: normalizeToolPolicy(session.toolPolicy, updatedAt)
179
232
  };
180
233
  }
181
234
  function mergeSessionUpdate(current, data, now = Date.now()) {
@@ -189,7 +242,8 @@ function mergeSessionUpdate(current, data, now = Date.now()) {
189
242
  return {
190
243
  ...updated,
191
244
  status,
192
- expiresAt: resolveSessionExpiresAt(status, updatedAt)
245
+ expiresAt: resolveSessionExpiresAt(status, updatedAt),
246
+ toolPolicy: normalizeToolPolicy(updated.toolPolicy, updatedAt)
193
247
  };
194
248
  }
195
249
  function normalizeStoredSession(session) {
@@ -202,7 +256,8 @@ function normalizeStoredSession(session) {
202
256
  status,
203
257
  createdAt,
204
258
  updatedAt,
205
- expiresAt
259
+ expiresAt,
260
+ toolPolicy: normalizeToolPolicy(session.toolPolicy, updatedAt)
206
261
  };
207
262
  }
208
263
  function isSessionExpired(session, now = Date.now()) {
@@ -1060,7 +1115,8 @@ var SupabaseStorageBackend = class {
1060
1115
  userId: row.user_id,
1061
1116
  headers: decryptObject(row.headers),
1062
1117
  authUrl: row.auth_url,
1063
- status: row.status ?? "pending"
1118
+ status: row.status ?? "pending",
1119
+ toolPolicy: normalizeToolPolicy(row.tool_policy)
1064
1120
  };
1065
1121
  }
1066
1122
  mapRowToCredentials(row, userId, sessionId) {
@@ -1084,7 +1140,7 @@ var SupabaseStorageBackend = class {
1084
1140
  const createdAt = new Date(session.createdAt || Date.now()).toISOString();
1085
1141
  const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
1086
1142
  const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
1087
- const { error } = await this.supabase.from("mcp_sessions").insert({
1143
+ const insertData = {
1088
1144
  session_id: sessionId,
1089
1145
  user_id: userId,
1090
1146
  server_id: session.serverId,
@@ -1098,7 +1154,12 @@ var SupabaseStorageBackend = class {
1098
1154
  auth_url: session.authUrl ?? null,
1099
1155
  status,
1100
1156
  expires_at: expiresAt === null ? null : new Date(expiresAt).toISOString()
1101
- });
1157
+ };
1158
+ const toolPolicy = normalizeToolPolicy(session.toolPolicy);
1159
+ if (toolPolicy) {
1160
+ insertData.tool_policy = toolPolicy;
1161
+ }
1162
+ const { error } = await this.supabase.from("mcp_sessions").insert(insertData);
1102
1163
  if (error) {
1103
1164
  if (error.code === "23505") {
1104
1165
  throw new Error(`Session ${sessionId} already exists`);
@@ -1123,6 +1184,7 @@ var SupabaseStorageBackend = class {
1123
1184
  }
1124
1185
  if ("headers" in data) updateData.headers = encryptObject(data.headers);
1125
1186
  if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
1187
+ if ("toolPolicy" in data) updateData.tool_policy = normalizeToolPolicy(data.toolPolicy);
1126
1188
  const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
1127
1189
  let updatedRows = null;
1128
1190
  if (shouldUpdateSession) {
@@ -1295,7 +1357,8 @@ var NeonStorageBackend = class {
1295
1357
  userId: row.user_id,
1296
1358
  headers: decryptObject(row.headers),
1297
1359
  authUrl: row.auth_url ?? void 0,
1298
- status: row.status ?? "pending"
1360
+ status: row.status ?? "pending",
1361
+ toolPolicy: normalizeToolPolicy(row.tool_policy)
1299
1362
  };
1300
1363
  }
1301
1364
  mapRowToCredentials(row, userId, sessionId) {
@@ -1318,42 +1381,48 @@ var NeonStorageBackend = class {
1318
1381
  const status = session.status ?? "pending";
1319
1382
  const createdAt = new Date(session.createdAt || Date.now()).toISOString();
1320
1383
  const updatedAt = new Date(session.updatedAt ?? session.createdAt ?? Date.now()).toISOString();
1321
- const expiresAt = resolveSessionExpiresAt(status, new Date(createdAt).getTime());
1384
+ const createdAtMs = new Date(createdAt).getTime();
1385
+ const expiresAt = resolveSessionExpiresAt(status, createdAtMs);
1386
+ const toolPolicy = normalizeToolPolicy(session.toolPolicy, createdAtMs);
1387
+ const columns = [
1388
+ "session_id",
1389
+ "user_id",
1390
+ "server_id",
1391
+ "server_name",
1392
+ "server_url",
1393
+ "transport_type",
1394
+ "callback_url",
1395
+ "created_at",
1396
+ "updated_at",
1397
+ "headers",
1398
+ "auth_url",
1399
+ "status",
1400
+ "expires_at"
1401
+ ];
1402
+ const values = [
1403
+ sessionId,
1404
+ userId,
1405
+ session.serverId,
1406
+ session.serverName,
1407
+ session.serverUrl,
1408
+ session.transportType,
1409
+ session.callbackUrl,
1410
+ createdAt,
1411
+ updatedAt,
1412
+ encryptObject(session.headers),
1413
+ session.authUrl ?? null,
1414
+ status,
1415
+ expiresAt === null ? null : new Date(expiresAt).toISOString()
1416
+ ];
1417
+ if (toolPolicy) {
1418
+ columns.push("tool_policy");
1419
+ values.push(toolPolicy);
1420
+ }
1421
+ const placeholders = values.map((_, i) => `$${i + 1}`);
1322
1422
  try {
1323
1423
  await this.sql.query(
1324
- `INSERT INTO ${this.tableName} (
1325
- session_id,
1326
- user_id,
1327
- server_id,
1328
- server_name,
1329
- server_url,
1330
- transport_type,
1331
- callback_url,
1332
- created_at,
1333
- updated_at,
1334
- headers,
1335
- auth_url,
1336
- status,
1337
- expires_at
1338
- ) VALUES (
1339
- $1, $2, $3, $4, $5, $6, $7, $8,
1340
- $9, $10, $11, $12, $13
1341
- )`,
1342
- [
1343
- sessionId,
1344
- userId,
1345
- session.serverId,
1346
- session.serverName,
1347
- session.serverUrl,
1348
- session.transportType,
1349
- session.callbackUrl,
1350
- createdAt,
1351
- updatedAt,
1352
- encryptObject(session.headers),
1353
- session.authUrl ?? null,
1354
- status,
1355
- expiresAt === null ? null : new Date(expiresAt).toISOString()
1356
- ]
1424
+ `INSERT INTO ${this.tableName} (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`,
1425
+ values
1357
1426
  );
1358
1427
  } catch (error) {
1359
1428
  if (error.code === "23505") {
@@ -1370,36 +1439,36 @@ var NeonStorageBackend = class {
1370
1439
  const updatedSession = { ...currentSession, ...data };
1371
1440
  const status = updatedSession.status ?? "pending";
1372
1441
  const expiresAt = resolveSessionExpiresAt(status);
1373
- 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;
1442
+ 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;
1374
1443
  if (shouldUpdateSession) {
1444
+ const setClauses = [];
1445
+ const values = [];
1446
+ let paramIndex = 1;
1447
+ const addSet = (column, value) => {
1448
+ setClauses.push(`${column} = $${paramIndex++}`);
1449
+ values.push(value);
1450
+ };
1451
+ addSet("server_id", updatedSession.serverId);
1452
+ addSet("server_name", updatedSession.serverName);
1453
+ addSet("server_url", updatedSession.serverUrl);
1454
+ addSet("transport_type", updatedSession.transportType);
1455
+ addSet("callback_url", updatedSession.callbackUrl);
1456
+ addSet("status", status);
1457
+ addSet("headers", encryptObject(updatedSession.headers));
1458
+ addSet("auth_url", updatedSession.authUrl ?? null);
1459
+ addSet("expires_at", expiresAt === null ? null : new Date(expiresAt).toISOString());
1460
+ if ("toolPolicy" in data) {
1461
+ const policyUpdatedAt = updatedSession.updatedAt ?? Date.now();
1462
+ const toolPolicy = normalizeToolPolicy(updatedSession.toolPolicy, policyUpdatedAt) ?? { mode: "all", toolIds: [], updatedAt: policyUpdatedAt };
1463
+ addSet("tool_policy", toolPolicy);
1464
+ }
1465
+ setClauses.push("updated_at = now()");
1375
1466
  const updatedRows = await this.sql.query(
1376
1467
  `UPDATE ${this.tableName}
1377
- SET
1378
- server_id = $1,
1379
- server_name = $2,
1380
- server_url = $3,
1381
- transport_type = $4,
1382
- callback_url = $5,
1383
- status = $6,
1384
- headers = $7,
1385
- auth_url = $8,
1386
- expires_at = $9,
1387
- updated_at = now()
1388
- WHERE user_id = $10 AND session_id = $11
1468
+ SET ${setClauses.join(", ")}
1469
+ WHERE user_id = $${paramIndex++} AND session_id = $${paramIndex++}
1389
1470
  RETURNING id`,
1390
- [
1391
- updatedSession.serverId,
1392
- updatedSession.serverName,
1393
- updatedSession.serverUrl,
1394
- updatedSession.transportType,
1395
- updatedSession.callbackUrl,
1396
- status,
1397
- encryptObject(updatedSession.headers),
1398
- updatedSession.authUrl ?? null,
1399
- expiresAt === null ? null : new Date(expiresAt).toISOString(),
1400
- userId,
1401
- sessionId
1402
- ]
1471
+ [...values, userId, sessionId]
1403
1472
  );
1404
1473
  if (updatedRows.length === 0) {
1405
1474
  throw new Error(`Session ${sessionId} not found for userId ${userId}`);
@@ -2314,7 +2383,15 @@ var MCPClient = class {
2314
2383
  if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
2315
2384
  this.createdAt = Date.now();
2316
2385
  const updatedAt = this.createdAt;
2317
- console.log(`[MCPClient] Creating pending session ${this.sessionId} for connection setup`);
2386
+ this._onObservabilityEvent.fire({
2387
+ type: "mcp:client:session_created",
2388
+ level: "info",
2389
+ message: `Creating pending session ${this.sessionId} for connection setup`,
2390
+ sessionId: this.sessionId,
2391
+ serverId: this.serverId,
2392
+ timestamp: Date.now(),
2393
+ id: nanoid()
2394
+ });
2318
2395
  await sessions.create({
2319
2396
  sessionId: this.sessionId,
2320
2397
  userId: this.userId,
@@ -2444,7 +2521,15 @@ var MCPClient = class {
2444
2521
  this.transportType = transportType;
2445
2522
  this.emitStateChange("CONNECTED");
2446
2523
  this.emitProgress("Connected successfully");
2447
- console.log(`[MCPClient] Saving active session ${this.sessionId} (connect success)`);
2524
+ this._onObservabilityEvent.fire({
2525
+ type: "mcp:client:session_saved",
2526
+ level: "info",
2527
+ message: `Saving active session ${this.sessionId} (connect success)`,
2528
+ sessionId: this.sessionId,
2529
+ serverId: this.serverId,
2530
+ timestamp: Date.now(),
2531
+ id: nanoid()
2532
+ });
2448
2533
  await this.saveSession("active");
2449
2534
  } catch (error) {
2450
2535
  if (error instanceof UnauthorizedError$1 || error instanceof Error && error.message.toLowerCase().includes("unauthorized")) {
@@ -2461,7 +2546,15 @@ var MCPClient = class {
2461
2546
  throw new Error(message);
2462
2547
  }
2463
2548
  this.emitStateChange("AUTHENTICATING");
2464
- console.log(`[MCPClient] Saving pending OAuth session ${this.sessionId}`);
2549
+ this._onObservabilityEvent.fire({
2550
+ type: "mcp:client:session_saved",
2551
+ level: "info",
2552
+ message: `Saving pending OAuth session ${this.sessionId}`,
2553
+ sessionId: this.sessionId,
2554
+ serverId: this.serverId,
2555
+ timestamp: Date.now(),
2556
+ id: nanoid()
2557
+ });
2465
2558
  await this.saveSession("pending");
2466
2559
  if (this.serverId) {
2467
2560
  this._onConnectionEvent.fire({
@@ -2536,27 +2629,25 @@ var MCPClient = class {
2536
2629
  this.emitStateChange("AUTHENTICATED");
2537
2630
  authenticatedStateEmitted = true;
2538
2631
  }
2539
- this.emitProgress("Creating authenticated client...");
2540
- this.client = new Client(
2541
- {
2542
- name: MCP_CLIENT_NAME,
2543
- version: MCP_CLIENT_VERSION
2544
- },
2545
- {
2546
- capabilities: {
2547
- extensions: {
2548
- "io.modelcontextprotocol/ui": {
2549
- mimeTypes: ["text/html+mcp"]
2550
- }
2551
- }
2552
- }
2553
- }
2554
- );
2555
2632
  this.emitStateChange("CONNECTING");
2633
+ if (this.client.transport) {
2634
+ try {
2635
+ await this.client.close();
2636
+ } catch {
2637
+ }
2638
+ }
2556
2639
  await this.client.connect(this.transport);
2557
2640
  this.transportType = currentType;
2558
2641
  this.emitStateChange("CONNECTED");
2559
- console.log(`[MCPClient] Saving active session ${this.sessionId} (OAuth complete)`);
2642
+ this._onObservabilityEvent.fire({
2643
+ type: "mcp:client:session_saved",
2644
+ level: "info",
2645
+ message: `Saving active session ${this.sessionId} (OAuth complete)`,
2646
+ sessionId: this.sessionId,
2647
+ serverId: this.serverId,
2648
+ timestamp: Date.now(),
2649
+ id: nanoid()
2650
+ });
2560
2651
  await this.saveSession("active");
2561
2652
  return;
2562
2653
  } catch (error) {
@@ -2591,24 +2682,28 @@ var MCPClient = class {
2591
2682
  throw lastError;
2592
2683
  }
2593
2684
  }
2685
+ /**
2686
+ * Lists all available tools from the connected MCP server without emitting discovery events.
2687
+ * Gateways use this to apply policy before publishing tools to agents or UI state.
2688
+ */
2689
+ async fetchTools() {
2690
+ const request = {
2691
+ method: "tools/list",
2692
+ params: {}
2693
+ };
2694
+ return await this.withRetry(
2695
+ () => this.client.request(request, ListToolsResultSchema)
2696
+ );
2697
+ }
2594
2698
  /**
2595
2699
  * Lists all available tools from the connected MCP server
2596
2700
  * @returns List of tools with their schemas and descriptions
2597
2701
  * @throws {Error} When client is not connected
2598
2702
  */
2599
2703
  async listTools() {
2600
- if (!this.client) {
2601
- throw new Error("Not connected to server");
2602
- }
2603
2704
  this.emitStateChange("DISCOVERING");
2604
2705
  try {
2605
- const request = {
2606
- method: "tools/list",
2607
- params: {}
2608
- };
2609
- const result = await this.withRetry(
2610
- () => this.client.request(request, ListToolsResultSchema)
2611
- );
2706
+ const result = await this.fetchTools();
2612
2707
  if (this.serverId) {
2613
2708
  this._onConnectionEvent.fire({
2614
2709
  type: "tools_discovered",
@@ -2637,9 +2732,6 @@ var MCPClient = class {
2637
2732
  * @throws {Error} When client is not connected
2638
2733
  */
2639
2734
  async callTool(toolName, toolArgs) {
2640
- if (!this.client) {
2641
- throw new Error("Not connected to server");
2642
- }
2643
2735
  const request = {
2644
2736
  method: "tools/call",
2645
2737
  params: {
@@ -2693,9 +2785,6 @@ var MCPClient = class {
2693
2785
  * @throws {Error} When client is not connected
2694
2786
  */
2695
2787
  async listPrompts() {
2696
- if (!this.client) {
2697
- throw new Error("Not connected to server");
2698
- }
2699
2788
  this.emitStateChange("DISCOVERING");
2700
2789
  try {
2701
2790
  const request = {
@@ -2723,9 +2812,6 @@ var MCPClient = class {
2723
2812
  * @throws {Error} When client is not connected
2724
2813
  */
2725
2814
  async getPrompt(name, args) {
2726
- if (!this.client) {
2727
- throw new Error("Not connected to server");
2728
- }
2729
2815
  const request = {
2730
2816
  method: "prompts/get",
2731
2817
  params: {
@@ -2743,9 +2829,6 @@ var MCPClient = class {
2743
2829
  * @throws {Error} When client is not connected
2744
2830
  */
2745
2831
  async listResources() {
2746
- if (!this.client) {
2747
- throw new Error("Not connected to server");
2748
- }
2749
2832
  this.emitStateChange("DISCOVERING");
2750
2833
  try {
2751
2834
  const request = {
@@ -2772,9 +2855,6 @@ var MCPClient = class {
2772
2855
  * @throws {Error} When client is not connected
2773
2856
  */
2774
2857
  async readResource(uri) {
2775
- if (!this.client) {
2776
- throw new Error("Not connected to server");
2777
- }
2778
2858
  const request = {
2779
2859
  method: "resources/read",
2780
2860
  params: {
@@ -2830,7 +2910,16 @@ var MCPClient = class {
2830
2910
  try {
2831
2911
  await this.ensureSession();
2832
2912
  } catch (error) {
2833
- console.warn("[MCPClient] Initialization failed during clearSession:", error);
2913
+ this._onObservabilityEvent.fire({
2914
+ type: "mcp:client:error",
2915
+ level: "warn",
2916
+ message: "Initialization failed during clearSession",
2917
+ sessionId: this.sessionId,
2918
+ serverId: this.serverId,
2919
+ payload: { error: String(error) },
2920
+ timestamp: Date.now(),
2921
+ id: nanoid()
2922
+ });
2834
2923
  }
2835
2924
  if (this.oauthProvider) {
2836
2925
  await this.oauthProvider.invalidateCredentials("all");
@@ -2941,6 +3030,66 @@ var MCPClient = class {
2941
3030
  }
2942
3031
  };
2943
3032
 
3033
+ // src/server/mcp/tool-policy-gateway.ts
3034
+ var ToolPolicyGateway = class {
3035
+ constructor(userId, sessionId, client) {
3036
+ this.userId = userId;
3037
+ this.sessionId = sessionId;
3038
+ this.client = client;
3039
+ }
3040
+ isConnected() {
3041
+ return this.client.isConnected();
3042
+ }
3043
+ getServerId() {
3044
+ return this.client.getServerId?.();
3045
+ }
3046
+ getServerName() {
3047
+ return this.client.getServerName?.();
3048
+ }
3049
+ getServerUrl() {
3050
+ return this.client.getServerUrl?.();
3051
+ }
3052
+ getSessionId() {
3053
+ return this.client.getSessionId?.() ?? this.sessionId;
3054
+ }
3055
+ async listTools() {
3056
+ const session = await this.getSession();
3057
+ const result = await this.client.fetchTools();
3058
+ const tools = this.filterTools(session, result.tools);
3059
+ return {
3060
+ ...result,
3061
+ tools
3062
+ };
3063
+ }
3064
+ async listAllTools() {
3065
+ return await this.client.fetchTools();
3066
+ }
3067
+ async callTool(name, args) {
3068
+ const session = await this.getSession();
3069
+ this.assertAllowed(session, name);
3070
+ return await this.client.callTool(name, args);
3071
+ }
3072
+ filterTools(session, tools) {
3073
+ return filterToolsByPolicy(tools, session.toolPolicy, this.getPolicyServerId(session));
3074
+ }
3075
+ assertAllowed(session, toolName) {
3076
+ assertToolAllowed(session.toolPolicy, toolName, this.getPolicyServerId(session));
3077
+ }
3078
+ async getSession() {
3079
+ const session = await sessions.get(this.userId, this.sessionId);
3080
+ if (!session) {
3081
+ throw new Error("Session not found");
3082
+ }
3083
+ return session;
3084
+ }
3085
+ getPolicyServerId(session) {
3086
+ return this.client.getServerId?.() ?? session.serverId;
3087
+ }
3088
+ };
3089
+ function createToolPolicyGateway(userId, sessionId, client) {
3090
+ return new ToolPolicyGateway(userId, sessionId, client);
3091
+ }
3092
+
2944
3093
  // src/server/mcp/multi-session-client.ts
2945
3094
  var DEFAULT_TIMEOUT_MS = 15e3;
2946
3095
  var DEFAULT_MAX_RETRIES = 2;
@@ -3006,7 +3155,9 @@ var MultiSessionClient = class {
3006
3155
  * or to route a tool call to the right client by `serverId`.
3007
3156
  */
3008
3157
  getClients() {
3009
- return this.clients;
3158
+ return this.clients.map(
3159
+ (client) => createToolPolicyGateway(this.userId, client.getSessionId(), client)
3160
+ );
3010
3161
  }
3011
3162
  /**
3012
3163
  * Removes and disconnects a single session by ID.
@@ -3241,6 +3392,12 @@ var SSEConnectionManager = class {
3241
3392
  case "listTools":
3242
3393
  result = await this.listTools(request.params);
3243
3394
  break;
3395
+ case "setToolPolicy":
3396
+ result = await this.setToolPolicy(request.params);
3397
+ break;
3398
+ case "getToolPolicy":
3399
+ result = await this.getToolPolicy(request.params);
3400
+ break;
3244
3401
  case "callTool":
3245
3402
  result = await this.callTool(request.params);
3246
3403
  break;
@@ -3297,7 +3454,8 @@ var SSEConnectionManager = class {
3297
3454
  transport: s.transportType,
3298
3455
  createdAt: s.createdAt,
3299
3456
  updatedAt: s.updatedAt ?? s.createdAt,
3300
- status: s.status ?? "pending"
3457
+ status: s.status ?? "pending",
3458
+ toolPolicy: s.toolPolicy
3301
3459
  }))
3302
3460
  };
3303
3461
  }
@@ -3307,7 +3465,7 @@ var SSEConnectionManager = class {
3307
3465
  async connect(params) {
3308
3466
  const { serverName, serverUrl, callbackUrl, transportType } = params;
3309
3467
  const headers = normalizeHeaders(params.headers);
3310
- const serverId = params.serverId && params.serverId.length <= 12 ? params.serverId : await sessions.generateSessionId();
3468
+ const serverId = params.serverId && params.serverId.length <= 12 ? params.serverId : generateServerId();
3311
3469
  const existingSessions = await sessions.list(this.userId);
3312
3470
  const duplicate = existingSessions.find(
3313
3471
  (s) => s.serverId === serverId || s.serverUrl === serverUrl
@@ -3345,7 +3503,7 @@ var SSEConnectionManager = class {
3345
3503
  this.sendEvent(event);
3346
3504
  });
3347
3505
  await client.connect();
3348
- await client.listTools();
3506
+ await this.listPolicyFilteredTools(sessionId);
3349
3507
  return {
3350
3508
  sessionId,
3351
3509
  success: true
@@ -3416,22 +3574,98 @@ var SSEConnectionManager = class {
3416
3574
  this.clients.set(sessionId, client);
3417
3575
  return client;
3418
3576
  }
3577
+ async listPolicyFilteredTools(sessionId) {
3578
+ const session = await sessions.get(this.userId, sessionId);
3579
+ if (!session) {
3580
+ throw new Error("Session not found");
3581
+ }
3582
+ const client = await this.getOrCreateClient(sessionId);
3583
+ const gateway = createToolPolicyGateway(this.userId, sessionId, client);
3584
+ const result = await gateway.listTools();
3585
+ this.emitConnectionEvent({
3586
+ type: "tools_discovered",
3587
+ sessionId,
3588
+ serverId: session.serverId ?? "unknown",
3589
+ toolCount: result.tools.length,
3590
+ tools: result.tools,
3591
+ timestamp: Date.now()
3592
+ });
3593
+ return { session, result };
3594
+ }
3419
3595
  /**
3420
3596
  * List tools from a session
3421
3597
  */
3422
3598
  async listTools(params) {
3423
3599
  const { sessionId } = params;
3424
- const client = await this.getOrCreateClient(sessionId);
3425
- const result = await client.listTools();
3600
+ const { result } = await this.listPolicyFilteredTools(sessionId);
3426
3601
  return { tools: result.tools };
3427
3602
  }
3603
+ /**
3604
+ * Get all raw tools with effective access state for management UI.
3605
+ */
3606
+ async getToolPolicy(params) {
3607
+ const { sessionId } = params;
3608
+ const session = await sessions.get(this.userId, sessionId);
3609
+ if (!session) {
3610
+ throw new Error("Session not found");
3611
+ }
3612
+ const client = await this.getOrCreateClient(sessionId);
3613
+ const allTools = await client.fetchTools();
3614
+ const toolPolicy = session.toolPolicy ?? {
3615
+ mode: "all",
3616
+ toolIds: [],
3617
+ updatedAt: session.updatedAt ?? session.createdAt
3618
+ };
3619
+ const serverId = session.serverId ?? "unknown";
3620
+ const tools = allTools.tools.map((tool) => ({
3621
+ ...tool,
3622
+ toolId: createToolId(serverId, tool.name),
3623
+ allowed: isToolAllowed(toolPolicy, tool.name, session.serverId)
3624
+ }));
3625
+ return {
3626
+ toolPolicy,
3627
+ tools,
3628
+ toolCount: tools.length,
3629
+ allowedToolCount: tools.filter((tool) => tool.allowed).length
3630
+ };
3631
+ }
3632
+ /**
3633
+ * Update per-session tool access policy.
3634
+ */
3635
+ async setToolPolicy(params) {
3636
+ const { sessionId } = params;
3637
+ const session = await sessions.get(this.userId, sessionId);
3638
+ if (!session) {
3639
+ throw new Error("Session not found");
3640
+ }
3641
+ const client = await this.getOrCreateClient(sessionId);
3642
+ const allTools = await client.fetchTools();
3643
+ const toolPolicy = normalizeToolPolicyForUpdate(params.toolPolicy);
3644
+ validateToolPolicyAgainstTools(toolPolicy, allTools.tools, session.serverId);
3645
+ await sessions.update(this.userId, sessionId, { toolPolicy });
3646
+ const filteredTools = createToolPolicyGateway(this.userId, sessionId, client).filterTools({ ...session, toolPolicy }, allTools.tools);
3647
+ this.emitConnectionEvent({
3648
+ type: "tools_discovered",
3649
+ sessionId,
3650
+ serverId: session.serverId ?? "unknown",
3651
+ toolCount: filteredTools.length,
3652
+ tools: filteredTools,
3653
+ timestamp: Date.now()
3654
+ });
3655
+ return {
3656
+ success: true,
3657
+ toolPolicy,
3658
+ tools: filteredTools,
3659
+ toolCount: filteredTools.length
3660
+ };
3661
+ }
3428
3662
  /**
3429
3663
  * Call a tool on the MCP server
3430
3664
  */
3431
3665
  async callTool(params) {
3432
3666
  const { sessionId, toolName, toolArgs } = params;
3433
3667
  const client = await this.getOrCreateClient(sessionId);
3434
- const result = await client.callTool(toolName, toolArgs);
3668
+ const result = await createToolPolicyGateway(this.userId, sessionId, client).callTool(toolName, toolArgs);
3435
3669
  const meta = result._meta || {};
3436
3670
  return {
3437
3671
  ...result,
@@ -3481,7 +3715,7 @@ var SSEConnectionManager = class {
3481
3715
  client.onObservabilityEvent((event) => this.sendEvent(event));
3482
3716
  await client.connect();
3483
3717
  this.clients.set(sessionId, client);
3484
- const tools = await client.listTools();
3718
+ const { result: tools } = await this.listPolicyFilteredTools(sessionId);
3485
3719
  return { success: true, toolCount: tools.tools.length };
3486
3720
  } catch (error) {
3487
3721
  this.emitConnectionEvent({
@@ -3529,7 +3763,7 @@ var SSEConnectionManager = class {
3529
3763
  client.onConnectionEvent((event) => this.emitConnectionEvent(event));
3530
3764
  await client.finishAuth(code, oauthState);
3531
3765
  this.clients.set(sessionId, client);
3532
- const tools = await client.listTools();
3766
+ const { result: tools } = await this.listPolicyFilteredTools(sessionId);
3533
3767
  return { success: true, toolCount: tools.tools.length };
3534
3768
  } catch (error) {
3535
3769
  this.emitConnectionEvent({