@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.
- package/dist/controllers/oauthCallbackController.js +0 -1
- package/dist/controllers/oauthCallbackController.js.map +1 -1
- package/dist/controllers/serverController.js +13 -1
- package/dist/controllers/serverController.js.map +1 -1
- package/dist/services/mcpOAuthProvider.js +11 -1
- package/dist/services/mcpOAuthProvider.js.map +1 -1
- package/dist/services/mcpService.js +189 -13
- package/dist/services/mcpService.js.map +1 -1
- package/dist/utils/jsonSchemaValidator.js +26 -0
- package/dist/utils/jsonSchemaValidator.js.map +1 -0
- package/dist/utils/serverConfigPersistence.js +7 -0
- package/dist/utils/serverConfigPersistence.js.map +1 -1
- package/frontend/dist/assets/{ActivityPage-Ci3V0MRG.js → ActivityPage-D0SFqsRe.js} +2 -2
- package/frontend/dist/assets/{ActivityPage-Ci3V0MRG.js.map → ActivityPage-D0SFqsRe.js.map} +1 -1
- package/frontend/dist/assets/{Dashboard-CugghcDJ.js → Dashboard-DzWkQnht.js} +2 -2
- package/frontend/dist/assets/{Dashboard-CugghcDJ.js.map → Dashboard-DzWkQnht.js.map} +1 -1
- package/frontend/dist/assets/{EndpointCopy-BXZo8Cty.js → EndpointCopy-Bqjt3lGe.js} +2 -2
- package/frontend/dist/assets/{EndpointCopy-BXZo8Cty.js.map → EndpointCopy-Bqjt3lGe.js.map} +1 -1
- package/frontend/dist/assets/{GroupsPage-W6RjpC0i.js → GroupsPage-DsmZMSK0.js} +2 -2
- package/frontend/dist/assets/{GroupsPage-W6RjpC0i.js.map → GroupsPage-DsmZMSK0.js.map} +1 -1
- package/frontend/dist/assets/{LoginPage-CeK6PaNa.js → LoginPage-_Gr1ere-.js} +2 -2
- package/frontend/dist/assets/{LoginPage-CeK6PaNa.js.map → LoginPage-_Gr1ere-.js.map} +1 -1
- package/frontend/dist/assets/{LogsPage--YJi3jrU.js → LogsPage-D2vByutC.js} +2 -2
- package/frontend/dist/assets/{LogsPage--YJi3jrU.js.map → LogsPage-D2vByutC.js.map} +1 -1
- package/frontend/dist/assets/{MarketPage-CM_ywdX2.js → MarketPage-BhyrxwBv.js} +2 -2
- package/frontend/dist/assets/{MarketPage-CM_ywdX2.js.map → MarketPage-BhyrxwBv.js.map} +1 -1
- package/frontend/dist/assets/{PromptsPage-Bt7UumtQ.js → PromptsPage-BLn95Icp.js} +2 -2
- package/frontend/dist/assets/{PromptsPage-Bt7UumtQ.js.map → PromptsPage-BLn95Icp.js.map} +1 -1
- package/frontend/dist/assets/{ResourcesPage-DhB3ycyY.js → ResourcesPage-B27GVOO6.js} +2 -2
- package/frontend/dist/assets/{ResourcesPage-DhB3ycyY.js.map → ResourcesPage-B27GVOO6.js.map} +1 -1
- package/frontend/dist/assets/ServersPage-BuoWpLAq.js +39 -0
- package/frontend/dist/assets/ServersPage-BuoWpLAq.js.map +1 -0
- package/frontend/dist/assets/{SettingsPage-DfCWu2KX.js → SettingsPage-BzCwUvx7.js} +2 -2
- package/frontend/dist/assets/{SettingsPage-DfCWu2KX.js.map → SettingsPage-BzCwUvx7.js.map} +1 -1
- package/frontend/dist/assets/{StatusDot-HBipO3HS.js → StatusDot-BUWJw3dA.js} +2 -2
- package/frontend/dist/assets/{StatusDot-HBipO3HS.js.map → StatusDot-BUWJw3dA.js.map} +1 -1
- package/frontend/dist/assets/{ToggleGroup-pYyiJRvx.js → ToggleGroup-BPAIOm5B.js} +2 -2
- package/frontend/dist/assets/{ToggleGroup-pYyiJRvx.js.map → ToggleGroup-BPAIOm5B.js.map} +1 -1
- package/frontend/dist/assets/{UsersPage-BNoq4Qio.js → UsersPage-SZrL0bKd.js} +2 -2
- package/frontend/dist/assets/{UsersPage-BNoq4Qio.js.map → UsersPage-SZrL0bKd.js.map} +1 -1
- package/frontend/dist/assets/{contextCost-DcQ1PVMm.js → contextCost-BxT09q-Y.js} +2 -2
- package/frontend/dist/assets/{contextCost-DcQ1PVMm.js.map → contextCost-BxT09q-Y.js.map} +1 -1
- package/frontend/dist/assets/index-Bz_euXvO.js +3 -0
- package/frontend/dist/assets/{index-CZJVfOO6.js.map → index-Bz_euXvO.js.map} +1 -1
- package/frontend/dist/assets/index-DEAy4k6z.css +1 -0
- package/frontend/dist/assets/{resourceService-CJciejgX.js → resourceService-CWKSpa0p.js} +2 -2
- package/frontend/dist/assets/{resourceService-CJciejgX.js.map → resourceService-CWKSpa0p.js.map} +1 -1
- package/frontend/dist/assets/useSettingsData-0sbahDtL.js +2 -0
- package/frontend/dist/assets/{useSettingsData-hgvZTFTk.js.map → useSettingsData-0sbahDtL.js.map} +1 -1
- package/frontend/dist/assets/variableDetection-DfkBZ0gv.js +16 -0
- package/frontend/dist/assets/variableDetection-DfkBZ0gv.js.map +1 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +2 -2
- package/frontend/dist/assets/ServersPage-BFwvEzGn.js +0 -37
- package/frontend/dist/assets/ServersPage-BFwvEzGn.js.map +0 -1
- package/frontend/dist/assets/index-BfEBXfi0.css +0 -1
- package/frontend/dist/assets/index-CZJVfOO6.js +0 -3
- package/frontend/dist/assets/useSettingsData-hgvZTFTk.js +0 -2
- package/frontend/dist/assets/variableDetection-DPNQ6jqO.js +0 -16
- 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
|
|
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
|
-
*
|
|
1851
|
-
*
|
|
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
|
|
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 =
|
|
1864
|
-
|
|
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
|
-
|
|
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({
|