@samanhappy/mcphub 1.0.10 → 1.0.11

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 (105) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/commands/tools.js +4 -0
  3. package/dist/cli/commands/tools.js.map +1 -1
  4. package/dist/controllers/contextCostController.js +24 -0
  5. package/dist/controllers/contextCostController.js.map +1 -0
  6. package/dist/controllers/discoveryController.js +203 -0
  7. package/dist/controllers/discoveryController.js.map +1 -0
  8. package/dist/controllers/oauthCallbackController.js +3 -8
  9. package/dist/controllers/oauthCallbackController.js.map +1 -1
  10. package/dist/dao/SystemConfigDaoDbImpl.js +3 -0
  11. package/dist/dao/SystemConfigDaoDbImpl.js.map +1 -1
  12. package/dist/db/entities/SystemConfig.js +4 -0
  13. package/dist/db/entities/SystemConfig.js.map +1 -1
  14. package/dist/db/repositories/SystemConfigRepository.js +1 -0
  15. package/dist/db/repositories/SystemConfigRepository.js.map +1 -1
  16. package/dist/routes/index.js +16 -0
  17. package/dist/routes/index.js.map +1 -1
  18. package/dist/services/contextCostService.js +101 -0
  19. package/dist/services/contextCostService.js.map +1 -0
  20. package/dist/services/groupService.js +1 -1
  21. package/dist/services/groupService.js.map +1 -1
  22. package/dist/services/hostedRuntimeCatalogService.js +2 -1
  23. package/dist/services/hostedRuntimeCatalogService.js.map +1 -1
  24. package/dist/services/mcpService.js +243 -98
  25. package/dist/services/mcpService.js.map +1 -1
  26. package/dist/services/openApiGeneratorService.js +5 -4
  27. package/dist/services/openApiGeneratorService.js.map +1 -1
  28. package/dist/services/smartRoutingService.js +53 -30
  29. package/dist/services/smartRoutingService.js.map +1 -1
  30. package/dist/services/vectorSearchService.js +3 -1
  31. package/dist/services/vectorSearchService.js.map +1 -1
  32. package/dist/utils/mcpApps.js +43 -0
  33. package/dist/utils/mcpApps.js.map +1 -0
  34. package/dist/utils/migration.js +1 -0
  35. package/dist/utils/migration.js.map +1 -1
  36. package/dist/utils/serialization.js +17 -1
  37. package/dist/utils/serialization.js.map +1 -1
  38. package/dist/utils/tokenCost.js +71 -0
  39. package/dist/utils/tokenCost.js.map +1 -0
  40. package/frontend/dist/assets/{ActivityPage-RTpGLfzM.js → ActivityPage-DY-oWiYy.js} +2 -2
  41. package/frontend/dist/assets/{ActivityPage-RTpGLfzM.js.map → ActivityPage-DY-oWiYy.js.map} +1 -1
  42. package/frontend/dist/assets/{ConfirmDialog-CxlizGia.js → ConfirmDialog-Cag_haxr.js} +2 -2
  43. package/frontend/dist/assets/{ConfirmDialog-CxlizGia.js.map → ConfirmDialog-Cag_haxr.js.map} +1 -1
  44. package/frontend/dist/assets/Dashboard-DraIR5Hs.js +2 -0
  45. package/frontend/dist/assets/Dashboard-DraIR5Hs.js.map +1 -0
  46. package/frontend/dist/assets/{DeleteDialog-DRbWonMu.js → DeleteDialog-BBfJpiiD.js} +2 -2
  47. package/frontend/dist/assets/{DeleteDialog-DRbWonMu.js.map → DeleteDialog-BBfJpiiD.js.map} +1 -1
  48. package/frontend/dist/assets/{EndpointCopy-BtPORuga.js → EndpointCopy-Bkkaa0MC.js} +2 -2
  49. package/frontend/dist/assets/{EndpointCopy-BtPORuga.js.map → EndpointCopy-Bkkaa0MC.js.map} +1 -1
  50. package/frontend/dist/assets/GroupsPage-CTFtynaX.js +33 -0
  51. package/frontend/dist/assets/GroupsPage-CTFtynaX.js.map +1 -0
  52. package/frontend/dist/assets/{LoginPage-Nks21Yj0.js → LoginPage-BeZ9wg33.js} +2 -2
  53. package/frontend/dist/assets/{LoginPage-Nks21Yj0.js.map → LoginPage-BeZ9wg33.js.map} +1 -1
  54. package/frontend/dist/assets/{LogsPage-CoupcpK1.js → LogsPage-D5LqNWlN.js} +2 -2
  55. package/frontend/dist/assets/LogsPage-D5LqNWlN.js.map +1 -0
  56. package/frontend/dist/assets/{MarketPage-PwkvXlle.js → MarketPage-DmtOnQVN.js} +2 -2
  57. package/frontend/dist/assets/{MarketPage-PwkvXlle.js.map → MarketPage-DmtOnQVN.js.map} +1 -1
  58. package/frontend/dist/assets/{Pagination-BFi-X7qY.js → Pagination-DBAu79mv.js} +2 -2
  59. package/frontend/dist/assets/{Pagination-BFi-X7qY.js.map → Pagination-DBAu79mv.js.map} +1 -1
  60. package/frontend/dist/assets/{PromptsPage-DQiX0bAK.js → PromptsPage-blyfeDf6.js} +2 -2
  61. package/frontend/dist/assets/{PromptsPage-DQiX0bAK.js.map → PromptsPage-blyfeDf6.js.map} +1 -1
  62. package/frontend/dist/assets/{ResourcesPage-Dltmu_Ze.js → ResourcesPage-3gvaBRAN.js} +2 -2
  63. package/frontend/dist/assets/{ResourcesPage-Dltmu_Ze.js.map → ResourcesPage-3gvaBRAN.js.map} +1 -1
  64. package/frontend/dist/assets/ServersPage-WhPsI-wN.js +37 -0
  65. package/frontend/dist/assets/ServersPage-WhPsI-wN.js.map +1 -0
  66. package/frontend/dist/assets/{SettingsPage-BbRB2fCw.js → SettingsPage-Dg2-dPPZ.js} +2 -2
  67. package/frontend/dist/assets/{SettingsPage-BbRB2fCw.js.map → SettingsPage-Dg2-dPPZ.js.map} +1 -1
  68. package/frontend/dist/assets/{StatusDot-BRb7WUcz.js → StatusDot-D3_AIDzm.js} +2 -2
  69. package/frontend/dist/assets/{StatusDot-BRb7WUcz.js.map → StatusDot-D3_AIDzm.js.map} +1 -1
  70. package/frontend/dist/assets/{ToggleGroup-tIqxkae6.js → ToggleGroup-DU1_5iss.js} +2 -2
  71. package/frontend/dist/assets/{ToggleGroup-tIqxkae6.js.map → ToggleGroup-DU1_5iss.js.map} +1 -1
  72. package/frontend/dist/assets/{UsersPage-DNMaUcUn.js → UsersPage-Cwi4XfTK.js} +2 -2
  73. package/frontend/dist/assets/{UsersPage-DNMaUcUn.js.map → UsersPage-Cwi4XfTK.js.map} +1 -1
  74. package/frontend/dist/assets/contextCost-qVdQkBuO.js +2 -0
  75. package/frontend/dist/assets/contextCost-qVdQkBuO.js.map +1 -0
  76. package/frontend/dist/assets/{framework-vendor-BUhDPOUZ.js → framework-vendor-DeqnZ0v6.js} +5 -5
  77. package/frontend/dist/assets/{framework-vendor-BUhDPOUZ.js.map → framework-vendor-DeqnZ0v6.js.map} +1 -1
  78. package/frontend/dist/assets/i18n-vendor-DP1IRITP.js +10 -0
  79. package/frontend/dist/assets/i18n-vendor-DP1IRITP.js.map +1 -0
  80. package/frontend/dist/assets/{icons-vendor-CKgJB3SC.js → icons-vendor-IYX_ppgH.js} +2 -2
  81. package/frontend/dist/assets/{icons-vendor-CKgJB3SC.js.map → icons-vendor-IYX_ppgH.js.map} +1 -1
  82. package/frontend/dist/assets/index-B-q7kaxJ.js +3 -0
  83. package/frontend/dist/assets/{index-C6LZBP3G.js.map → index-B-q7kaxJ.js.map} +1 -1
  84. package/frontend/dist/assets/{index-B9E3ygVt.css → index-D8V2fxz_.css} +1 -1
  85. package/frontend/dist/assets/{resourceService-BuhqcwTn.js → resourceService-DvnBlx1V.js} +2 -2
  86. package/frontend/dist/assets/{resourceService-BuhqcwTn.js.map → resourceService-DvnBlx1V.js.map} +1 -1
  87. package/frontend/dist/assets/useSettingsData-Dc1fQY2w.js +2 -0
  88. package/frontend/dist/assets/{useSettingsData-iQpBXEjX.js.map → useSettingsData-Dc1fQY2w.js.map} +1 -1
  89. package/frontend/dist/assets/{variableDetection-GTKqOf1F.js → variableDetection-Dnh8qFpO.js} +2 -2
  90. package/frontend/dist/assets/{variableDetection-GTKqOf1F.js.map → variableDetection-Dnh8qFpO.js.map} +1 -1
  91. package/frontend/dist/index.html +5 -5
  92. package/package.json +7 -7
  93. package/frontend/dist/assets/Dashboard-KV-zezVb.js +0 -2
  94. package/frontend/dist/assets/Dashboard-KV-zezVb.js.map +0 -1
  95. package/frontend/dist/assets/GroupsPage-BhNYw74w.js +0 -33
  96. package/frontend/dist/assets/GroupsPage-BhNYw74w.js.map +0 -1
  97. package/frontend/dist/assets/LogsPage-CoupcpK1.js.map +0 -1
  98. package/frontend/dist/assets/ServersPage-CcbF8dGi.js +0 -37
  99. package/frontend/dist/assets/ServersPage-CcbF8dGi.js.map +0 -1
  100. package/frontend/dist/assets/i18n-vendor-Kbr87Ofu.js +0 -2
  101. package/frontend/dist/assets/i18n-vendor-Kbr87Ofu.js.map +0 -1
  102. package/frontend/dist/assets/index-C6LZBP3G.js +0 -3
  103. package/frontend/dist/assets/useServerData-C9R1u5V4.js +0 -2
  104. package/frontend/dist/assets/useServerData-C9R1u5V4.js.map +0 -1
  105. package/frontend/dist/assets/useSettingsData-iQpBXEjX.js +0 -2
