@samanhappy/mcphub 1.0.27 → 1.0.28

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 (60) hide show
  1. package/dist/controllers/oauthCallbackController.js +0 -1
  2. package/dist/controllers/oauthCallbackController.js.map +1 -1
  3. package/dist/controllers/serverController.js +13 -1
  4. package/dist/controllers/serverController.js.map +1 -1
  5. package/dist/services/mcpOAuthProvider.js +11 -1
  6. package/dist/services/mcpOAuthProvider.js.map +1 -1
  7. package/dist/services/mcpService.js +189 -13
  8. package/dist/services/mcpService.js.map +1 -1
  9. package/dist/utils/jsonSchemaValidator.js +26 -0
  10. package/dist/utils/jsonSchemaValidator.js.map +1 -0
  11. package/dist/utils/serverConfigPersistence.js +7 -0
  12. package/dist/utils/serverConfigPersistence.js.map +1 -1
  13. package/frontend/dist/assets/{ActivityPage-Ci3V0MRG.js → ActivityPage-D0SFqsRe.js} +2 -2
  14. package/frontend/dist/assets/{ActivityPage-Ci3V0MRG.js.map → ActivityPage-D0SFqsRe.js.map} +1 -1
  15. package/frontend/dist/assets/{Dashboard-CugghcDJ.js → Dashboard-DzWkQnht.js} +2 -2
  16. package/frontend/dist/assets/{Dashboard-CugghcDJ.js.map → Dashboard-DzWkQnht.js.map} +1 -1
  17. package/frontend/dist/assets/{EndpointCopy-BXZo8Cty.js → EndpointCopy-Bqjt3lGe.js} +2 -2
  18. package/frontend/dist/assets/{EndpointCopy-BXZo8Cty.js.map → EndpointCopy-Bqjt3lGe.js.map} +1 -1
  19. package/frontend/dist/assets/{GroupsPage-W6RjpC0i.js → GroupsPage-DsmZMSK0.js} +2 -2
  20. package/frontend/dist/assets/{GroupsPage-W6RjpC0i.js.map → GroupsPage-DsmZMSK0.js.map} +1 -1
  21. package/frontend/dist/assets/{LoginPage-CeK6PaNa.js → LoginPage-_Gr1ere-.js} +2 -2
  22. package/frontend/dist/assets/{LoginPage-CeK6PaNa.js.map → LoginPage-_Gr1ere-.js.map} +1 -1
  23. package/frontend/dist/assets/{LogsPage--YJi3jrU.js → LogsPage-D2vByutC.js} +2 -2
  24. package/frontend/dist/assets/{LogsPage--YJi3jrU.js.map → LogsPage-D2vByutC.js.map} +1 -1
  25. package/frontend/dist/assets/{MarketPage-CM_ywdX2.js → MarketPage-BhyrxwBv.js} +2 -2
  26. package/frontend/dist/assets/{MarketPage-CM_ywdX2.js.map → MarketPage-BhyrxwBv.js.map} +1 -1
  27. package/frontend/dist/assets/{PromptsPage-Bt7UumtQ.js → PromptsPage-BLn95Icp.js} +2 -2
  28. package/frontend/dist/assets/{PromptsPage-Bt7UumtQ.js.map → PromptsPage-BLn95Icp.js.map} +1 -1
  29. package/frontend/dist/assets/{ResourcesPage-DhB3ycyY.js → ResourcesPage-B27GVOO6.js} +2 -2
  30. package/frontend/dist/assets/{ResourcesPage-DhB3ycyY.js.map → ResourcesPage-B27GVOO6.js.map} +1 -1
  31. package/frontend/dist/assets/ServersPage-BuoWpLAq.js +39 -0
  32. package/frontend/dist/assets/ServersPage-BuoWpLAq.js.map +1 -0
  33. package/frontend/dist/assets/{SettingsPage-DfCWu2KX.js → SettingsPage-BzCwUvx7.js} +2 -2
  34. package/frontend/dist/assets/{SettingsPage-DfCWu2KX.js.map → SettingsPage-BzCwUvx7.js.map} +1 -1
  35. package/frontend/dist/assets/{StatusDot-HBipO3HS.js → StatusDot-BUWJw3dA.js} +2 -2
  36. package/frontend/dist/assets/{StatusDot-HBipO3HS.js.map → StatusDot-BUWJw3dA.js.map} +1 -1
  37. package/frontend/dist/assets/{ToggleGroup-pYyiJRvx.js → ToggleGroup-BPAIOm5B.js} +2 -2
  38. package/frontend/dist/assets/{ToggleGroup-pYyiJRvx.js.map → ToggleGroup-BPAIOm5B.js.map} +1 -1
  39. package/frontend/dist/assets/{UsersPage-BNoq4Qio.js → UsersPage-SZrL0bKd.js} +2 -2
  40. package/frontend/dist/assets/{UsersPage-BNoq4Qio.js.map → UsersPage-SZrL0bKd.js.map} +1 -1
  41. package/frontend/dist/assets/{contextCost-DcQ1PVMm.js → contextCost-BxT09q-Y.js} +2 -2
  42. package/frontend/dist/assets/{contextCost-DcQ1PVMm.js.map → contextCost-BxT09q-Y.js.map} +1 -1
  43. package/frontend/dist/assets/index-Bz_euXvO.js +3 -0
  44. package/frontend/dist/assets/{index-CZJVfOO6.js.map → index-Bz_euXvO.js.map} +1 -1
  45. package/frontend/dist/assets/index-DEAy4k6z.css +1 -0
  46. package/frontend/dist/assets/{resourceService-CJciejgX.js → resourceService-CWKSpa0p.js} +2 -2
  47. package/frontend/dist/assets/{resourceService-CJciejgX.js.map → resourceService-CWKSpa0p.js.map} +1 -1
  48. package/frontend/dist/assets/useSettingsData-0sbahDtL.js +2 -0
  49. package/frontend/dist/assets/{useSettingsData-hgvZTFTk.js.map → useSettingsData-0sbahDtL.js.map} +1 -1
  50. package/frontend/dist/assets/variableDetection-DfkBZ0gv.js +16 -0
  51. package/frontend/dist/assets/variableDetection-DfkBZ0gv.js.map +1 -0
  52. package/frontend/dist/index.html +2 -2
  53. package/package.json +2 -2
  54. package/frontend/dist/assets/ServersPage-BFwvEzGn.js +0 -37
  55. package/frontend/dist/assets/ServersPage-BFwvEzGn.js.map +0 -1
  56. package/frontend/dist/assets/index-BfEBXfi0.css +0 -1
  57. package/frontend/dist/assets/index-CZJVfOO6.js +0 -3
  58. package/frontend/dist/assets/useSettingsData-hgvZTFTk.js +0 -2
  59. package/frontend/dist/assets/variableDetection-DPNQ6jqO.js +0 -16
  60. package/frontend/dist/assets/variableDetection-DPNQ6jqO.js.map +0 -1
