@samanhappy/mcphub 1.0.27 → 1.0.29
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/README.fr.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/dist/clients/openapi.js +194 -15
- package/dist/clients/openapi.js.map +1 -1
- package/dist/controllers/oauthCallbackController.js +0 -1
- package/dist/controllers/oauthCallbackController.js.map +1 -1
- package/dist/controllers/serverController.js +34 -2
- package/dist/controllers/serverController.js.map +1 -1
- package/dist/server.js +12 -9
- package/dist/server.js.map +1 -1
- package/dist/services/mcpOAuthProvider.js +24 -5
- package/dist/services/mcpOAuthProvider.js.map +1 -1
- package/dist/services/mcpService.js +208 -15
- 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 +10 -0
- package/dist/utils/serverConfigPersistence.js.map +1 -1
- package/dist/utils/serverShutdown.js +27 -0
- package/dist/utils/serverShutdown.js.map +1 -0
- package/frontend/dist/assets/{ActivityPage-Ci3V0MRG.js → ActivityPage-F14wJ7k4.js} +2 -2
- package/frontend/dist/assets/{ActivityPage-Ci3V0MRG.js.map → ActivityPage-F14wJ7k4.js.map} +1 -1
- package/frontend/dist/assets/{Dashboard-CugghcDJ.js → Dashboard-CDd51FL-.js} +2 -2
- package/frontend/dist/assets/{Dashboard-CugghcDJ.js.map → Dashboard-CDd51FL-.js.map} +1 -1
- package/frontend/dist/assets/{EndpointCopy-BXZo8Cty.js → EndpointCopy-AZCY_Rwi.js} +2 -2
- package/frontend/dist/assets/{EndpointCopy-BXZo8Cty.js.map → EndpointCopy-AZCY_Rwi.js.map} +1 -1
- package/frontend/dist/assets/{GroupsPage-W6RjpC0i.js → GroupsPage-CaJKH8Lx.js} +2 -2
- package/frontend/dist/assets/{GroupsPage-W6RjpC0i.js.map → GroupsPage-CaJKH8Lx.js.map} +1 -1
- package/frontend/dist/assets/{LoginPage-CeK6PaNa.js → LoginPage-DeHCwVAG.js} +2 -2
- package/frontend/dist/assets/{LoginPage-CeK6PaNa.js.map → LoginPage-DeHCwVAG.js.map} +1 -1
- package/frontend/dist/assets/{LogsPage--YJi3jrU.js → LogsPage-CnCc36oX.js} +2 -2
- package/frontend/dist/assets/{LogsPage--YJi3jrU.js.map → LogsPage-CnCc36oX.js.map} +1 -1
- package/frontend/dist/assets/{MarketPage-CM_ywdX2.js → MarketPage-BfhuTkze.js} +2 -2
- package/frontend/dist/assets/{MarketPage-CM_ywdX2.js.map → MarketPage-BfhuTkze.js.map} +1 -1
- package/frontend/dist/assets/{PromptsPage-Bt7UumtQ.js → PromptsPage-Dq-mlu1T.js} +2 -2
- package/frontend/dist/assets/{PromptsPage-Bt7UumtQ.js.map → PromptsPage-Dq-mlu1T.js.map} +1 -1
- package/frontend/dist/assets/{ResourcesPage-DhB3ycyY.js → ResourcesPage-CDquGhgr.js} +2 -2
- package/frontend/dist/assets/{ResourcesPage-DhB3ycyY.js.map → ResourcesPage-CDquGhgr.js.map} +1 -1
- package/frontend/dist/assets/ServersPage-z4d-9c6s.js +39 -0
- package/frontend/dist/assets/ServersPage-z4d-9c6s.js.map +1 -0
- package/frontend/dist/assets/{SettingsPage-DfCWu2KX.js → SettingsPage-BrpO0gki.js} +2 -2
- package/frontend/dist/assets/{SettingsPage-DfCWu2KX.js.map → SettingsPage-BrpO0gki.js.map} +1 -1
- package/frontend/dist/assets/{StatusDot-HBipO3HS.js → StatusDot-BznpIDQ4.js} +2 -2
- package/frontend/dist/assets/{StatusDot-HBipO3HS.js.map → StatusDot-BznpIDQ4.js.map} +1 -1
- package/frontend/dist/assets/{ToggleGroup-pYyiJRvx.js → ToggleGroup-DRYZvM0V.js} +2 -2
- package/frontend/dist/assets/{ToggleGroup-pYyiJRvx.js.map → ToggleGroup-DRYZvM0V.js.map} +1 -1
- package/frontend/dist/assets/{UsersPage-BNoq4Qio.js → UsersPage-Bbb4nPBC.js} +2 -2
- package/frontend/dist/assets/{UsersPage-BNoq4Qio.js.map → UsersPage-Bbb4nPBC.js.map} +1 -1
- package/frontend/dist/assets/{contextCost-DcQ1PVMm.js → contextCost-BFsl4S2x.js} +2 -2
- package/frontend/dist/assets/{contextCost-DcQ1PVMm.js.map → contextCost-BFsl4S2x.js.map} +1 -1
- package/frontend/dist/assets/index-CgcdOGDO.js +3 -0
- package/frontend/dist/assets/{index-CZJVfOO6.js.map → index-CgcdOGDO.js.map} +1 -1
- package/frontend/dist/assets/index-DEAy4k6z.css +1 -0
- package/frontend/dist/assets/{resourceService-CJciejgX.js → resourceService-Ck6rpYB-.js} +2 -2
- package/frontend/dist/assets/{resourceService-CJciejgX.js.map → resourceService-Ck6rpYB-.js.map} +1 -1
- package/frontend/dist/assets/useSettingsData-Dkw2yET1.js +2 -0
- package/frontend/dist/assets/{useSettingsData-hgvZTFTk.js.map → useSettingsData-Dkw2yET1.js.map} +1 -1
- package/frontend/dist/assets/variableDetection-BnH3Qdq5.js +16 -0
- package/frontend/dist/assets/variableDetection-BnH3Qdq5.js.map +1 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +4 -3
- 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';
|
|
@@ -287,6 +288,11 @@ const cleanupIsolatedSession = (sessionId) => {
|
|
|
287
288
|
}
|
|
288
289
|
console.log(`Cleaned up isolated clients for session: ${sessionId}`);
|
|
289
290
|
}
|
|
291
|
+
// Drop per-session OpenAPI cookie jars so authenticated state does not
|
|
292
|
+
// outlive the downstream session.
|
|
293
|
+
for (const serverInfo of serverInfos) {
|
|
294
|
+
serverInfo.openApiClient?.clearSessionCookies?.(sessionId);
|
|
295
|
+
}
|
|
290
296
|
};
|
|
291
297
|
/** Helper to write an isolated session upstream client into the tracking map. */
|
|
292
298
|
const setSessionIsolatedClient = (sessionId, serverName, client, transport) => {
|
|
@@ -543,6 +549,7 @@ const createUpstreamMcpClient = (name, getServerInfo) => {
|
|
|
543
549
|
version: '1.0.0',
|
|
544
550
|
}, {
|
|
545
551
|
capabilities: MCP_APPS_CAPABILITIES,
|
|
552
|
+
jsonSchemaValidator: new ResilientJsonSchemaValidator(),
|
|
546
553
|
listChanged: {
|
|
547
554
|
tools: {
|
|
548
555
|
onChanged: (error, tools) => {
|
|
@@ -1169,10 +1176,12 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1169
1176
|
});
|
|
1170
1177
|
continue;
|
|
1171
1178
|
}
|
|
1172
|
-
// On-demand servers: skip startup connect; spawn lazily on first
|
|
1179
|
+
// On-demand stdio servers: skip startup connect; spawn lazily on first
|
|
1180
|
+
// tool call. startOnDemand is a stdio-only optimisation - HTTP/SSE and
|
|
1181
|
+
// OpenAPI servers have no deferred child process, so they connect normally.
|
|
1173
1182
|
// Preserve existing runtime state (tools/prompts populated from a previous
|
|
1174
1183
|
// connection) so the tool list stays visible to agents even while sleeping.
|
|
1175
|
-
if (expandedConf.startOnDemand === true) {
|
|
1184
|
+
if (expandedConf.startOnDemand === true && isStdioServer(expandedConf)) {
|
|
1176
1185
|
const existingOnDemand = existingServerInfos.find((s) => s.name === name);
|
|
1177
1186
|
nextServerInfos.push({
|
|
1178
1187
|
// Carry over cached tools/prompts/resources if the server was previously connected
|
|
@@ -1360,7 +1369,6 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1360
1369
|
serverInfo.oauth = {
|
|
1361
1370
|
authorizationUrl: pendingAuth.authorizationUrl,
|
|
1362
1371
|
state: pendingAuth.state,
|
|
1363
|
-
codeVerifier: pendingAuth.codeVerifier,
|
|
1364
1372
|
};
|
|
1365
1373
|
}
|
|
1366
1374
|
nextServerInfos.push(serverInfo);
|
|
@@ -1473,6 +1481,19 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1473
1481
|
throw error;
|
|
1474
1482
|
}
|
|
1475
1483
|
serverInfos = nextServerInfos;
|
|
1484
|
+
// Populate the tool cache for on-demand stdio servers so their tools are
|
|
1485
|
+
// visible to agents, then put them back to sleep. Running here (rather than
|
|
1486
|
+
// only at startup) also covers servers added/enabled/reloaded after startup.
|
|
1487
|
+
const primePromise = primeOnDemandServers();
|
|
1488
|
+
// At full startup (no serverName) keep prime fire-and-forget so a slow or
|
|
1489
|
+
// broken on-demand server does not block init. For a targeted reload/edit
|
|
1490
|
+
// (serverName set), await it so the caller - and the dashboard refresh that
|
|
1491
|
+
// follows the HTTP response - sees the cached tools immediately instead of an
|
|
1492
|
+
// empty list that only fills in after the fire-and-forget prime completes and
|
|
1493
|
+
// the next poll picks it up. See #1032.
|
|
1494
|
+
if (serverName) {
|
|
1495
|
+
await primePromise;
|
|
1496
|
+
}
|
|
1476
1497
|
return serverInfos;
|
|
1477
1498
|
};
|
|
1478
1499
|
// Register all MCP tools
|
|
@@ -1549,6 +1570,14 @@ export const getServersInfo = async (page, limit, user) => {
|
|
|
1549
1570
|
enabled: promptConfig?.enabled !== false, // Default to true if not explicitly disabled
|
|
1550
1571
|
};
|
|
1551
1572
|
});
|
|
1573
|
+
const resourcesWithEnabled = resources.map((resource) => {
|
|
1574
|
+
const resourceConfig = serverConfig?.resources?.[resource.uri];
|
|
1575
|
+
return {
|
|
1576
|
+
...resource,
|
|
1577
|
+
description: resolveDescriptionOverride(resource.description, resourceConfig),
|
|
1578
|
+
enabled: resourceConfig?.enabled !== false,
|
|
1579
|
+
};
|
|
1580
|
+
});
|
|
1552
1581
|
return {
|
|
1553
1582
|
name,
|
|
1554
1583
|
version,
|
|
@@ -1559,7 +1588,7 @@ export const getServersInfo = async (page, limit, user) => {
|
|
|
1559
1588
|
error,
|
|
1560
1589
|
tools: toolsWithEnabled,
|
|
1561
1590
|
prompts: promptsWithEnabled,
|
|
1562
|
-
resources,
|
|
1591
|
+
resources: resourcesWithEnabled,
|
|
1563
1592
|
createTime,
|
|
1564
1593
|
enabled,
|
|
1565
1594
|
oauth: oauth || oauthConnected
|
|
@@ -1568,6 +1597,7 @@ export const getServersInfo = async (page, limit, user) => {
|
|
|
1568
1597
|
? {
|
|
1569
1598
|
authorizationUrl: oauth.authorizationUrl,
|
|
1570
1599
|
state: oauth.state,
|
|
1600
|
+
...(oauth.clientIdConfigured ? { clientIdConfigured: true } : {}),
|
|
1571
1601
|
// Don't expose codeVerifier to frontend for security
|
|
1572
1602
|
}
|
|
1573
1603
|
: {}),
|
|
@@ -1688,7 +1718,7 @@ const filterToolsByConfig = async (serverName, tools) => {
|
|
|
1688
1718
|
};
|
|
1689
1719
|
// Get server by tool name
|
|
1690
1720
|
const getServerByTool = (toolName) => {
|
|
1691
|
-
return serverInfos.find((serverInfo) => serverInfo.tools.some((tool) => tool.name === toolName));
|
|
1721
|
+
return serverInfos.find((serverInfo) => serverInfo.enabled !== false && serverInfo.tools.some((tool) => tool.name === toolName));
|
|
1692
1722
|
};
|
|
1693
1723
|
// Add new server
|
|
1694
1724
|
export const addServer = async (name, config) => {
|
|
@@ -1847,30 +1877,160 @@ const scheduleIdleShutdown = (serverInfo) => {
|
|
|
1847
1877
|
};
|
|
1848
1878
|
/**
|
|
1849
1879
|
* Ensures an on-demand server is connected before a tool call proceeds.
|
|
1850
|
-
*
|
|
1851
|
-
*
|
|
1880
|
+
*
|
|
1881
|
+
* Performs a direct, awaited wake-up that mutates the existing `serverInfo`
|
|
1882
|
+
* in place (preserving the reference callers hold), rather than going through
|
|
1883
|
+
* `reconnectServer` -> `initializeClientsFromSettings`. The latter skips the
|
|
1884
|
+
* connect for on-demand servers, resolves before the fire-and-forget handshake
|
|
1885
|
+
* completes, and rebuilds `serverInfos` (leaving callers with a stale object) -
|
|
1886
|
+
* so it could never actually wake a sleeping server. See #1029.
|
|
1887
|
+
*
|
|
1888
|
+
* Concurrent callers coalesce on `spawningPromise` so only one spawn runs.
|
|
1852
1889
|
*/
|
|
1853
1890
|
const ensureServerReady = async (serverInfo) => {
|
|
1854
1891
|
if (serverInfo.status === 'connected')
|
|
1855
1892
|
return;
|
|
1856
|
-
// Another call is already spawning
|
|
1893
|
+
// Another call is already spawning - wait for it
|
|
1857
1894
|
if (serverInfo.spawningPromise) {
|
|
1858
1895
|
await serverInfo.spawningPromise;
|
|
1859
1896
|
return;
|
|
1860
1897
|
}
|
|
1861
|
-
console.log(`[${serverInfo.name}] Cold-starting on-demand server…`);
|
|
1862
1898
|
const spawnStart = Date.now();
|
|
1863
|
-
const spawnPromise =
|
|
1864
|
-
|
|
1865
|
-
|
|
1899
|
+
const spawnPromise = (async () => {
|
|
1900
|
+
// Load the config fresh from the DAO for the expanded transport config and
|
|
1901
|
+
// to read the server name. See #1029.
|
|
1902
|
+
const rawConfig = await getServerDao().findById(serverInfo.name);
|
|
1903
|
+
if (!rawConfig) {
|
|
1904
|
+
throw new Error('Server configuration not found for on-demand wake');
|
|
1905
|
+
}
|
|
1906
|
+
if (rawConfig.enabled === false) {
|
|
1907
|
+
throw new Error(`Cannot start disabled on-demand server: ${rawConfig.name}`);
|
|
1908
|
+
}
|
|
1909
|
+
const expandedConf = replaceEnvVars(rawConfig);
|
|
1910
|
+
// startOnDemand is a stdio-only optimisation; OpenAPI/HTTP servers have no
|
|
1911
|
+
// deferred process to spawn, so there is nothing to wake.
|
|
1912
|
+
if (expandedConf.type === 'openapi') {
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
const name = rawConfig.name;
|
|
1916
|
+
console.log(`[${name}] Cold-starting on-demand server…`);
|
|
1917
|
+
const transport = await createTransportFromConfig(name, expandedConf);
|
|
1918
|
+
const client = createUpstreamMcpClient(name, () => serverInfo);
|
|
1919
|
+
const serverRequestOptions = expandedConf.options || {};
|
|
1920
|
+
const defaultRequestTimeout = Number(process.env.DEFAULT_REQUEST_TIMEOUT) || 60000;
|
|
1921
|
+
const requestOptions = {
|
|
1922
|
+
timeout: serverRequestOptions.timeout || defaultRequestTimeout,
|
|
1923
|
+
resetTimeoutOnProgress: serverRequestOptions.resetTimeoutOnProgress ?? true,
|
|
1924
|
+
maxTotalTimeout: serverRequestOptions.maxTotalTimeout,
|
|
1925
|
+
};
|
|
1926
|
+
await connectClientWithDiagnostics(client, transport, requestOptions);
|
|
1927
|
+
// Mutate in place so every holder of this serverInfo (resolution paths,
|
|
1928
|
+
// the caller about to invoke the tool) sees the live client/status.
|
|
1929
|
+
serverInfo.client = client;
|
|
1930
|
+
serverInfo.transport = transport;
|
|
1931
|
+
serverInfo.options = requestOptions;
|
|
1932
|
+
serverInfo.version = client.getServerVersion?.()?.version;
|
|
1933
|
+
serverInfo.instructions = client.getInstructions?.();
|
|
1934
|
+
serverInfo.config = expandedConf;
|
|
1935
|
+
serverInfo.status = 'connected';
|
|
1936
|
+
serverInfo.error = null;
|
|
1937
|
+
let toolCount = 0;
|
|
1938
|
+
let promptCount = 0;
|
|
1939
|
+
let resourceCount = 0;
|
|
1940
|
+
const capabilities = client.getServerCapabilities?.();
|
|
1941
|
+
if (capabilities?.tools) {
|
|
1942
|
+
try {
|
|
1943
|
+
const tools = await client.listTools({}, requestOptions);
|
|
1944
|
+
toolCount = tools.tools.length;
|
|
1945
|
+
updateServerToolsCache(serverInfo, tools.tools);
|
|
1946
|
+
broadcastToolListChanged();
|
|
1947
|
+
}
|
|
1948
|
+
catch (error) {
|
|
1949
|
+
console.warn(`[${name}] Failed to list tools during wake`, {
|
|
1950
|
+
error: summarizeErrorForLogging(error),
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
if (capabilities?.prompts) {
|
|
1955
|
+
try {
|
|
1956
|
+
const prompts = await client.listPrompts({}, requestOptions);
|
|
1957
|
+
promptCount = prompts.prompts.length;
|
|
1958
|
+
updateServerPromptsCache(serverInfo, prompts.prompts);
|
|
1959
|
+
}
|
|
1960
|
+
catch (error) {
|
|
1961
|
+
console.warn(`[${name}] Failed to list prompts during wake`, {
|
|
1962
|
+
error: summarizeErrorForLogging(error),
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
if (capabilities?.resources) {
|
|
1967
|
+
try {
|
|
1968
|
+
const resources = await client.listResources({}, requestOptions);
|
|
1969
|
+
resourceCount = resources.resources.length;
|
|
1970
|
+
updateServerResourcesCache(serverInfo, resources.resources);
|
|
1971
|
+
}
|
|
1972
|
+
catch (error) {
|
|
1973
|
+
console.warn(`[${name}] Failed to list resources during wake`, {
|
|
1974
|
+
error: summarizeErrorForLogging(error),
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
// No-op for stdio transports; only SSE/streamable-http opt in via enableKeepAlive.
|
|
1979
|
+
setupServerKeepAlive(serverInfo, expandedConf);
|
|
1980
|
+
console.log(`[${name}] On-demand server ready in ${Date.now() - spawnStart}ms` +
|
|
1981
|
+
` (${toolCount} tools, ${promptCount} prompts, ${resourceCount} resources)`);
|
|
1982
|
+
})();
|
|
1866
1983
|
serverInfo.spawningPromise = spawnPromise;
|
|
1867
1984
|
try {
|
|
1868
1985
|
await spawnPromise;
|
|
1869
1986
|
}
|
|
1987
|
+
catch (error) {
|
|
1988
|
+
serverInfo.status = 'disconnected';
|
|
1989
|
+
serverInfo.error = `Failed to start on-demand server: ${formatErrorForLogging(error)}`;
|
|
1990
|
+
throw error;
|
|
1991
|
+
}
|
|
1870
1992
|
finally {
|
|
1871
1993
|
serverInfo.spawningPromise = undefined;
|
|
1872
1994
|
}
|
|
1873
1995
|
};
|
|
1996
|
+
// stdio servers are the only ones that benefit from deferred spawning; HTTP/SSE
|
|
1997
|
+
// and OpenAPI servers have no long-lived child process to skip.
|
|
1998
|
+
const isStdioServer = (conf) => !!conf && (conf.type === 'stdio' || (!conf.type && !!conf.command));
|
|
1999
|
+
/**
|
|
2000
|
+
* Connects each on-demand stdio server once to populate its tool cache, then
|
|
2001
|
+
* shuts the child back down (keeping the cache). Run after startup init so the
|
|
2002
|
+
* tools are advertised immediately and a later `tools/call` can wake the server.
|
|
2003
|
+
*
|
|
2004
|
+
* Returns a promise so a targeted reload/edit can await it (via
|
|
2005
|
+
* initializeClientsFromSettings) and surface the populated tool cache before
|
|
2006
|
+
* the HTTP response - otherwise the dashboard refresh that follows the edit
|
|
2007
|
+
* sees an empty list until the next poll. At full startup the caller still
|
|
2008
|
+
* ignores the promise (fire-and-forget) so a slow or broken on-demand server
|
|
2009
|
+
* does not block init. Each server is isolated so one failure does not affect
|
|
2010
|
+
* the others. See #1029 / #1032.
|
|
2011
|
+
*/
|
|
2012
|
+
const primeOnDemandServers = () => {
|
|
2013
|
+
const targets = serverInfos.filter((si) => si.config?.startOnDemand === true &&
|
|
2014
|
+
isStdioServer(si.config) &&
|
|
2015
|
+
si.tools.length === 0 &&
|
|
2016
|
+
si.enabled !== false);
|
|
2017
|
+
if (targets.length === 0)
|
|
2018
|
+
return Promise.resolve();
|
|
2019
|
+
console.log(`Priming ${targets.length} on-demand server(s) for tool discovery…`);
|
|
2020
|
+
return Promise.allSettled(targets.map(async (si) => {
|
|
2021
|
+
try {
|
|
2022
|
+
await ensureServerReady(si);
|
|
2023
|
+
// Sleep the child but keep the cached tool list visible to agents.
|
|
2024
|
+
shutdownOnDemandServer(si);
|
|
2025
|
+
console.log('On-demand server primed and asleep');
|
|
2026
|
+
}
|
|
2027
|
+
catch (error) {
|
|
2028
|
+
console.warn('Failed to prime on-demand server', {
|
|
2029
|
+
error: summarizeErrorForLogging(error),
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
})).then(() => undefined);
|
|
2033
|
+
};
|
|
1874
2034
|
export const resetServerOAuthConnection = (name) => {
|
|
1875
2035
|
const serverInfo = serverInfos.find((serverInfo) => serverInfo.name === name);
|
|
1876
2036
|
if (!serverInfo) {
|
|
@@ -2063,7 +2223,11 @@ const resolveToolInGroup = async (group, toolName, allowRawName) => {
|
|
|
2063
2223
|
}
|
|
2064
2224
|
const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(lookupGroup);
|
|
2065
2225
|
for (const serverInfo of filteredServerInfos) {
|
|
2066
|
-
|
|
2226
|
+
// A disconnected on-demand server may still have a cached tool list from a
|
|
2227
|
+
// previous wake-up; allow it through so it can be selected and woken by the
|
|
2228
|
+
// caller. Other disconnected servers cannot serve the request. See #1029.
|
|
2229
|
+
if ((serverInfo.status !== 'connected' && !serverInfo.config?.startOnDemand) ||
|
|
2230
|
+
serverInfo.enabled === false) {
|
|
2067
2231
|
continue;
|
|
2068
2232
|
}
|
|
2069
2233
|
const serverConfig = serverConfigsByName.get(serverInfo.name);
|
|
@@ -2135,6 +2299,14 @@ export const handleListToolsRequest = async (_, extra) => {
|
|
|
2135
2299
|
}
|
|
2136
2300
|
const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(group);
|
|
2137
2301
|
const appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
|
|
2302
|
+
// If the startup prime of an on-demand server is still in flight, wait for it
|
|
2303
|
+
// so this list reflects the freshly cached tools instead of returning empty.
|
|
2304
|
+
// No wake is triggered from list itself; the prime handles that. See #1029.
|
|
2305
|
+
await Promise.allSettled(filteredServerInfos
|
|
2306
|
+
.filter((si) => si.config?.startOnDemand === true &&
|
|
2307
|
+
si.tools.length === 0 &&
|
|
2308
|
+
si.spawningPromise)
|
|
2309
|
+
.map((si) => si.spawningPromise));
|
|
2138
2310
|
const allTools = [];
|
|
2139
2311
|
for (const serverInfo of filteredServerInfos) {
|
|
2140
2312
|
if (serverInfo.tools && serverInfo.tools.length > 0) {
|
|
@@ -2170,6 +2342,18 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2170
2342
|
const requestContextService = RequestContextService.getInstance();
|
|
2171
2343
|
const bearerKeyContext = requestContextService.getBearerKeyContext();
|
|
2172
2344
|
const sessionId = extra.sessionId || '';
|
|
2345
|
+
// For OpenAPI cookie-session isolation, use a real per-caller session id.
|
|
2346
|
+
// Direct API controllers fall back to shared synthetic ids ('api-session' /
|
|
2347
|
+
// 'openapi-session') when x-session-id is absent, which would leak cookies
|
|
2348
|
+
// across callers, so only accept an explicitly-provided session header or a
|
|
2349
|
+
// non-synthetic extra.sessionId. Fail-safe (undefined) otherwise.
|
|
2350
|
+
const isSyntheticSessionFallback = (id) => id === 'api-session' || id === 'openapi-session';
|
|
2351
|
+
const explicitXSessionId = extra?.headers?.['x-session-id'];
|
|
2352
|
+
const cookieSessionId = [
|
|
2353
|
+
requestContextService.getSessionId(),
|
|
2354
|
+
typeof explicitXSessionId === 'string' ? explicitXSessionId : undefined,
|
|
2355
|
+
typeof extra?.sessionId === 'string' ? extra.sessionId : undefined,
|
|
2356
|
+
].find((id) => typeof id === 'string' && id.length > 0 && !isSyntheticSessionFallback(id));
|
|
2173
2357
|
// Extract group and key info from request context (set by SSE/HTTP handlers)
|
|
2174
2358
|
// Fallback to extra for backward compatibility (e.g., direct API calls)
|
|
2175
2359
|
const group = requestContextService.getGroupContext() || extra?.group || getGroup(sessionId) || undefined;
|
|
@@ -2305,7 +2489,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2305
2489
|
}
|
|
2306
2490
|
}
|
|
2307
2491
|
await reserveHostedIfNeeded(targetServerInfo.name, cleanToolName);
|
|
2308
|
-
const result = await openApiClient.callTool(cleanToolName, finalArgs, passthroughHeaders);
|
|
2492
|
+
const result = await openApiClient.callTool(cleanToolName, finalArgs, passthroughHeaders, false, cookieSessionId);
|
|
2309
2493
|
await settleHostedIfNeeded({
|
|
2310
2494
|
success: true,
|
|
2311
2495
|
requestContent: finalArgs,
|
|
@@ -2427,6 +2611,13 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2427
2611
|
throw new Error(`Server not found: ${request.params.name}`);
|
|
2428
2612
|
}
|
|
2429
2613
|
assertToolAvailableForRoute(tool, appsRouteContext);
|
|
2614
|
+
// Wake the on-demand server before invoking the tool. It may be asleep with
|
|
2615
|
+
// a cached tool list (visible above) but no live client. Mirrors the
|
|
2616
|
+
// $smart call_tool path. See #1029.
|
|
2617
|
+
if (serverInfo.config?.startOnDemand && serverInfo.status !== 'connected') {
|
|
2618
|
+
await ensureServerReady(serverInfo);
|
|
2619
|
+
}
|
|
2620
|
+
serverInfo.lastUsedAt = Date.now();
|
|
2430
2621
|
// Handle OpenAPI servers differently
|
|
2431
2622
|
if (serverInfo.openApiClient) {
|
|
2432
2623
|
// For OpenAPI servers, use the OpenAPI client
|
|
@@ -2464,7 +2655,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2464
2655
|
}
|
|
2465
2656
|
const finalArgs = request.params.arguments || {};
|
|
2466
2657
|
await reserveHostedIfNeeded(serverInfo.name, cleanToolName);
|
|
2467
|
-
const result = await openApiClient.callTool(cleanToolName, finalArgs, passthroughHeaders);
|
|
2658
|
+
const result = await openApiClient.callTool(cleanToolName, finalArgs, passthroughHeaders, false, cookieSessionId);
|
|
2468
2659
|
await settleHostedIfNeeded({
|
|
2469
2660
|
success: true,
|
|
2470
2661
|
requestContent: finalArgs,
|
|
@@ -2526,6 +2717,8 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2526
2717
|
toolName: cleanToolName,
|
|
2527
2718
|
result: summarizeToolResultForLogging(result),
|
|
2528
2719
|
});
|
|
2720
|
+
// Reset idle-shutdown timer for on-demand servers after each successful call
|
|
2721
|
+
scheduleIdleShutdown(serverInfo);
|
|
2529
2722
|
// Log successful activity
|
|
2530
2723
|
const duration = Date.now() - startTime;
|
|
2531
2724
|
await activityLogger.logToolCall({
|