@@ -24,6 +24,7 @@ import { initSmartRoutingService, getSmartRoutingTools, handleSearchToolsRequest
24
24
  import { getActivityLoggingService } from './activityLoggingService.js';
25
25
  import { assertHostedToolAllowed, filterHostedTools, reserveHostedToolCall, settleHostedToolCall, } from './hostedAuthService.js';
26
26
  import { formatErrorForLogging, sanitizeStringForLogging, summarizeErrorForLogging, } from '../utils/serialization.js';
27
+ import { MCP_APPS_CAPABILITIES, filterModelVisibleTools, hasMcpAppsCapability, isAppOnlyTool, stripMcpAppsMetadata, } from '../utils/mcpApps.js';
27
28
  const servers = {};
28
29
  import { setupClientKeepAlive } from './keepAliveService.js';
29
30
  /**
@@ -194,18 +195,26 @@ export const notifyToolChanged = async (name, options) => {
194
195
  await registerAllTools(false, name, options);
195
196
  broadcastToolListChanged();
196
197
  };
197
- export const broadcastToolListChanged = () => {
198
+ const broadcastListChanged = (listType, sendNotification) => {
198
199
  Object.values(servers).forEach((server) => {
199
- server
200
- .sendToolListChanged()
201
- .catch((error) => {
202
- console.warn('Failed to send tool list changed notification:', error.message);
203
- })
200
+ sendNotification(server)
204
201
  .then(() => {
205
- console.log('Tool list changed notification sent successfully');
202
+ console.log(`${listType} list changed notification sent successfully`);
203
+ })
204
+ .catch((error) => {
205
+ console.warn(`Failed to send ${listType} list changed notification:`, error.message);
206
206
  });
207
207
  });
208
208
  };
209
+ export const broadcastToolListChanged = () => {
210
+ broadcastListChanged('tool', (server) => server.sendToolListChanged());
211
+ };
212
+ export const broadcastResourceListChanged = () => {
213
+ broadcastListChanged('resource', (server) => server.sendResourceListChanged());
214
+ };
215
+ export const broadcastPromptListChanged = () => {
216
+ broadcastListChanged('prompt', (server) => server.sendPromptListChanged());
217
+ };
209
218
  export const updateServerInfoVisibility = (serverName, visibility) => {
210
219
  const serverInfo = getServerByName(serverName);
211
220
  if (!serverInfo) {
@@ -230,8 +239,11 @@ export const syncToolEmbedding = async (serverName, toolName) => {
230
239
  console.warn(`Tool not found: ${toolName} on server: ${serverName}`);
231
240
  return;
232
241
  }
242
+ if (isAppOnlyTool(tool)) {
243
+ return;
244
+ }
233
245
  // Save tool as vector embedding for search
234
- saveToolsAsVectorEmbeddings(serverName, [tool]).catch((error) => {
246
+ syncToolsAsVectorEmbeddings(serverName, [tool]).catch((error) => {
235
247
  console.warn(`[EMBED_SYNC_ERROR] Failed to sync embedding for tool "${toolName}" on server "${serverName}"`);
236
248
  console.error('Error syncing single tool embedding', { serverName, toolName, error });
237
249
  });
@@ -245,26 +257,122 @@ const cleanInputSchema = (schema) => {
245
257
  delete cleanedSchema.$schema;
246
258
  return cleanedSchema;
247
259
  };
260
+ export const normalizeToolForCache = (serverName, tool) => {
261
+ return {
262
+ ...tool,
263
+ name: `${serverName}${getNameSeparator()}${tool.name}`,
264
+ description: tool.description || '',
265
+ inputSchema: cleanInputSchema(tool.inputSchema || {}),
266
+ };
267
+ };
268
+ const syncToolsAsVectorEmbeddings = async (serverName, tools, options) => {
269
+ const modelVisibleTools = filterModelVisibleTools(tools);
270
+ if (modelVisibleTools.length === 0) {
271
+ await removeServerToolEmbeddings(serverName);
272
+ return;
273
+ }
274
+ await saveToolsAsVectorEmbeddings(serverName, modelVisibleTools, options);
275
+ };
248
276
  // Normalize prompt payload to satisfy MCP ListPrompts response schema
249
277
  const normalizePromptForList = (prompt) => {
250
278
  return {
279
+ ...prompt,
251
280
  name: prompt.name,
252
281
  title: prompt.title || prompt.name,
253
282
  description: prompt.description || '',
254
283
  arguments: Array.isArray(prompt.arguments) ? prompt.arguments : [],
255
284
  };
256
285
  };
286
+ const normalizePromptForCache = (serverName, prompt) => {
287
+ return normalizePromptForList({
288
+ ...prompt,
289
+ name: `${serverName}${getNameSeparator()}${prompt.name}`,
290
+ });
291
+ };
257
292
  // Normalize resource payload to avoid nullable DB fields violating MCP schema
258
293
  const normalizeResourceForList = (resource) => {
259
294
  return {
295
+ ...resource,
260
296
  uri: resource.uri,
261
297
  name: resource.name || '',
262
298
  description: resource.description || '',
263
299
  mimeType: resource.mimeType || '',
264
300
  };
265
301
  };
302
+ const normalizeResourceForCache = (resource) => {
303
+ return normalizeResourceForList(resource);
304
+ };
266
305
  // Store all server information
267
306
  let serverInfos = [];
307
+ export const updateServerToolsCache = (serverInfo, tools, options) => {
308
+ serverInfo.tools = tools.map((tool) => normalizeToolForCache(serverInfo.name, tool));
309
+ syncToolsAsVectorEmbeddings(serverInfo.name, serverInfo.tools, {
310
+ reportProgress: options?.reportEmbeddingProgress === true,
311
+ }).catch(() => {
312
+ console.warn('[EMBED_SYNC_ERROR] Failed to sync tool embeddings');
313
+ });
314
+ };
315
+ const updateServerPromptsCache = (serverInfo, prompts) => {
316
+ serverInfo.prompts = prompts.map((prompt) => normalizePromptForCache(serverInfo.name, prompt));
317
+ };
318
+ const updateServerResourcesCache = (serverInfo, resources) => {
319
+ serverInfo.resources = resources.map(normalizeResourceForCache);
320
+ };
321
+ const logListChangedRefreshError = (listType) => {
322
+ console.warn(`Failed to refresh ${listType} list after upstream notification`);
323
+ };
324
+ const createUpstreamMcpClient = (name, getServerInfo) => {
325
+ return new Client({
326
+ name: `mcp-client-${name}`,
327
+ version: '1.0.0',
328
+ }, {
329
+ capabilities: MCP_APPS_CAPABILITIES,
330
+ listChanged: {
331
+ tools: {
332
+ onChanged: (error, tools) => {
333
+ const serverInfo = getServerInfo();
334
+ if (error) {
335
+ logListChangedRefreshError('tool');
336
+ return;
337
+ }
338
+ if (!serverInfo || !tools) {
339
+ return;
340
+ }
341
+ updateServerToolsCache(serverInfo, tools);
342
+ broadcastToolListChanged();
343
+ },
344
+ },
345
+ prompts: {
346
+ onChanged: (error, prompts) => {
347
+ const serverInfo = getServerInfo();
348
+ if (error) {
349
+ logListChangedRefreshError('prompt');
350
+ return;
351
+ }
352
+ if (!serverInfo || !prompts) {
353
+ return;
354
+ }
355
+ updateServerPromptsCache(serverInfo, prompts);
356
+ broadcastPromptListChanged();
357
+ },
358
+ },
359
+ resources: {
360
+ onChanged: (error, resources) => {
361
+ const serverInfo = getServerInfo();
362
+ if (error) {
363
+ logListChangedRefreshError('resource');
364
+ return;
365
+ }
366
+ if (!serverInfo || !resources) {
367
+ return;
368
+ }
369
+ updateServerResourcesCache(serverInfo, resources);
370
+ broadcastResourceListChanged();
371
+ },
372
+ },
373
+ },
374
+ });
375
+ };
268
376
  // Normalize and infer server type for safe client display
269
377
  const normalizeServerType = (type) => {
270
378
  if (!type)
@@ -679,12 +787,7 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
679
787
  // Recreate transport using helper function
680
788
  const newTransport = await createTransportFromConfig(serverInfo.name, server);
681
789
  // Create new client
682
- const client = new Client({
683
- name: `mcp-client-${serverInfo.name}`,
684
- version: '1.0.0',
685
- }, {
686
- capabilities: {},
687
- });
790
+ const client = createUpstreamMcpClient(serverInfo.name, () => serverInfo);
688
791
  // Reconnect with new transport
689
792
  await client.connect(newTransport, serverInfo.options || {});
690
793
  // Update server info with new client and transport
@@ -694,19 +797,7 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
694
797
  // Reload tools list after reconnection
695
798
  try {
696
799
  const tools = await client.listTools({}, serverInfo.options || {});
697
- serverInfo.tools = tools.tools.map((tool) => ({
698
- name: `${serverInfo.name}${getNameSeparator()}${tool.name}`,
699
- description: tool.description || '',
700
- inputSchema: cleanInputSchema(tool.inputSchema || {}),
701
- }));
702
- // Save tools as vector embeddings for search
703
- saveToolsAsVectorEmbeddings(serverInfo.name, serverInfo.tools).catch((error) => {
704
- console.warn(`[EMBED_SYNC_ERROR] Failed to sync tool embeddings after reconnect for server "${serverInfo.name}"`);
705
- console.error('Error syncing tool embeddings after reconnect', {
706
- serverName: serverInfo.name,
707
- error: summarizeErrorForLogging(error),
708
- });
709
- });
800
+ updateServerToolsCache(serverInfo, tools.tools);
710
801
  }
711
802
  catch (listToolsError) {
712
803
  console.warn('Failed to reload tools after reconnection', {
@@ -850,7 +941,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
850
941
  serverInfo.openApiClient = openApiClient;
851
942
  console.log(`Successfully initialized OpenAPI server: ${name} with ${mcpTools.length} tools`);
852
943
  // Save tools as vector embeddings for search
853
- saveToolsAsVectorEmbeddings(name, mcpTools, {
944
+ syncToolsAsVectorEmbeddings(name, mcpTools, {
854
945
  reportProgress: options?.reportEmbeddingProgress === true && serverName === name,
855
946
  }).catch((error) => {
856
947
  console.warn(`[EMBED_SYNC_ERROR] Failed to sync OpenAPI embeddings for server "${name}"`);
@@ -875,12 +966,8 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
875
966
  else {
876
967
  transport = await createTransportFromConfig(name, expandedConf);
877
968
  }
878
- const client = new Client({
879
- name: `mcp-client-${name}`,
880
- version: '1.0.0',
881
- }, {
882
- capabilities: {},
883
- });
969
+ const serverInfoRef = {};
970
+ const client = createUpstreamMcpClient(name, () => serverInfoRef.current);
884
971
  // Get request options from server configuration, with fallbacks
885
972
  const serverRequestOptions = expandedConf.options || {};
886
973
  const defaultRequestTimeout = Number(process.env.DEFAULT_REQUEST_TIMEOUT) || 60000;
@@ -911,6 +998,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
911
998
  createTime: Date.now(),
912
999
  config: expandedConf, // Store reference to expanded config
913
1000
  };
1001
+ serverInfoRef.current = serverInfo;
914
1002
  const pendingAuth = expandedConf.oauth?.pendingAuthorization;
915
1003
  if (pendingAuth) {
916
1004
  serverInfo.status = 'oauth_required';
@@ -937,20 +1025,8 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
937
1025
  .listTools({}, initRequestOptions || requestOptions)
938
1026
  .then((tools) => {
939
1027
  console.log(`Successfully listed ${tools.tools.length} tools for server: ${name}`);
940
- serverInfo.tools = tools.tools.map((tool) => ({
941
- name: `${name}${getNameSeparator()}${tool.name}`,
942
- description: tool.description || '',
943
- inputSchema: cleanInputSchema(tool.inputSchema || {}),
944
- }));
945
- // Save tools as vector embeddings for search
946
- saveToolsAsVectorEmbeddings(name, serverInfo.tools, {
947
- reportProgress: options?.reportEmbeddingProgress === true && serverName === name,
948
- }).catch((embeddingError) => {
949
- console.warn(`[EMBED_SYNC_ERROR] Failed to sync tool embeddings for connected server "${name}"`);
950
- console.error('Error syncing tool embeddings for connected server', {
951
- serverName: name,
952
- error: summarizeErrorForLogging(embeddingError),
953
- });
1028
+ updateServerToolsCache(serverInfo, tools.tools, {
1029
+ reportEmbeddingProgress: options?.reportEmbeddingProgress === true && serverName === name,
954
1030
  });
955
1031
  })
956
1032
  .catch((error) => {
@@ -966,12 +1042,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
966
1042
  .listPrompts({}, initRequestOptions || requestOptions)
967
1043
  .then((prompts) => {
968
1044
  console.log(`Successfully listed ${prompts.prompts.length} prompts for server: ${name}`);
969
- serverInfo.prompts = prompts.prompts.map((prompt) => normalizePromptForList({
970
- name: `${name}${getNameSeparator()}${prompt.name}`,
971
- title: prompt.title,
972
- description: prompt.description,
973
- arguments: prompt.arguments,
974
- }));
1045
+ updateServerPromptsCache(serverInfo, prompts.prompts);
975
1046
  })
976
1047
  .catch((error) => {
977
1048
  console.error('Failed to list prompts for server', {
@@ -986,12 +1057,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
986
1057
  .listResources({}, initRequestOptions || requestOptions)
987
1058
  .then((resources) => {
988
1059
  console.log(`Successfully listed ${resources.resources.length} resources for server: ${name}`);
989
- serverInfo.resources = resources.resources.map((resource) => normalizeResourceForList({
990
- uri: resource.uri,
991
- name: resource.name,
992
- description: resource.description,
993
- mimeType: resource.mimeType,
994
- }));
1060
+ updateServerResourcesCache(serverInfo, resources.resources);
995
1061
  })
996
1062
  .catch((error) => {
997
1063
  console.error('Failed to list resources for server', {
@@ -1360,6 +1426,48 @@ export const toggleServerStatus = async (name, enabled) => {
1360
1426
  return { success: false, message: 'Failed to toggle server status' };
1361
1427
  }
1362
1428
  };
1429
+ const getMcpAppsRouteContext = async (sessionId, group) => {
1430
+ if (!sessionId ||
1431
+ isSmartRoutingGroup(group) ||
1432
+ !hasMcpAppsCapability(servers[sessionId]?.getClientCapabilities())) {
1433
+ return { enabled: false };
1434
+ }
1435
+ const { filteredServerInfos } = await getFilteredServerInfosForGroup(group);
1436
+ if (filteredServerInfos.length !== 1 ||
1437
+ filteredServerInfos[0].status !== 'connected' ||
1438
+ !filteredServerInfos[0].client) {
1439
+ return { enabled: false };
1440
+ }
1441
+ return {
1442
+ enabled: true,
1443
+ serverInfo: filteredServerInfos[0],
1444
+ };
1445
+ };
1446
+ const normalizeToolNameForServer = (serverName, toolName) => {
1447
+ const prefix = `${serverName}${getNameSeparator()}`;
1448
+ return toolName.startsWith(prefix) ? toolName.substring(prefix.length) : toolName;
1449
+ };
1450
+ const findToolOnServer = (serverInfo, toolName, allowRawName) => {
1451
+ return serverInfo.tools.find((tool) => tool.name === toolName ||
1452
+ (allowRawName && normalizeToolNameForServer(serverInfo.name, tool.name) === toolName));
1453
+ };
1454
+ const assertToolAvailableForRoute = (tool, appsRouteContext) => {
1455
+ if (isAppOnlyTool(tool) && !appsRouteContext.enabled) {
1456
+ throw new Error(`Tool '${tool.name}' is only available to MCP Apps`);
1457
+ }
1458
+ };
1459
+ const projectToolForDownstream = (serverName, tool, appsRouteContext) => {
1460
+ if (!appsRouteContext.enabled && isAppOnlyTool(tool)) {
1461
+ return undefined;
1462
+ }
1463
+ const projectedTool = appsRouteContext.enabled ? tool : stripMcpAppsMetadata(tool);
1464
+ return {
1465
+ ...projectedTool,
1466
+ name: appsRouteContext.enabled
1467
+ ? normalizeToolNameForServer(serverName, projectedTool.name)
1468
+ : projectedTool.name,
1469
+ };
1470
+ };
1363
1471
  export const handleListToolsRequest = async (_, extra) => {
1364
1472
  const sessionId = extra.sessionId || '';
1365
1473
  const group = getGroup(sessionId);
@@ -1370,6 +1478,7 @@ export const handleListToolsRequest = async (_, extra) => {
1370
1478
  return getSmartRoutingTools(group);
1371
1479
  }
1372
1480
  const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(group);
1481
+ const appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
1373
1482
  const allTools = [];
1374
1483
  for (const serverInfo of filteredServerInfos) {
1375
1484
  if (serverInfo.tools && serverInfo.tools.length > 0) {
@@ -1386,7 +1495,10 @@ export const handleListToolsRequest = async (_, extra) => {
1386
1495
  description: toolConfig?.description || tool.description, // Use custom description if available
1387
1496
  };
1388
1497
  });
1389
- allTools.push(...toolsWithCustomDescriptions);
1498
+ allTools.push(...toolsWithCustomDescriptions.flatMap((tool) => {
1499
+ const projectedTool = projectToolForDownstream(serverInfo.name, tool, appsRouteContext);
1500
+ return projectedTool ? [projectedTool] : [];
1501
+ }));
1390
1502
  }
1391
1503
  }
1392
1504
  return {
@@ -1405,6 +1517,7 @@ export const handleCallToolRequest = async (request, extra) => {
1405
1517
  // Fallback to extra for backward compatibility (e.g., direct API calls)
1406
1518
  const group = requestContextService.getGroupContext() || extra?.group || getGroup(sessionId) || undefined;
1407
1519
  const username = requestContextService.getUsernameContext() || extra?.username || undefined;
1520
+ let appsRouteContext = { enabled: false };
1408
1521
  const keyId = bearerKeyContext.keyId || extra?.keyId || undefined;
1409
1522
  const keyName = bearerKeyContext.keyName || extra?.keyName || undefined;
1410
1523
  const sourceIp = requestContextService.getRequestContext()?.remoteAddress || undefined;
@@ -1425,6 +1538,7 @@ export const handleCallToolRequest = async (request, extra) => {
1425
1538
  });
1426
1539
  };
1427
1540
  try {
1541
+ appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
1428
1542
  // Special handling for smart routing tools
1429
1543
  if (request.params.name === 'search_tools') {
1430
1544
  const { query, limit = 10 } = request.params.arguments || {};
@@ -1443,7 +1557,10 @@ export const handleCallToolRequest = async (request, extra) => {
1443
1557
  }
1444
1558
  const { arguments: toolArgs } = request.params.arguments || {};
1445
1559
  let targetServerInfo;
1446
- if (extra && extra.server) {
1560
+ if (appsRouteContext.enabled) {
1561
+ targetServerInfo = appsRouteContext.serverInfo;
1562
+ }
1563
+ else if (extra && extra.server) {
1447
1564
  targetServerInfo = getServerByName(extra.server);
1448
1565
  }
1449
1566
  else {
@@ -1456,10 +1573,11 @@ export const handleCallToolRequest = async (request, extra) => {
1456
1573
  throw new Error(`No available servers found with tool: ${toolName}`);
1457
1574
  }
1458
1575
  // Check if the tool exists on the server
1459
- const toolExists = targetServerInfo.tools.some((tool) => tool.name === toolName);
1460
- if (!toolExists) {
1576
+ const tool = findToolOnServer(targetServerInfo, toolName, appsRouteContext.enabled);
1577
+ if (!tool) {
1461
1578
  throw new Error(`Tool '${toolName}' not found on server '${targetServerInfo.name}'`);
1462
1579
  }
1580
+ assertToolAvailableForRoute(tool, appsRouteContext);
1463
1581
  // Handle OpenAPI servers differently
1464
1582
  if (targetServerInfo.openApiClient) {
1465
1583
  // For OpenAPI servers, use the OpenAPI client
@@ -1472,11 +1590,7 @@ export const handleCallToolRequest = async (request, extra) => {
1472
1590
  arguments: summarizeArgumentsForLogging(finalArgs),
1473
1591
  });
1474
1592
  // Remove server prefix from tool name if present
1475
- const separator = getNameSeparator();
1476
- const prefix = `${targetServerInfo.name}${separator}`;
1477
- const cleanToolName = toolName.startsWith(prefix)
1478
- ? toolName.substring(prefix.length)
1479
- : toolName;
1593
+ const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, toolName);
1480
1594
  // Extract passthrough headers from extra or request context
1481
1595
  let passthroughHeaders;
1482
1596
  let requestHeaders = null;
@@ -1549,11 +1663,7 @@ export const handleCallToolRequest = async (request, extra) => {
1549
1663
  serverName: targetServerInfo.name,
1550
1664
  arguments: summarizeArgumentsForLogging(finalArgs),
1551
1665
  });
1552
- const separator = getNameSeparator();
1553
- const prefix = `${targetServerInfo.name}${separator}`;
1554
- const cleanToolName = toolName.startsWith(prefix)
1555
- ? toolName.substring(prefix.length)
1556
- : toolName;
1666
+ const cleanToolName = normalizeToolNameForServer(targetServerInfo.name, toolName);
1557
1667
  await reserveHostedIfNeeded(targetServerInfo.name, cleanToolName);
1558
1668
  const result = await callToolWithReconnect(targetServerInfo, {
1559
1669
  name: cleanToolName,
@@ -1588,20 +1698,22 @@ export const handleCallToolRequest = async (request, extra) => {
1588
1698
  return result;
1589
1699
  }
1590
1700
  // Regular tool handling
1591
- const serverInfo = getServerByTool(request.params.name);
1592
- if (!serverInfo) {
1701
+ const serverInfo = appsRouteContext.enabled
1702
+ ? appsRouteContext.serverInfo
1703
+ : getServerByTool(request.params.name);
1704
+ const tool = serverInfo
1705
+ ? findToolOnServer(serverInfo, request.params.name, appsRouteContext.enabled)
1706
+ : undefined;
1707
+ if (!serverInfo || !tool) {
1593
1708
  throw new Error(`Server not found: ${request.params.name}`);
1594
1709
  }
1710
+ assertToolAvailableForRoute(tool, appsRouteContext);
1595
1711
  // Handle OpenAPI servers differently
1596
1712
  if (serverInfo.openApiClient) {
1597
1713
  // For OpenAPI servers, use the OpenAPI client
1598
1714
  const openApiClient = serverInfo.openApiClient;
1599
1715
  // Remove server prefix from tool name if present
1600
- const separator = getNameSeparator();
1601
- const prefix = `${serverInfo.name}${separator}`;
1602
- const cleanToolName = request.params.name.startsWith(prefix)
1603
- ? request.params.name.substring(prefix.length)
1604
- : request.params.name;
1716
+ const cleanToolName = normalizeToolNameForServer(serverInfo.name, request.params.name);
1605
1717
  console.log('Invoking OpenAPI tool', {
1606
1718
  toolName: cleanToolName,
1607
1719
  serverName: serverInfo.name,
@@ -1673,11 +1785,7 @@ export const handleCallToolRequest = async (request, extra) => {
1673
1785
  if (!client) {
1674
1786
  throw new Error(`Client not found for server: ${serverInfo.name}`);
1675
1787
  }
1676
- const separator = getNameSeparator();
1677
- const prefix = `${serverInfo.name}${separator}`;
1678
- const cleanToolName = request.params.name.startsWith(prefix)
1679
- ? request.params.name.substring(prefix.length)
1680
- : request.params.name;
1788
+ const cleanToolName = normalizeToolNameForServer(serverInfo.name, request.params.name);
1681
1789
  await reserveHostedIfNeeded(serverInfo.name, cleanToolName);
1682
1790
  const result = await callToolWithReconnect(serverInfo, { ...request.params, name: cleanToolName }, serverInfo.options || {});
1683
1791
  await settleHostedIfNeeded({
@@ -1860,6 +1968,7 @@ export const handleListResourcesRequest = async (_, extra) => {
1860
1968
  const sessionId = extra.sessionId || '';
1861
1969
  const group = getGroup(sessionId);
1862
1970
  console.log(`Handling ListResourcesRequest for group: ${group}`);
1971
+ const appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
1863
1972
  // Start with built-in resources (only enabled ones)
1864
1973
  const builtinResources = await getBuiltinResourceDao().findEnabled();
1865
1974
  const allResources = builtinResources.map((br) => normalizeResourceForList({
@@ -1884,10 +1993,13 @@ export const handleListResourcesRequest = async (_, extra) => {
1884
1993
  // Apply custom descriptions from server configuration
1885
1994
  const resourcesWithCustomDescriptions = enabledResources.map((resource) => {
1886
1995
  const resourceConfig = serverConfig?.resources?.[resource.uri];
1887
- return normalizeResourceForList({
1996
+ const normalizedResource = normalizeResourceForList({
1888
1997
  ...resource,
1889
1998
  description: resourceConfig?.description || resource.description,
1890
1999
  });
2000
+ return appsRouteContext.enabled
2001
+ ? normalizedResource
2002
+ : stripMcpAppsMetadata(normalizedResource);
1891
2003
  });
1892
2004
  allResources.push(...resourcesWithCustomDescriptions);
1893
2005
  }
@@ -1900,6 +2012,7 @@ export const handleListResourceTemplatesRequest = async (_, extra) => {
1900
2012
  const sessionId = extra.sessionId || '';
1901
2013
  const group = getGroup(sessionId);
1902
2014
  console.log(`Handling ListResourceTemplatesRequest for group: ${group}`);
2015
+ const appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
1903
2016
  const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(group, {
1904
2017
  requireClient: true,
1905
2018
  });
@@ -1908,15 +2021,21 @@ export const handleListResourceTemplatesRequest = async (_, extra) => {
1908
2021
  return [];
1909
2022
  }
1910
2023
  const templates = await serverInfo.client.listResourceTemplates({}, serverInfo.options || {});
1911
- return filterResourceTemplatesByGroup(group, serverInfo.name, templates.resourceTemplates || [], serverConfigsByName.get(serverInfo.name));
2024
+ const filteredTemplates = await filterResourceTemplatesByGroup(group, serverInfo.name, templates.resourceTemplates || [], serverConfigsByName.get(serverInfo.name));
2025
+ return appsRouteContext.enabled
2026
+ ? filteredTemplates
2027
+ : filteredTemplates.map((template) => stripMcpAppsMetadata(template));
1912
2028
  }));
1913
2029
  return {
1914
2030
  resourceTemplates: results.flatMap((result) => result.status === 'fulfilled' ? result.value : []),
1915
2031
  };
1916
2032
  };
1917
- export const handleReadResourceRequest = async (request, _extra) => {
2033
+ export const handleReadResourceRequest = async (request, extra) => {
1918
2034
  try {
1919
2035
  const { uri } = request.params;
2036
+ const sessionId = extra.sessionId || '';
2037
+ const group = getGroup(sessionId);
2038
+ const appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
1920
2039
  // Check built-in resources first
1921
2040
  const builtinResource = await getBuiltinResourceDao().findByUri(uri);
1922
2041
  if (builtinResource && builtinResource.enabled !== false) {
@@ -1930,18 +2049,39 @@ export const handleReadResourceRequest = async (request, _extra) => {
1930
2049
  ],
1931
2050
  };
1932
2051
  }
1933
- // Find the server that owns this resource
1934
- const server = serverInfos.find((serverInfo) => serverInfo.status === 'connected' &&
1935
- serverInfo.enabled !== false &&
1936
- serverInfo.resources?.find((resource) => resource.uri === uri));
1937
- if (!server) {
2052
+ const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(group);
2053
+ let server;
2054
+ for (const serverInfo of filteredServerInfos) {
2055
+ if (serverInfo.status !== 'connected') {
2056
+ continue;
2057
+ }
2058
+ const serverConfig = await getServerDao().findById(serverInfo.name);
2059
+ let enabledResources = serverInfo.resources;
2060
+ if (serverConfig?.resources) {
2061
+ enabledResources = enabledResources.filter((resource) => serverConfig.resources?.[resource.uri]?.enabled !== false);
2062
+ }
2063
+ enabledResources = await filterResourcesByGroup(group, serverInfo.name, enabledResources, serverConfigsByName.get(serverInfo.name));
2064
+ if (enabledResources.some((resource) => resource.uri === uri)) {
2065
+ server = serverInfo;
2066
+ break;
2067
+ }
2068
+ }
2069
+ if (!server && appsRouteContext.enabled && uri.startsWith('ui://')) {
2070
+ server = appsRouteContext.serverInfo;
2071
+ }
2072
+ if (!server?.client) {
1938
2073
  throw new Error(`Resource not found: ${uri}`);
1939
2074
  }
1940
- const result = await server.client?.readResource({ uri });
1941
- if (!result) {
2075
+ const result = await server.client.readResource({ uri });
2076
+ if (!result || !Array.isArray(result.contents)) {
1942
2077
  throw new Error(`Failed to read resource: ${uri}`);
1943
2078
  }
1944
- return result;
2079
+ return appsRouteContext.enabled
2080
+ ? result
2081
+ : {
2082
+ ...result,
2083
+ contents: result.contents.map((content) => stripMcpAppsMetadata(content)),
2084
+ };
1945
2085
  }
1946
2086
  catch (error) {
1947
2087
  console.error('Error handling ReadResourceRequest', summarizeErrorForLogging(error));
@@ -1968,7 +2108,12 @@ export const createMcpServer = (name, version, options) => {
1968
2108
  }
1969
2109
  // If no group, use default name (global routing)
1970
2110
  const server = new Server({ name: serverName, version }, {
1971
- capabilities: { tools: {}, prompts: {}, resources: {} },
2111
+ capabilities: {
2112
+ tools: { listChanged: true },
2113
+ prompts: { listChanged: true },
2114
+ resources: { listChanged: true },
2115
+ ...MCP_APPS_CAPABILITIES,
2116
+ },
1972
2117
  ...(normalizedOptions.instructions !== undefined
1973
2118
  ? { instructions: normalizedOptions.instructions }
1974
2119
  : {}),