@@ -11,6 +11,7 @@ import { StreamableHTTPClientTransport, } from '@modelcontextprotocol/sdk/client
11
11
  import { normalizeHeaders } from '@modelcontextprotocol/sdk/shared/transport.js';
12
12
  import { createFetchWithProxy, getProxyConfigFromEnv } from './proxy.js';
13
13
  import { assertSafeUrl, createRedirectValidatingFetch } from '../utils/ssrf.js';
14
+ import { ResilientJsonSchemaValidator } from '../utils/jsonSchemaValidator.js';
14
15
  import { getUserDao } from '../dao/index.js';
15
16
  import { expandEnvVars, replaceEnvVars, getNameSeparator } from '../config/index.js';
16
17
  import config from '../config/index.js';
@@ -543,6 +544,7 @@ const createUpstreamMcpClient = (name, getServerInfo) => {
543
544
  version: '1.0.0',
544
545
  }, {
545
546
  capabilities: MCP_APPS_CAPABILITIES,
547
+ jsonSchemaValidator: new ResilientJsonSchemaValidator(),
546
548
  listChanged: {
547
549
  tools: {
548
550
  onChanged: (error, tools) => {
@@ -1169,10 +1171,12 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1169
1171
  });
1170
1172
  continue;
1171
1173
  }
1172
- // On-demand servers: skip startup connect; spawn lazily on first tool call.
1174
+ // On-demand stdio servers: skip startup connect; spawn lazily on first
1175
+ // tool call. startOnDemand is a stdio-only optimisation - HTTP/SSE and
1176
+ // OpenAPI servers have no deferred child process, so they connect normally.
1173
1177
  // Preserve existing runtime state (tools/prompts populated from a previous
1174
1178
  // connection) so the tool list stays visible to agents even while sleeping.
1175
- if (expandedConf.startOnDemand === true) {
1179
+ if (expandedConf.startOnDemand === true && isStdioServer(expandedConf)) {
1176
1180
  const existingOnDemand = existingServerInfos.find((s) => s.name === name);
1177
1181
  nextServerInfos.push({
1178
1182
  // Carry over cached tools/prompts/resources if the server was previously connected
@@ -1360,7 +1364,6 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1360
1364
  serverInfo.oauth = {
1361
1365
  authorizationUrl: pendingAuth.authorizationUrl,
1362
1366
  state: pendingAuth.state,
1363
- codeVerifier: pendingAuth.codeVerifier,
1364
1367
  };
1365
1368
  }
1366
1369
  nextServerInfos.push(serverInfo);
@@ -1473,6 +1476,19 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
1473
1476
  throw error;
1474
1477
  }
1475
1478
  serverInfos = nextServerInfos;
1479
+ // Populate the tool cache for on-demand stdio servers so their tools are
1480
+ // visible to agents, then put them back to sleep. Running here (rather than
1481
+ // only at startup) also covers servers added/enabled/reloaded after startup.
1482
+ const primePromise = primeOnDemandServers();
1483
+ // At full startup (no serverName) keep prime fire-and-forget so a slow or
1484
+ // broken on-demand server does not block init. For a targeted reload/edit
1485
+ // (serverName set), await it so the caller - and the dashboard refresh that
1486
+ // follows the HTTP response - sees the cached tools immediately instead of an
1487
+ // empty list that only fills in after the fire-and-forget prime completes and
1488
+ // the next poll picks it up. See #1032.
1489
+ if (serverName) {
1490
+ await primePromise;
1491
+ }
1476
1492
  return serverInfos;
1477
1493
  };
1478
1494
  // Register all MCP tools
@@ -1549,6 +1565,14 @@ export const getServersInfo = async (page, limit, user) => {
1549
1565
  enabled: promptConfig?.enabled !== false, // Default to true if not explicitly disabled
1550
1566
  };
1551
1567
  });
1568
+ const resourcesWithEnabled = resources.map((resource) => {
1569
+ const resourceConfig = serverConfig?.resources?.[resource.uri];
1570
+ return {
1571
+ ...resource,
1572
+ description: resolveDescriptionOverride(resource.description, resourceConfig),
1573
+ enabled: resourceConfig?.enabled !== false,
1574
+ };
1575
+ });
1552
1576
  return {
1553
1577
  name,
1554
1578
  version,
@@ -1559,7 +1583,7 @@ export const getServersInfo = async (page, limit, user) => {
1559
1583
  error,
1560
1584
  tools: toolsWithEnabled,
1561
1585
  prompts: promptsWithEnabled,
1562
- resources,
1586
+ resources: resourcesWithEnabled,
1563
1587
  createTime,
1564
1588
  enabled,
1565
1589
  oauth: oauth || oauthConnected
@@ -1568,6 +1592,7 @@ export const getServersInfo = async (page, limit, user) => {
1568
1592
  ? {
1569
1593
  authorizationUrl: oauth.authorizationUrl,
1570
1594
  state: oauth.state,
1595
+ ...(oauth.clientIdConfigured ? { clientIdConfigured: true } : {}),
1571
1596
  // Don't expose codeVerifier to frontend for security
1572
1597
  }
1573
1598
  : {}),
@@ -1688,7 +1713,7 @@ const filterToolsByConfig = async (serverName, tools) => {
1688
1713
  };
1689
1714
  // Get server by tool name
1690
1715
  const getServerByTool = (toolName) => {
1691
- return serverInfos.find((serverInfo) => serverInfo.tools.some((tool) => tool.name === toolName));
1716
+ return serverInfos.find((serverInfo) => serverInfo.enabled !== false && serverInfo.tools.some((tool) => tool.name === toolName));
1692
1717
  };
1693
1718
  // Add new server
1694
1719
  export const addServer = async (name, config) => {
@@ -1847,30 +1872,160 @@ const scheduleIdleShutdown = (serverInfo) => {
1847
1872
  };
1848
1873
  /**
1849
1874
  * Ensures an on-demand server is connected before a tool call proceeds.
1850
- * Uses a singleton promise so concurrent callers wait for the same spawn
1851
- * instead of triggering duplicate process launches.
1875
+ *
1876
+ * Performs a direct, awaited wake-up that mutates the existing `serverInfo`
1877
+ * in place (preserving the reference callers hold), rather than going through
1878
+ * `reconnectServer` -> `initializeClientsFromSettings`. The latter skips the
1879
+ * connect for on-demand servers, resolves before the fire-and-forget handshake
1880
+ * completes, and rebuilds `serverInfos` (leaving callers with a stale object) -
1881
+ * so it could never actually wake a sleeping server. See #1029.
1882
+ *
1883
+ * Concurrent callers coalesce on `spawningPromise` so only one spawn runs.
1852
1884
  */
1853
1885
  const ensureServerReady = async (serverInfo) => {
1854
1886
  if (serverInfo.status === 'connected')
1855
1887
  return;
1856
- // Another call is already spawning wait for it
1888
+ // Another call is already spawning - wait for it
1857
1889
  if (serverInfo.spawningPromise) {
1858
1890
  await serverInfo.spawningPromise;
1859
1891
  return;
1860
1892
  }
1861
- console.log(`[${serverInfo.name}] Cold-starting on-demand server…`);
1862
1893
  const spawnStart = Date.now();
1863
- const spawnPromise = reconnectServer(serverInfo.name).then(() => {
1864
- console.log(`[${serverInfo.name}] On-demand server ready in ${Date.now() - spawnStart}ms`);
1865
- });
1894
+ const spawnPromise = (async () => {
1895
+ // Load the config fresh from the DAO for the expanded transport config and
1896
+ // to read the server name. See #1029.
1897
+ const rawConfig = await getServerDao().findById(serverInfo.name);
1898
+ if (!rawConfig) {
1899
+ throw new Error('Server configuration not found for on-demand wake');
1900
+ }
1901
+ if (rawConfig.enabled === false) {
1902
+ throw new Error(`Cannot start disabled on-demand server: ${rawConfig.name}`);
1903
+ }
1904
+ const expandedConf = replaceEnvVars(rawConfig);
1905
+ // startOnDemand is a stdio-only optimisation; OpenAPI/HTTP servers have no
1906
+ // deferred process to spawn, so there is nothing to wake.
1907
+ if (expandedConf.type === 'openapi') {
1908
+ return;
1909
+ }
1910
+ const name = rawConfig.name;
1911
+ console.log(`[${name}] Cold-starting on-demand server…`);
1912
+ const transport = await createTransportFromConfig(name, expandedConf);
1913
+ const client = createUpstreamMcpClient(name, () => serverInfo);
1914
+ const serverRequestOptions = expandedConf.options || {};
1915
+ const defaultRequestTimeout = Number(process.env.DEFAULT_REQUEST_TIMEOUT) || 60000;
1916
+ const requestOptions = {
1917
+ timeout: serverRequestOptions.timeout || defaultRequestTimeout,
1918
+ resetTimeoutOnProgress: serverRequestOptions.resetTimeoutOnProgress ?? true,
1919
+ maxTotalTimeout: serverRequestOptions.maxTotalTimeout,
1920
+ };
1921
+ await connectClientWithDiagnostics(client, transport, requestOptions);
1922
+ // Mutate in place so every holder of this serverInfo (resolution paths,
1923
+ // the caller about to invoke the tool) sees the live client/status.
1924
+ serverInfo.client = client;
1925
+ serverInfo.transport = transport;
1926
+ serverInfo.options = requestOptions;
1927
+ serverInfo.version = client.getServerVersion?.()?.version;
1928
+ serverInfo.instructions = client.getInstructions?.();
1929
+ serverInfo.config = expandedConf;
1930
+ serverInfo.status = 'connected';
1931
+ serverInfo.error = null;
1932
+ let toolCount = 0;
1933
+ let promptCount = 0;
1934
+ let resourceCount = 0;
1935
+ const capabilities = client.getServerCapabilities?.();
1936
+ if (capabilities?.tools) {
1937
+ try {
1938
+ const tools = await client.listTools({}, requestOptions);
1939
+ toolCount = tools.tools.length;
1940
+ updateServerToolsCache(serverInfo, tools.tools);
1941
+ broadcastToolListChanged();
1942
+ }
1943
+ catch (error) {
1944
+ console.warn(`[${name}] Failed to list tools during wake`, {
1945
+ error: summarizeErrorForLogging(error),
1946
+ });
1947
+ }
1948
+ }
1949
+ if (capabilities?.prompts) {
1950
+ try {
1951
+ const prompts = await client.listPrompts({}, requestOptions);
1952
+ promptCount = prompts.prompts.length;
1953
+ updateServerPromptsCache(serverInfo, prompts.prompts);
1954
+ }
1955
+ catch (error) {
1956
+ console.warn(`[${name}] Failed to list prompts during wake`, {
1957
+ error: summarizeErrorForLogging(error),
1958
+ });
1959
+ }
1960
+ }
1961
+ if (capabilities?.resources) {
1962
+ try {
1963
+ const resources = await client.listResources({}, requestOptions);
1964
+ resourceCount = resources.resources.length;
1965
+ updateServerResourcesCache(serverInfo, resources.resources);
1966
+ }
1967
+ catch (error) {
1968
+ console.warn(`[${name}] Failed to list resources during wake`, {
1969
+ error: summarizeErrorForLogging(error),
1970
+ });
1971
+ }
1972
+ }
1973
+ // No-op for stdio transports; only SSE/streamable-http opt in via enableKeepAlive.
1974
+ setupServerKeepAlive(serverInfo, expandedConf);
1975
+ console.log(`[${name}] On-demand server ready in ${Date.now() - spawnStart}ms` +
1976
+ ` (${toolCount} tools, ${promptCount} prompts, ${resourceCount} resources)`);
1977
+ })();
1866
1978
  serverInfo.spawningPromise = spawnPromise;
1867
1979
  try {
1868
1980
  await spawnPromise;
1869
1981
  }
1982
+ catch (error) {
1983
+ serverInfo.status = 'disconnected';
1984
+ serverInfo.error = `Failed to start on-demand server: ${formatErrorForLogging(error)}`;
1985
+ throw error;
1986
+ }
1870
1987
  finally {
1871
1988
  serverInfo.spawningPromise = undefined;
1872
1989
  }
1873
1990
  };
1991
+ // stdio servers are the only ones that benefit from deferred spawning; HTTP/SSE
1992
+ // and OpenAPI servers have no long-lived child process to skip.
1993
+ const isStdioServer = (conf) => !!conf && (conf.type === 'stdio' || (!conf.type && !!conf.command));
1994
+ /**
1995
+ * Connects each on-demand stdio server once to populate its tool cache, then
1996
+ * shuts the child back down (keeping the cache). Run after startup init so the
1997
+ * tools are advertised immediately and a later `tools/call` can wake the server.
1998
+ *
1999
+ * Returns a promise so a targeted reload/edit can await it (via
2000
+ * initializeClientsFromSettings) and surface the populated tool cache before
2001
+ * the HTTP response - otherwise the dashboard refresh that follows the edit
2002
+ * sees an empty list until the next poll. At full startup the caller still
2003
+ * ignores the promise (fire-and-forget) so a slow or broken on-demand server
2004
+ * does not block init. Each server is isolated so one failure does not affect
2005
+ * the others. See #1029 / #1032.
2006
+ */
2007
+ const primeOnDemandServers = () => {
2008
+ const targets = serverInfos.filter((si) => si.config?.startOnDemand === true &&
2009
+ isStdioServer(si.config) &&
2010
+ si.tools.length === 0 &&
2011
+ si.enabled !== false);
2012
+ if (targets.length === 0)
2013
+ return Promise.resolve();
2014
+ console.log(`Priming ${targets.length} on-demand server(s) for tool discovery…`);
2015
+ return Promise.allSettled(targets.map(async (si) => {
2016
+ try {
2017
+ await ensureServerReady(si);
2018
+ // Sleep the child but keep the cached tool list visible to agents.
2019
+ shutdownOnDemandServer(si);
2020
+ console.log('On-demand server primed and asleep');
2021
+ }
2022
+ catch (error) {
2023
+ console.warn('Failed to prime on-demand server', {
2024
+ error: summarizeErrorForLogging(error),
2025
+ });
2026
+ }
2027
+ })).then(() => undefined);
2028
+ };
1874
2029
  export const resetServerOAuthConnection = (name) => {
1875
2030
  const serverInfo = serverInfos.find((serverInfo) => serverInfo.name === name);
1876
2031
  if (!serverInfo) {
@@ -2063,7 +2218,11 @@ const resolveToolInGroup = async (group, toolName, allowRawName) => {
2063
2218
  }
2064
2219
  const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(lookupGroup);
2065
2220
  for (const serverInfo of filteredServerInfos) {
2066
- if (serverInfo.status !== 'connected' || serverInfo.enabled === false) {
2221
+ // A disconnected on-demand server may still have a cached tool list from a
2222
+ // previous wake-up; allow it through so it can be selected and woken by the
2223
+ // caller. Other disconnected servers cannot serve the request. See #1029.
2224
+ if ((serverInfo.status !== 'connected' && !serverInfo.config?.startOnDemand) ||
2225
+ serverInfo.enabled === false) {
2067
2226
  continue;
2068
2227
  }
2069
2228
  const serverConfig = serverConfigsByName.get(serverInfo.name);
@@ -2135,6 +2294,14 @@ export const handleListToolsRequest = async (_, extra) => {
2135
2294
  }
2136
2295
  const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(group);
2137
2296
  const appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
2297
+ // If the startup prime of an on-demand server is still in flight, wait for it
2298
+ // so this list reflects the freshly cached tools instead of returning empty.
2299
+ // No wake is triggered from list itself; the prime handles that. See #1029.
2300
+ await Promise.allSettled(filteredServerInfos
2301
+ .filter((si) => si.config?.startOnDemand === true &&
2302
+ si.tools.length === 0 &&
2303
+ si.spawningPromise)
2304
+ .map((si) => si.spawningPromise));
2138
2305
  const allTools = [];
2139
2306
  for (const serverInfo of filteredServerInfos) {
2140
2307
  if (serverInfo.tools && serverInfo.tools.length > 0) {
@@ -2427,6 +2594,13 @@ export const handleCallToolRequest = async (request, extra) => {
2427
2594
  throw new Error(`Server not found: ${request.params.name}`);
2428
2595
  }
2429
2596
  assertToolAvailableForRoute(tool, appsRouteContext);
2597
+ // Wake the on-demand server before invoking the tool. It may be asleep with
2598
+ // a cached tool list (visible above) but no live client. Mirrors the
2599
+ // $smart call_tool path. See #1029.
2600
+ if (serverInfo.config?.startOnDemand && serverInfo.status !== 'connected') {
2601
+ await ensureServerReady(serverInfo);
2602
+ }
2603
+ serverInfo.lastUsedAt = Date.now();
2430
2604
  // Handle OpenAPI servers differently
2431
2605
  if (serverInfo.openApiClient) {
2432
2606
  // For OpenAPI servers, use the OpenAPI client
@@ -2526,6 +2700,8 @@ export const handleCallToolRequest = async (request, extra) => {
2526
2700
  toolName: cleanToolName,
2527
2701
  result: summarizeToolResultForLogging(result),
2528
2702
  });
2703
+ // Reset idle-shutdown timer for on-demand servers after each successful call
2704
+ scheduleIdleShutdown(serverInfo);
2529
2705
  // Log successful activity
2530
2706
  const duration = Date.now() - startTime;
2531
2707
  await activityLogger.logToolCall({