@samanhappy/mcphub 1.0.31 → 1.0.33
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.md +11 -1
- package/README.zh.md +12 -2
- package/dist/betterAuth.js +13 -5
- package/dist/betterAuth.js.map +1 -1
- package/dist/cli/commands/cache.js.map +1 -1
- package/dist/cli/commands/call.js +2 -2
- package/dist/cli/commands/call.js.map +1 -1
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/discover.js.map +1 -1
- package/dist/cli/commands/export.js.map +1 -1
- package/dist/cli/commands/groups.js.map +1 -1
- package/dist/cli/commands/install.js +1 -1
- package/dist/cli/commands/install.js.map +1 -1
- package/dist/cli/commands/keys.js.map +1 -1
- package/dist/cli/commands/login.js.map +1 -1
- package/dist/cli/commands/servers.js.map +1 -1
- package/dist/cli/commands/tools.js +2 -2
- package/dist/cli/commands/tools.js.map +1 -1
- package/dist/cli/context.js +1 -1
- package/dist/cli/context.js.map +1 -1
- package/dist/cli/http.js +3 -1
- package/dist/cli/http.js.map +1 -1
- package/dist/clients/openapi.js +498 -76
- package/dist/clients/openapi.js.map +1 -1
- package/dist/config/DaoConfigService.js +3 -2
- package/dist/config/DaoConfigService.js.map +1 -1
- package/dist/config/configManager.js +9 -8
- package/dist/config/configManager.js.map +1 -1
- package/dist/config/index.js +54 -8
- package/dist/config/index.js.map +1 -1
- package/dist/config/jwt.js +2 -1
- package/dist/config/jwt.js.map +1 -1
- package/dist/config/migrationUtils.js +46 -45
- package/dist/config/migrationUtils.js.map +1 -1
- package/dist/constants/oauthServerDefaults.js +5 -0
- package/dist/constants/oauthServerDefaults.js.map +1 -1
- package/dist/controllers/activityController.js +6 -5
- package/dist/controllers/activityController.js.map +1 -1
- package/dist/controllers/authController.js +5 -4
- package/dist/controllers/authController.js.map +1 -1
- package/dist/controllers/bearerKeyController.js +7 -6
- package/dist/controllers/bearerKeyController.js.map +1 -1
- package/dist/controllers/betterAuthController.js +2 -1
- package/dist/controllers/betterAuthController.js.map +1 -1
- package/dist/controllers/builtinPromptController.js +13 -5
- package/dist/controllers/builtinPromptController.js.map +1 -1
- package/dist/controllers/builtinResourceController.js +14 -6
- package/dist/controllers/builtinResourceController.js.map +1 -1
- package/dist/controllers/changelogController.js +5 -2
- package/dist/controllers/changelogController.js.map +1 -1
- package/dist/controllers/cloudController.js +11 -10
- package/dist/controllers/cloudController.js.map +1 -1
- package/dist/controllers/configController.js +4 -3
- package/dist/controllers/configController.js.map +1 -1
- package/dist/controllers/contextCostController.js +3 -2
- package/dist/controllers/contextCostController.js.map +1 -1
- package/dist/controllers/groupController.js +2 -1
- package/dist/controllers/groupController.js.map +1 -1
- package/dist/controllers/healthController.js +2 -1
- package/dist/controllers/healthController.js.map +1 -1
- package/dist/controllers/hostedInternalController.js +2 -1
- package/dist/controllers/hostedInternalController.js.map +1 -1
- package/dist/controllers/logController.js +5 -4
- package/dist/controllers/logController.js.map +1 -1
- package/dist/controllers/mcpbController.js +23 -20
- package/dist/controllers/mcpbController.js.map +1 -1
- package/dist/controllers/oauthCallbackController.js +54 -27
- package/dist/controllers/oauthCallbackController.js.map +1 -1
- package/dist/controllers/oauthClientController.js +7 -6
- package/dist/controllers/oauthClientController.js.map +1 -1
- package/dist/controllers/oauthDynamicRegistrationController.js +22 -4
- package/dist/controllers/oauthDynamicRegistrationController.js.map +1 -1
- package/dist/controllers/oauthServerController.js +62 -15
- package/dist/controllers/oauthServerController.js.map +1 -1
- package/dist/controllers/openApiController.js +7 -6
- package/dist/controllers/openApiController.js.map +1 -1
- package/dist/controllers/promptController.js +2 -1
- package/dist/controllers/promptController.js.map +1 -1
- package/dist/controllers/registryController.js +4 -3
- package/dist/controllers/registryController.js.map +1 -1
- package/dist/controllers/serverController.js +154 -65
- package/dist/controllers/serverController.js.map +1 -1
- package/dist/controllers/templateController.js +3 -2
- package/dist/controllers/templateController.js.map +1 -1
- package/dist/controllers/toolController.js +2 -1
- package/dist/controllers/toolController.js.map +1 -1
- package/dist/dao/DaoFactory.js +3 -2
- package/dist/dao/DaoFactory.js.map +1 -1
- package/dist/dao/ServerDao.js.map +1 -1
- package/dist/dao/UserConfigDaoDbImpl.js.map +1 -1
- package/dist/dao/UserDao.js.map +1 -1
- package/dist/dao/base/JsonFileBaseDao.js +3 -2
- package/dist/dao/base/JsonFileBaseDao.js.map +1 -1
- package/dist/dao/examples.js +35 -34
- package/dist/dao/examples.js.map +1 -1
- package/dist/db/connection.js +59 -58
- package/dist/db/connection.js.map +1 -1
- package/dist/db/index.js +2 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/repositories/ServerRepository.js +3 -3
- package/dist/db/repositories/ServerRepository.js.map +1 -1
- package/dist/db/repositories/SystemConfigRepository.js +2 -1
- package/dist/db/repositories/SystemConfigRepository.js.map +1 -1
- package/dist/db/repositories/UserConfigRepository.js.map +1 -1
- package/dist/db/repositories/VectorEmbeddingRepository.js +6 -5
- package/dist/db/repositories/VectorEmbeddingRepository.js.map +1 -1
- package/dist/index.js +28 -19
- package/dist/index.js.map +1 -1
- package/dist/middlewares/auth.js +4 -3
- package/dist/middlewares/auth.js.map +1 -1
- package/dist/middlewares/index.js +2 -1
- package/dist/middlewares/index.js.map +1 -1
- package/dist/middlewares/userContext.js +7 -6
- package/dist/middlewares/userContext.js.map +1 -1
- package/dist/models/OAuth.js +7 -6
- package/dist/models/OAuth.js.map +1 -1
- package/dist/models/User.js +13 -12
- package/dist/models/User.js.map +1 -1
- package/dist/routes/index.js +11 -10
- package/dist/routes/index.js.map +1 -1
- package/dist/scripts/dao-demo.js +28 -27
- package/dist/scripts/dao-demo.js.map +1 -1
- package/dist/server.js +36 -30
- package/dist/server.js.map +1 -1
- package/dist/services/activityLoggingService.js +3 -2
- package/dist/services/activityLoggingService.js.map +1 -1
- package/dist/services/authorizationService.js +28 -0
- package/dist/services/authorizationService.js.map +1 -0
- package/dist/services/betterAuthConfig.js +1 -4
- package/dist/services/betterAuthConfig.js.map +1 -1
- package/dist/services/betterAuthSession.js +8 -7
- package/dist/services/betterAuthSession.js.map +1 -1
- package/dist/services/changelogService.js +5 -2
- package/dist/services/changelogService.js.map +1 -1
- package/dist/services/cimdClientService.js +195 -0
- package/dist/services/cimdClientService.js.map +1 -0
- package/dist/services/cloudService.js +10 -9
- package/dist/services/cloudService.js.map +1 -1
- package/dist/services/contextCostService.js +6 -2
- package/dist/services/contextCostService.js.map +1 -1
- package/dist/services/groupService.js +8 -7
- package/dist/services/groupService.js.map +1 -1
- package/dist/services/hostedAuthService.js +3 -2
- package/dist/services/hostedAuthService.js.map +1 -1
- package/dist/services/hostedControlPlaneClient.js.map +1 -1
- package/dist/services/hostedEventSubscriber.js +9 -8
- package/dist/services/hostedEventSubscriber.js.map +1 -1
- package/dist/services/hostedRuntimeCatalogNames.js +1 -3
- package/dist/services/hostedRuntimeCatalogNames.js.map +1 -1
- package/dist/services/keepAliveService.js +7 -6
- package/dist/services/keepAliveService.js.map +1 -1
- package/dist/services/marketService.js +2 -1
- package/dist/services/marketService.js.map +1 -1
- package/dist/services/mcpOAuthProvider.js +22 -21
- package/dist/services/mcpOAuthProvider.js.map +1 -1
- package/dist/services/mcpService.js +239 -130
- package/dist/services/mcpService.js.map +1 -1
- package/dist/services/oauthClientRegistration.js +30 -29
- package/dist/services/oauthClientRegistration.js.map +1 -1
- package/dist/services/oauthServerService.js +39 -18
- package/dist/services/oauthServerService.js.map +1 -1
- package/dist/services/oauthService.js +16 -15
- package/dist/services/oauthService.js.map +1 -1
- package/dist/services/oauthSettingsStore.js +4 -3
- package/dist/services/oauthSettingsStore.js.map +1 -1
- package/dist/services/openApiGeneratorService.js +1 -3
- package/dist/services/openApiGeneratorService.js.map +1 -1
- package/dist/services/openApiToolStatsService.js +37 -0
- package/dist/services/openApiToolStatsService.js.map +1 -0
- package/dist/services/registry.js +2 -1
- package/dist/services/registry.js.map +1 -1
- package/dist/services/requestContextService.js.map +1 -1
- package/dist/services/serverConfigPresenter.js +85 -0
- package/dist/services/serverConfigPresenter.js.map +1 -0
- package/dist/services/smartRoutingService.js +6 -5
- package/dist/services/smartRoutingService.js.map +1 -1
- package/dist/services/sseService.js +52 -51
- package/dist/services/sseService.js.map +1 -1
- package/dist/services/templateService.js +105 -23
- package/dist/services/templateService.js.map +1 -1
- package/dist/services/toolResultCompressionService.js +8 -5
- package/dist/services/toolResultCompressionService.js.map +1 -1
- package/dist/services/upstreamOAuthDisconnectService.js +9 -6
- package/dist/services/upstreamOAuthDisconnectService.js.map +1 -1
- package/dist/services/userService.js +5 -4
- package/dist/services/userService.js.map +1 -1
- package/dist/services/vectorSearchService.js +126 -112
- package/dist/services/vectorSearchService.js.map +1 -1
- package/dist/utils/cacheUtils.js +5 -4
- package/dist/utils/cacheUtils.js.map +1 -1
- package/dist/utils/corsOrigin.js +42 -0
- package/dist/utils/corsOrigin.js.map +1 -0
- package/dist/utils/dbRetry.js +2 -5
- package/dist/utils/dbRetry.js.map +1 -1
- package/dist/utils/frontendShell.js +2 -1
- package/dist/utils/frontendShell.js.map +1 -1
- package/dist/utils/i18n.js.map +1 -1
- package/dist/utils/logger.js +170 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/migration.js +51 -50
- package/dist/utils/migration.js.map +1 -1
- package/dist/utils/oauthIssuer.js +57 -0
- package/dist/utils/oauthIssuer.js.map +1 -0
- package/dist/utils/openApiRequestBody.js +220 -0
- package/dist/utils/openApiRequestBody.js.map +1 -0
- package/dist/utils/parameterConversion.js +2 -1
- package/dist/utils/parameterConversion.js.map +1 -1
- package/dist/utils/path.js +9 -8
- package/dist/utils/path.js.map +1 -1
- package/dist/utils/processTree.js +47 -0
- package/dist/utils/processTree.js.map +1 -0
- package/dist/utils/proxyTrust.js.map +1 -1
- package/dist/utils/rateLimit.js +8 -0
- package/dist/utils/rateLimit.js.map +1 -1
- package/dist/utils/requireAdmin.js +15 -0
- package/dist/utils/requireAdmin.js.map +1 -0
- package/dist/utils/serverConfigPersistence.js +3 -0
- package/dist/utils/serverConfigPersistence.js.map +1 -1
- package/dist/utils/serverConfigValidation.js +13 -0
- package/dist/utils/serverConfigValidation.js.map +1 -0
- package/dist/utils/serverNameValidation.js +56 -0
- package/dist/utils/serverNameValidation.js.map +1 -0
- package/dist/utils/serverShutdown.js +3 -2
- package/dist/utils/serverShutdown.js.map +1 -1
- package/dist/utils/smartRouting.js +4 -3
- package/dist/utils/smartRouting.js.map +1 -1
- package/dist/utils/ssrf.js +23 -10
- package/dist/utils/ssrf.js.map +1 -1
- package/dist/utils/systemConfigCache.js +40 -2
- package/dist/utils/systemConfigCache.js.map +1 -1
- package/dist/utils/tokenCost.js.map +1 -1
- package/dist/utils/tokenTruncation.js +9 -4
- package/dist/utils/tokenTruncation.js.map +1 -1
- package/dist/utils/version.js +3 -2
- package/dist/utils/version.js.map +1 -1
- package/frontend/dist/assets/{ActivityPage-Dx53zxEM.js → ActivityPage-Du9Nh-Jy.js} +2 -2
- package/frontend/dist/assets/{ActivityPage-Dx53zxEM.js.map → ActivityPage-Du9Nh-Jy.js.map} +1 -1
- package/frontend/dist/assets/ConfirmDialog-3APts2Y9.js +2 -0
- package/frontend/dist/assets/ConfirmDialog-3APts2Y9.js.map +1 -0
- package/frontend/dist/assets/Dashboard-J4G-AZkI.js +2 -0
- package/frontend/dist/assets/{Dashboard-ZYVsSvOw.js.map → Dashboard-J4G-AZkI.js.map} +1 -1
- package/frontend/dist/assets/{DeleteDialog-DNe-zTEl.js → DeleteDialog-D9ycSRvn.js} +2 -2
- package/frontend/dist/assets/{DeleteDialog-DNe-zTEl.js.map → DeleteDialog-D9ycSRvn.js.map} +1 -1
- package/frontend/dist/assets/{EndpointCopy-ngxLPeDF.js → EndpointCopy-DFeLcU9A.js} +2 -2
- package/frontend/dist/assets/{EndpointCopy-ngxLPeDF.js.map → EndpointCopy-DFeLcU9A.js.map} +1 -1
- package/frontend/dist/assets/GroupsPage-CGMZBiXd.js +33 -0
- package/frontend/dist/assets/{GroupsPage-JPtupGpI.js.map → GroupsPage-CGMZBiXd.js.map} +1 -1
- package/frontend/dist/assets/{LoginPage-B6rU3Ugr.js → LoginPage-g5pJLBk0.js} +2 -2
- package/frontend/dist/assets/{LoginPage-B6rU3Ugr.js.map → LoginPage-g5pJLBk0.js.map} +1 -1
- package/frontend/dist/assets/{LogsPage-CXq382fb.js → LogsPage-Bkv7xhNf.js} +2 -2
- package/frontend/dist/assets/{LogsPage-CXq382fb.js.map → LogsPage-Bkv7xhNf.js.map} +1 -1
- package/frontend/dist/assets/MarketPage-WkLrckIr.js +3 -0
- package/frontend/dist/assets/MarketPage-WkLrckIr.js.map +1 -0
- package/frontend/dist/assets/{OAuthConsentPage-C-n6tv3-.js → OAuthConsentPage-CA35XoBK.js} +2 -2
- package/frontend/dist/assets/{OAuthConsentPage-C-n6tv3-.js.map → OAuthConsentPage-CA35XoBK.js.map} +1 -1
- package/frontend/dist/assets/{Pagination-DLSuQ8hw.js → Pagination-Dwko3g48.js} +2 -2
- package/frontend/dist/assets/{Pagination-DLSuQ8hw.js.map → Pagination-Dwko3g48.js.map} +1 -1
- package/frontend/dist/assets/{PromptsPage-BLpy76K4.js → PromptsPage-BuDdW69R.js} +2 -2
- package/frontend/dist/assets/{PromptsPage-BLpy76K4.js.map → PromptsPage-BuDdW69R.js.map} +1 -1
- package/frontend/dist/assets/{ResourcesPage-C5nynRqE.js → ResourcesPage-Cu-eD5e2.js} +2 -2
- package/frontend/dist/assets/{ResourcesPage-C5nynRqE.js.map → ResourcesPage-Cu-eD5e2.js.map} +1 -1
- package/frontend/dist/assets/{ServersPage-Ca5W9cPW.js → ServersPage-B1Np5x3N.js} +8 -8
- package/frontend/dist/assets/ServersPage-B1Np5x3N.js.map +1 -0
- package/frontend/dist/assets/{SettingsPage-DgFBjp_O.js → SettingsPage-BQN8-ZQh.js} +4 -4
- package/frontend/dist/assets/SettingsPage-BQN8-ZQh.js.map +1 -0
- package/frontend/dist/assets/{StatusDot-CmSkukx0.js → StatusDot-WyvgqplP.js} +2 -2
- package/frontend/dist/assets/{StatusDot-CmSkukx0.js.map → StatusDot-WyvgqplP.js.map} +1 -1
- package/frontend/dist/assets/{ToggleGroup-sG_2LQom.js → ToggleGroup-CmeedsZU.js} +2 -2
- package/frontend/dist/assets/{ToggleGroup-sG_2LQom.js.map → ToggleGroup-CmeedsZU.js.map} +1 -1
- package/frontend/dist/assets/UsersPage-DqDFrIIW.js +2 -0
- package/frontend/dist/assets/UsersPage-DqDFrIIW.js.map +1 -0
- package/frontend/dist/assets/contextCost-CilWqGWT.js +2 -0
- package/frontend/dist/assets/contextCost-CilWqGWT.js.map +1 -0
- package/frontend/dist/assets/{framework-vendor-I9Y6AgD5.js → framework-vendor-BHAQX05P.js} +3 -3
- package/frontend/dist/assets/{framework-vendor-I9Y6AgD5.js.map → framework-vendor-BHAQX05P.js.map} +1 -1
- package/frontend/dist/assets/{i18n-vendor-y9bLzIeh.js → i18n-vendor-CMcB7XUR.js} +2 -2
- package/frontend/dist/assets/{i18n-vendor-y9bLzIeh.js.map → i18n-vendor-CMcB7XUR.js.map} +1 -1
- package/frontend/dist/assets/{icons-vendor-BFl4A9fq.js → icons-vendor-HZHHWeTa.js} +2 -2
- package/frontend/dist/assets/{icons-vendor-BFl4A9fq.js.map → icons-vendor-HZHHWeTa.js.map} +1 -1
- package/frontend/dist/assets/index-BvU2-VT8.js +3 -0
- package/frontend/dist/assets/{index-CLugsCsy.js.map → index-BvU2-VT8.js.map} +1 -1
- package/frontend/dist/assets/index-DtrCVU1h.css +1 -0
- package/frontend/dist/assets/passwordValidation-DTLWgUy9.js +2 -0
- package/frontend/dist/assets/passwordValidation-DTLWgUy9.js.map +1 -0
- package/frontend/dist/assets/{resourceService-33rS8QGJ.js → resourceService-BFH4w47h.js} +2 -2
- package/frontend/dist/assets/{resourceService-33rS8QGJ.js.map → resourceService-BFH4w47h.js.map} +1 -1
- package/frontend/dist/assets/useCostData-CoM1SxdS.js +2 -0
- package/frontend/dist/assets/useCostData-CoM1SxdS.js.map +1 -0
- package/frontend/dist/assets/useSettingsData-DKrECrG1.js +2 -0
- package/frontend/dist/assets/{useSettingsData-fK2bv9oN.js.map → useSettingsData-DKrECrG1.js.map} +1 -1
- package/frontend/dist/assets/variableDetection-CIrYDjrq.js +16 -0
- package/frontend/dist/assets/variableDetection-CIrYDjrq.js.map +1 -0
- package/frontend/dist/index.html +5 -5
- package/package.json +2 -2
- package/frontend/dist/assets/ConfirmDialog-CYfXEuyU.js +0 -2
- package/frontend/dist/assets/ConfirmDialog-CYfXEuyU.js.map +0 -1
- package/frontend/dist/assets/Dashboard-ZYVsSvOw.js +0 -2
- package/frontend/dist/assets/GroupsPage-JPtupGpI.js +0 -33
- package/frontend/dist/assets/MarketPage-BGnQQyXK.js +0 -3
- package/frontend/dist/assets/MarketPage-BGnQQyXK.js.map +0 -1
- package/frontend/dist/assets/ServersPage-Ca5W9cPW.js.map +0 -1
- package/frontend/dist/assets/SettingsPage-DgFBjp_O.js.map +0 -1
- package/frontend/dist/assets/UsersPage-PHkFXLL8.js +0 -2
- package/frontend/dist/assets/UsersPage-PHkFXLL8.js.map +0 -1
- package/frontend/dist/assets/contextCost-D_iNNbhX.js +0 -2
- package/frontend/dist/assets/contextCost-D_iNNbhX.js.map +0 -1
- package/frontend/dist/assets/index-CGvImZ3F.css +0 -1
- package/frontend/dist/assets/index-CLugsCsy.js +0 -3
- package/frontend/dist/assets/useSettingsData-fK2bv9oN.js +0 -2
- package/frontend/dist/assets/variableDetection-ByJ_cnxa.js +0 -16
- package/frontend/dist/assets/variableDetection-ByJ_cnxa.js.map +0 -1
|
@@ -2,6 +2,7 @@ import os from 'os';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import treeKill from 'tree-kill';
|
|
5
|
+
import { isProcessTreeKillAvailable } from '../utils/processTree.js';
|
|
5
6
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
7
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
7
8
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
@@ -16,6 +17,7 @@ import { ResilientJsonSchemaValidator } from '../utils/jsonSchemaValidator.js';
|
|
|
16
17
|
import { getUserDao } from '../dao/index.js';
|
|
17
18
|
import { expandEnvVars, replaceEnvVars, getNameSeparator } from '../config/index.js';
|
|
18
19
|
import config from '../config/index.js';
|
|
20
|
+
import { validateServerName } from '../utils/serverNameValidation.js';
|
|
19
21
|
import { getGroup } from './sseService.js';
|
|
20
22
|
import { getServerConfigInGroup, normalizeGroupServers } from './groupService.js';
|
|
21
23
|
import { removeServerToolEmbeddings, saveToolsAsVectorEmbeddings } from './vectorSearchService.js';
|
|
@@ -35,6 +37,7 @@ import { MCP_APPS_CAPABILITIES, filterModelVisibleTools, hasMcpAppsCapability, i
|
|
|
35
37
|
import { supportsCacheRefresh, injectRefreshFlag, clearRunnerCache } from '../utils/cacheUtils.js';
|
|
36
38
|
const servers = {};
|
|
37
39
|
import { setupClientKeepAlive } from './keepAliveService.js';
|
|
40
|
+
import { logger } from '../utils/logger.js';
|
|
38
41
|
/**
|
|
39
42
|
* Check if proxychains4 is available on the system (Linux/macOS only).
|
|
40
43
|
* Returns the path to proxychains4 if found, null otherwise.
|
|
@@ -77,12 +80,12 @@ const generateProxychainsConfig = (serverName, proxyConfig) => {
|
|
|
77
80
|
if (fs.existsSync(proxyConfig.configPath)) {
|
|
78
81
|
return proxyConfig.configPath;
|
|
79
82
|
}
|
|
80
|
-
|
|
83
|
+
logger.warn(`[${serverName}] Custom proxychains config not found: ${proxyConfig.configPath}`);
|
|
81
84
|
return null;
|
|
82
85
|
}
|
|
83
86
|
// Validate required fields
|
|
84
87
|
if (!proxyConfig.host || !proxyConfig.port) {
|
|
85
|
-
|
|
88
|
+
logger.warn(`[${serverName}] Proxy host and port are required for proxychains4`);
|
|
86
89
|
return null;
|
|
87
90
|
}
|
|
88
91
|
const proxyType = proxyConfig.type || 'socks5';
|
|
@@ -114,7 +117,7 @@ ${proxyLine}
|
|
|
114
117
|
// Write config file
|
|
115
118
|
const configPath = path.join(tempDir, `${serverName.replace(/[^a-zA-Z0-9-_]/g, '_')}.conf`);
|
|
116
119
|
fs.writeFileSync(configPath, configContent, 'utf-8');
|
|
117
|
-
|
|
120
|
+
logger.log(`[${serverName}] Generated proxychains4 config: ${configPath}`);
|
|
118
121
|
return configPath;
|
|
119
122
|
};
|
|
120
123
|
/**
|
|
@@ -124,9 +127,20 @@ ${proxyLine}
|
|
|
124
127
|
const isSafeCommand = (command) => {
|
|
125
128
|
// Block shell builtins that could be used for command injection
|
|
126
129
|
const blockedCommands = new Set([
|
|
127
|
-
'sh',
|
|
128
|
-
'
|
|
129
|
-
'
|
|
130
|
+
'sh',
|
|
131
|
+
'bash',
|
|
132
|
+
'zsh',
|
|
133
|
+
'fish',
|
|
134
|
+
'csh',
|
|
135
|
+
'ksh',
|
|
136
|
+
'tcsh',
|
|
137
|
+
'cmd',
|
|
138
|
+
'powershell',
|
|
139
|
+
'pwsh',
|
|
140
|
+
'eval',
|
|
141
|
+
'exec',
|
|
142
|
+
'source',
|
|
143
|
+
'.',
|
|
130
144
|
]);
|
|
131
145
|
const basename = command.split('/').pop()?.split('\\').pop()?.toLowerCase() || '';
|
|
132
146
|
if (blockedCommands.has(basename)) {
|
|
@@ -143,7 +157,7 @@ const isSafeCommand = (command) => {
|
|
|
143
157
|
* Removes shell metacharacters and dangerous patterns.
|
|
144
158
|
*/
|
|
145
159
|
const sanitizeArgs = (args) => {
|
|
146
|
-
return args.map(arg => {
|
|
160
|
+
return args.map((arg) => {
|
|
147
161
|
// Remove shell metacharacters that could be used for injection
|
|
148
162
|
// Allow common flags (-f, --flag, -vvv) and paths
|
|
149
163
|
if (/^[a-zA-Z0-9._/\\:@=+,-]+$/.test(arg)) {
|
|
@@ -151,7 +165,7 @@ const sanitizeArgs = (args) => {
|
|
|
151
165
|
}
|
|
152
166
|
// For arguments with special characters, log a warning
|
|
153
167
|
// Also block redirection operators (>, <, >>) and newlines
|
|
154
|
-
|
|
168
|
+
logger.warn(`[proxychains] Potentially unsafe argument blocked: ${arg}`);
|
|
155
169
|
return arg.replace(/[;&|`$(){}[\]!><\n\r]/g, '');
|
|
156
170
|
});
|
|
157
171
|
};
|
|
@@ -166,30 +180,30 @@ const wrapWithProxychains = (serverName, command, args, proxyConfig) => {
|
|
|
166
180
|
}
|
|
167
181
|
// Check platform - Windows is not supported
|
|
168
182
|
if (process.platform === 'win32') {
|
|
169
|
-
|
|
183
|
+
logger.warn(`[${serverName}] proxychains4 proxy is not supported on Windows, ignoring proxy configuration`);
|
|
170
184
|
return { command, args };
|
|
171
185
|
}
|
|
172
186
|
// SECURITY: Validate command is safe
|
|
173
187
|
if (!isSafeCommand(command)) {
|
|
174
|
-
|
|
188
|
+
logger.error(`[${serverName}] Blocked unsafe command for proxychains4 wrapping: ${command}`);
|
|
175
189
|
throw new Error(`[${serverName}] Unsafe command blocked: ${command}. Shell builtins and metacharacters are not allowed.`);
|
|
176
190
|
}
|
|
177
191
|
// Find proxychains4 binary
|
|
178
192
|
const proxychains4Path = findProxychains4();
|
|
179
193
|
if (!proxychains4Path) {
|
|
180
|
-
|
|
194
|
+
logger.warn(`[${serverName}] proxychains4 not found on system, install it with: apt install proxychains4 (Debian/Ubuntu) or brew install proxychains-ng (macOS)`);
|
|
181
195
|
return { command, args };
|
|
182
196
|
}
|
|
183
197
|
// Generate or get config file
|
|
184
198
|
const configPath = generateProxychainsConfig(serverName, proxyConfig);
|
|
185
199
|
if (!configPath) {
|
|
186
|
-
|
|
200
|
+
logger.warn(`[${serverName}] Failed to setup proxychains4 configuration, skipping proxy`);
|
|
187
201
|
return { command, args };
|
|
188
202
|
}
|
|
189
203
|
// SECURITY: Sanitize arguments
|
|
190
204
|
const sanitizedArgs = sanitizeArgs(args);
|
|
191
205
|
// Wrap command with proxychains4
|
|
192
|
-
|
|
206
|
+
logger.log(`[${serverName}] Using proxychains4 proxy: ${proxyConfig.type || 'socks5'}://${proxyConfig.host}:${proxyConfig.port}`);
|
|
193
207
|
return {
|
|
194
208
|
command: proxychains4Path,
|
|
195
209
|
args: ['-f', configPath, command, ...sanitizedArgs],
|
|
@@ -237,7 +251,7 @@ export const getMcpServer = async (sessionId, group) => {
|
|
|
237
251
|
servers[sessionId] = createMcpServer(descriptor.name, descriptor.version, descriptor);
|
|
238
252
|
}
|
|
239
253
|
else {
|
|
240
|
-
|
|
254
|
+
logger.log(`MCP server already exists for sessionId: ${sessionId}`);
|
|
241
255
|
}
|
|
242
256
|
return servers[sessionId];
|
|
243
257
|
};
|
|
@@ -255,7 +269,7 @@ const closeIsolatedClient = (serverName, client, transport) => {
|
|
|
255
269
|
client.close();
|
|
256
270
|
}
|
|
257
271
|
catch (e) {
|
|
258
|
-
|
|
272
|
+
logger.warn(`[${serverName}] Error closing isolated client:`, e);
|
|
259
273
|
}
|
|
260
274
|
const candidateTransport = transport;
|
|
261
275
|
const stdioPid = typeof candidateTransport.pid === 'number' ? candidateTransport.pid : null;
|
|
@@ -263,7 +277,7 @@ const closeIsolatedClient = (serverName, client, transport) => {
|
|
|
263
277
|
transport.close();
|
|
264
278
|
}
|
|
265
279
|
catch (e) {
|
|
266
|
-
|
|
280
|
+
logger.warn(`[${serverName}] Error closing isolated transport:`, e);
|
|
267
281
|
}
|
|
268
282
|
// For stdio transports, kill the whole process tree to avoid orphans
|
|
269
283
|
if (stdioPid) {
|
|
@@ -287,7 +301,7 @@ const cleanupIsolatedSession = (sessionId) => {
|
|
|
287
301
|
isolatedClientCreationLocks.delete(key);
|
|
288
302
|
}
|
|
289
303
|
}
|
|
290
|
-
|
|
304
|
+
logger.log(`Cleaned up isolated clients for session: ${sessionId}`);
|
|
291
305
|
}
|
|
292
306
|
// Drop per-session OpenAPI cookie jars so authenticated state does not
|
|
293
307
|
// outlive the downstream session.
|
|
@@ -355,12 +369,12 @@ const getOrCreateIsolatedClient = async (sessionId, serverInfo) => {
|
|
|
355
369
|
// Guard: the session was cleaned up during the async connect if its map was
|
|
356
370
|
// deleted (identity no longer matches) — close the just-created connection.
|
|
357
371
|
if (sessionIsolatedClients.get(sessionId) !== reservedClients) {
|
|
358
|
-
|
|
372
|
+
logger.warn(`Session ${sessionId} was deleted during isolated client creation for ${serverInfo.name}, closing new client`);
|
|
359
373
|
closeIsolatedClient(serverInfo.name, client, transport);
|
|
360
374
|
throw new Error(`Session ${sessionId} no longer exists`);
|
|
361
375
|
}
|
|
362
376
|
setSessionIsolatedClient(sessionId, serverInfo.name, client, transport);
|
|
363
|
-
|
|
377
|
+
logger.log(`Created isolated client for session ${sessionId} -> ${serverInfo.name}`);
|
|
364
378
|
return { client, transport };
|
|
365
379
|
})();
|
|
366
380
|
isolatedClientCreationLocks.set(lockKey, createPromise);
|
|
@@ -382,10 +396,10 @@ const broadcastListChanged = (listType, sendNotification) => {
|
|
|
382
396
|
Object.values(servers).forEach((server) => {
|
|
383
397
|
sendNotification(server)
|
|
384
398
|
.then(() => {
|
|
385
|
-
|
|
399
|
+
logger.log(`${listType} list changed notification sent successfully`);
|
|
386
400
|
})
|
|
387
401
|
.catch((error) => {
|
|
388
|
-
|
|
402
|
+
logger.warn(`Failed to send ${listType} list changed notification:`, error.message);
|
|
389
403
|
});
|
|
390
404
|
});
|
|
391
405
|
};
|
|
@@ -416,12 +430,12 @@ export const updateServerInfoVisibility = (serverName, visibility, sharedWithUse
|
|
|
416
430
|
export const syncToolEmbedding = async (serverName, toolName) => {
|
|
417
431
|
const serverInfo = getServerByName(serverName);
|
|
418
432
|
if (!serverInfo) {
|
|
419
|
-
|
|
433
|
+
logger.warn(`Server not found: ${serverName}`);
|
|
420
434
|
return;
|
|
421
435
|
}
|
|
422
436
|
const tool = serverInfo.tools.find((t) => t.name === toolName);
|
|
423
437
|
if (!tool) {
|
|
424
|
-
|
|
438
|
+
logger.warn(`Tool not found: ${toolName} on server: ${serverName}`);
|
|
425
439
|
return;
|
|
426
440
|
}
|
|
427
441
|
if (isAppOnlyTool(tool)) {
|
|
@@ -429,8 +443,8 @@ export const syncToolEmbedding = async (serverName, toolName) => {
|
|
|
429
443
|
}
|
|
430
444
|
// Save tool as vector embedding for search
|
|
431
445
|
syncToolsAsVectorEmbeddings(serverName, [tool]).catch((error) => {
|
|
432
|
-
|
|
433
|
-
|
|
446
|
+
logger.warn(`[EMBED_SYNC_ERROR] Failed to sync embedding for tool "${toolName}" on server "${serverName}"`);
|
|
447
|
+
logger.error('Error syncing single tool embedding', { serverName, toolName, error });
|
|
434
448
|
});
|
|
435
449
|
};
|
|
436
450
|
// Helper function to clean $schema field from inputSchema
|
|
@@ -540,7 +554,7 @@ export const updateServerToolsCache = (serverInfo, tools, options) => {
|
|
|
540
554
|
syncToolsAsVectorEmbeddings(serverInfo.name, serverInfo.tools, {
|
|
541
555
|
reportProgress: options?.reportEmbeddingProgress === true,
|
|
542
556
|
}).catch(() => {
|
|
543
|
-
|
|
557
|
+
logger.warn('[EMBED_SYNC_ERROR] Failed to sync tool embeddings');
|
|
544
558
|
});
|
|
545
559
|
};
|
|
546
560
|
const updateServerPromptsCache = (serverInfo, prompts) => {
|
|
@@ -550,7 +564,7 @@ const updateServerResourcesCache = (serverInfo, resources) => {
|
|
|
550
564
|
serverInfo.resources = resources.map(normalizeResourceForCache);
|
|
551
565
|
};
|
|
552
566
|
const logListChangedRefreshError = (listType) => {
|
|
553
|
-
|
|
567
|
+
logger.warn(`Failed to refresh ${listType} list after upstream notification`);
|
|
554
568
|
};
|
|
555
569
|
const createUpstreamMcpClient = (name, getServerInfo) => {
|
|
556
570
|
return new Client({
|
|
@@ -661,7 +675,7 @@ export const cleanupAllServers = () => {
|
|
|
661
675
|
}
|
|
662
676
|
}
|
|
663
677
|
catch (error) {
|
|
664
|
-
|
|
678
|
+
logger.warn('Error closing server', { serverName: serverInfo.name, error });
|
|
665
679
|
}
|
|
666
680
|
}
|
|
667
681
|
serverInfos = [];
|
|
@@ -920,9 +934,7 @@ export const createTransportFromConfig = async (name, conf) => {
|
|
|
920
934
|
// Admin-owned servers may legitimately target internal services, so they
|
|
921
935
|
// skip the internal-IP blocklist. allowInternal also governs per-hop
|
|
922
936
|
// redirect validation in createRedirectValidatingFetch below.
|
|
923
|
-
const ownerUser = conf.owner
|
|
924
|
-
? await getUserDao().findByUsername(conf.owner)
|
|
925
|
-
: null;
|
|
937
|
+
const ownerUser = conf.owner ? await getUserDao().findByUsername(conf.owner) : null;
|
|
926
938
|
const allowInternal = !!ownerUser?.isAdmin;
|
|
927
939
|
if (conf.url) {
|
|
928
940
|
await assertSafeUrl(conf.url, { allowInternal });
|
|
@@ -943,7 +955,7 @@ export const createTransportFromConfig = async (name, conf) => {
|
|
|
943
955
|
// Authorization value would override the valid access token and cause
|
|
944
956
|
// 401 re-authorization loops.
|
|
945
957
|
headers = stripAuthorizationHeader(headers);
|
|
946
|
-
|
|
958
|
+
logger.log(`OAuth provider configured for server: ${name}`);
|
|
947
959
|
}
|
|
948
960
|
if (Object.keys(headers).length > 0) {
|
|
949
961
|
options.requestInit = {
|
|
@@ -965,7 +977,7 @@ export const createTransportFromConfig = async (name, conf) => {
|
|
|
965
977
|
options.authProvider = authProvider;
|
|
966
978
|
// Drop static Authorization header when OAuth manages auth (see above).
|
|
967
979
|
headers = stripAuthorizationHeader(headers);
|
|
968
|
-
|
|
980
|
+
logger.log(`OAuth provider configured for server: ${name}`);
|
|
969
981
|
}
|
|
970
982
|
if (Object.keys(headers).length > 0) {
|
|
971
983
|
options.eventSourceInit = {
|
|
@@ -1009,7 +1021,7 @@ export const createTransportFromConfig = async (name, conf) => {
|
|
|
1009
1021
|
if (pendingReinstalls.has(name)) {
|
|
1010
1022
|
resolvedArgs = injectRefreshFlag(conf.command, resolvedArgs);
|
|
1011
1023
|
pendingReinstalls.delete(name);
|
|
1012
|
-
|
|
1024
|
+
logger.log(`[${name}] Injected cache refresh flags for reinstall`);
|
|
1013
1025
|
}
|
|
1014
1026
|
const { command: finalCommand, args: finalArgs } = wrapWithProxychains(name, conf.command, resolvedArgs, conf.proxy);
|
|
1015
1027
|
// Create STDIO transport with potentially wrapped command
|
|
@@ -1022,10 +1034,10 @@ export const createTransportFromConfig = async (name, conf) => {
|
|
|
1022
1034
|
});
|
|
1023
1035
|
if (transport.stderr) {
|
|
1024
1036
|
observeStdioStderr(transport, transport.stderr, (message) => {
|
|
1025
|
-
|
|
1037
|
+
logger.log('Upstream server stderr', JSON.stringify({
|
|
1026
1038
|
serverName: name,
|
|
1027
1039
|
message,
|
|
1028
|
-
});
|
|
1040
|
+
}));
|
|
1029
1041
|
});
|
|
1030
1042
|
}
|
|
1031
1043
|
}
|
|
@@ -1057,7 +1069,7 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
|
|
|
1057
1069
|
const isStreamableHttp = transport instanceof StreamableHTTPClientTransport;
|
|
1058
1070
|
const isSSE = transport instanceof SSEClientTransport;
|
|
1059
1071
|
if (attempt < maxRetries && transport && ((isStreamableHttp && isHttp40xError) || isSSE)) {
|
|
1060
|
-
|
|
1072
|
+
logger.warn(`${isHttp40xError ? 'HTTP 40x error' : 'error'} detected for ${isStreamableHttp ? 'StreamableHTTP' : 'SSE'} server ${serverInfo.name}${isolated ? ` (isolated session ${isolated.sessionId})` : ''}, attempting reconnection (attempt ${attempt + 1}/${maxRetries + 1})`);
|
|
1061
1073
|
try {
|
|
1062
1074
|
const server = await getServerDao().findById(serverInfo.name);
|
|
1063
1075
|
if (!server) {
|
|
@@ -1074,11 +1086,15 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
|
|
|
1074
1086
|
try {
|
|
1075
1087
|
client.close();
|
|
1076
1088
|
}
|
|
1077
|
-
catch {
|
|
1089
|
+
catch {
|
|
1090
|
+
/* empty */
|
|
1091
|
+
}
|
|
1078
1092
|
try {
|
|
1079
1093
|
transport.close();
|
|
1080
1094
|
}
|
|
1081
|
-
catch {
|
|
1095
|
+
catch {
|
|
1096
|
+
/* empty */
|
|
1097
|
+
}
|
|
1082
1098
|
setSessionIsolatedClient(isolated.sessionId, serverInfo.name, newClient, newTransport);
|
|
1083
1099
|
}
|
|
1084
1100
|
else {
|
|
@@ -1090,11 +1106,15 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
|
|
|
1090
1106
|
try {
|
|
1091
1107
|
serverInfo.client?.close();
|
|
1092
1108
|
}
|
|
1093
|
-
catch {
|
|
1109
|
+
catch {
|
|
1110
|
+
/* empty */
|
|
1111
|
+
}
|
|
1094
1112
|
try {
|
|
1095
1113
|
transport.close();
|
|
1096
1114
|
}
|
|
1097
|
-
catch {
|
|
1115
|
+
catch {
|
|
1116
|
+
/* empty */
|
|
1117
|
+
}
|
|
1098
1118
|
serverInfo.client = newClient;
|
|
1099
1119
|
serverInfo.transport = newTransport;
|
|
1100
1120
|
serverInfo.status = 'connected';
|
|
@@ -1108,18 +1128,18 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
|
|
|
1108
1128
|
updateServerToolsCache(serverInfo, tools.tools);
|
|
1109
1129
|
}
|
|
1110
1130
|
catch (listToolsError) {
|
|
1111
|
-
|
|
1131
|
+
logger.warn('Failed to reload tools after reconnection', {
|
|
1112
1132
|
serverName: serverInfo.name,
|
|
1113
1133
|
error: summarizeErrorForLogging(listToolsError),
|
|
1114
1134
|
});
|
|
1115
1135
|
// Continue anyway, as the connection might still work for the current tool
|
|
1116
1136
|
}
|
|
1117
|
-
|
|
1137
|
+
logger.log(`Successfully reconnected to server: ${serverInfo.name}`);
|
|
1118
1138
|
// Continue to next attempt
|
|
1119
1139
|
continue;
|
|
1120
1140
|
}
|
|
1121
1141
|
catch (reconnectError) {
|
|
1122
|
-
|
|
1142
|
+
logger.error('Failed to reconnect to server', {
|
|
1123
1143
|
serverName: serverInfo.name,
|
|
1124
1144
|
error: summarizeErrorForLogging(reconnectError),
|
|
1125
1145
|
});
|
|
@@ -1153,7 +1173,7 @@ const setupServerKeepAlive = (serverInfo, serverConfig) => {
|
|
|
1153
1173
|
throw error;
|
|
1154
1174
|
}
|
|
1155
1175
|
},
|
|
1156
|
-
}).catch((error) =>
|
|
1176
|
+
}).catch((error) => logger.warn('Keepalive setup failed', {
|
|
1157
1177
|
serverName: serverInfo.name,
|
|
1158
1178
|
error: summarizeErrorForLogging(error),
|
|
1159
1179
|
}));
|
|
@@ -1166,11 +1186,20 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1166
1186
|
try {
|
|
1167
1187
|
for (const conf of allServers) {
|
|
1168
1188
|
const { name } = conf;
|
|
1189
|
+
// Names loaded from disk are not rejected (that would break existing
|
|
1190
|
+
// configs on upgrade), but a strict client may drop the whole tools/list
|
|
1191
|
+
// if the server name produces a non-conforming downstream tool name.
|
|
1192
|
+
if (isInit) {
|
|
1193
|
+
const nameValidation = validateServerName(name);
|
|
1194
|
+
if (!nameValidation.valid) {
|
|
1195
|
+
logger.warn(`Server name '${name}' does not match the MCP tool-name charset (${nameValidation.message}); downstream tool names may be rejected by strict clients`);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1169
1198
|
// Expand environment variables in all configuration values
|
|
1170
1199
|
const expandedConf = replaceEnvVars(conf);
|
|
1171
1200
|
// Skip disabled servers
|
|
1172
1201
|
if (expandedConf.enabled === false) {
|
|
1173
|
-
|
|
1202
|
+
logger.log(`Skipping disabled server: ${name}`);
|
|
1174
1203
|
nextServerInfos.push({
|
|
1175
1204
|
name,
|
|
1176
1205
|
owner: expandedConf.owner,
|
|
@@ -1210,7 +1239,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1210
1239
|
enabled: true,
|
|
1211
1240
|
config: expandedConf,
|
|
1212
1241
|
});
|
|
1213
|
-
|
|
1242
|
+
logger.log(`Skipping startup connect for on-demand server: ${name}`);
|
|
1214
1243
|
continue;
|
|
1215
1244
|
}
|
|
1216
1245
|
// Reuse this server's existing runtime state instead of reconnecting when:
|
|
@@ -1226,8 +1255,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1226
1255
|
const existingServer = existingServerInfos.find((s) => s.name === name);
|
|
1227
1256
|
const isDifferentServer = Boolean(serverName) && serverName !== name;
|
|
1228
1257
|
const hasInflightOAuthAuthorization = existingServer?.status === 'oauth_required' &&
|
|
1229
|
-
Boolean(expandedConf.oauth?.pendingAuthorization?.state ||
|
|
1230
|
-
existingServer.oauth?.state);
|
|
1258
|
+
Boolean(expandedConf.oauth?.pendingAuthorization?.state || existingServer.oauth?.state);
|
|
1231
1259
|
if (existingServer &&
|
|
1232
1260
|
(isDifferentServer ||
|
|
1233
1261
|
(!serverName && (existingServer.status === 'connected' || hasInflightOAuthAuthorization)))) {
|
|
@@ -1238,7 +1266,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1238
1266
|
sharedWithUsers: expandedConf.sharedWithUsers,
|
|
1239
1267
|
enabled: expandedConf.enabled === undefined ? true : expandedConf.enabled,
|
|
1240
1268
|
});
|
|
1241
|
-
|
|
1269
|
+
logger.log(hasInflightOAuthAuthorization
|
|
1242
1270
|
? `Server '${name}' has an in-flight OAuth authorization; preserving existing state.`
|
|
1243
1271
|
: `Server '${name}' is already connected.`);
|
|
1244
1272
|
continue;
|
|
@@ -1248,7 +1276,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1248
1276
|
if (expandedConf.type === 'openapi') {
|
|
1249
1277
|
// Handle OpenAPI type servers
|
|
1250
1278
|
if (!expandedConf.openapi?.url && !expandedConf.openapi?.schema) {
|
|
1251
|
-
|
|
1279
|
+
logger.warn(`Skipping OpenAPI server '${name}': missing OpenAPI specification URL or schema`);
|
|
1252
1280
|
nextServerInfos.push({
|
|
1253
1281
|
name,
|
|
1254
1282
|
owner: expandedConf.owner,
|
|
@@ -1301,7 +1329,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1301
1329
|
});
|
|
1302
1330
|
},
|
|
1303
1331
|
});
|
|
1304
|
-
|
|
1332
|
+
logger.log(`Initializing OpenAPI server: ${name}...`);
|
|
1305
1333
|
// Perform async initialization
|
|
1306
1334
|
await openApiClient.initialize();
|
|
1307
1335
|
// Convert OpenAPI tools to MCP tool format
|
|
@@ -1315,7 +1343,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1315
1343
|
serverInfo.status = 'connected';
|
|
1316
1344
|
serverInfo.tools = mcpTools;
|
|
1317
1345
|
serverInfo.openApiClient = openApiClient;
|
|
1318
|
-
|
|
1346
|
+
logger.log(`Successfully initialized OpenAPI server: ${name} with ${mcpTools.length} tools`);
|
|
1319
1347
|
// Broadcast now that tools are loaded. OpenAPI servers expose tools
|
|
1320
1348
|
// only (no prompts/resources). See the standard-path note above.
|
|
1321
1349
|
broadcastToolListChanged();
|
|
@@ -1323,8 +1351,8 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1323
1351
|
syncToolsAsVectorEmbeddings(name, mcpTools, {
|
|
1324
1352
|
reportProgress: options?.reportEmbeddingProgress === true && serverName === name,
|
|
1325
1353
|
}).catch((error) => {
|
|
1326
|
-
|
|
1327
|
-
|
|
1354
|
+
logger.warn(`[EMBED_SYNC_ERROR] Failed to sync OpenAPI embeddings for server "${name}"`);
|
|
1355
|
+
logger.error('Error syncing OpenAPI tool embeddings', {
|
|
1328
1356
|
serverName: name,
|
|
1329
1357
|
error: summarizeErrorForLogging(error),
|
|
1330
1358
|
});
|
|
@@ -1332,7 +1360,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1332
1360
|
continue;
|
|
1333
1361
|
}
|
|
1334
1362
|
catch (error) {
|
|
1335
|
-
|
|
1363
|
+
logger.error('Failed to initialize OpenAPI server', {
|
|
1336
1364
|
serverName: name,
|
|
1337
1365
|
error: summarizeErrorForLogging(error),
|
|
1338
1366
|
});
|
|
@@ -1391,18 +1419,18 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1391
1419
|
nextServerInfos.push(serverInfo);
|
|
1392
1420
|
connectClientWithDiagnostics(client, transport, initRequestOptions || requestOptions)
|
|
1393
1421
|
.then(() => {
|
|
1394
|
-
|
|
1422
|
+
logger.log(`Successfully connected client for server: ${name}`);
|
|
1395
1423
|
const serverVersion = client.getServerVersion?.();
|
|
1396
1424
|
serverInfo.version = serverVersion?.version;
|
|
1397
1425
|
serverInfo.instructions = client.getInstructions?.();
|
|
1398
1426
|
const capabilities = client.getServerCapabilities();
|
|
1399
|
-
|
|
1427
|
+
logger.log('Server capabilities', JSON.stringify(capabilities));
|
|
1400
1428
|
let dataError = null;
|
|
1401
1429
|
if (capabilities?.tools) {
|
|
1402
1430
|
client
|
|
1403
1431
|
.listTools({}, initRequestOptions || requestOptions)
|
|
1404
1432
|
.then((tools) => {
|
|
1405
|
-
|
|
1433
|
+
logger.log(`Successfully listed ${tools.tools.length} tools for server: ${name}`);
|
|
1406
1434
|
updateServerToolsCache(serverInfo, tools.tools, {
|
|
1407
1435
|
reportEmbeddingProgress: options?.reportEmbeddingProgress === true && serverName === name,
|
|
1408
1436
|
});
|
|
@@ -1413,7 +1441,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1413
1441
|
broadcastToolListChanged();
|
|
1414
1442
|
})
|
|
1415
1443
|
.catch((error) => {
|
|
1416
|
-
|
|
1444
|
+
logger.error('Failed to list tools for server', {
|
|
1417
1445
|
serverName: name,
|
|
1418
1446
|
error: summarizeErrorForLogging(error),
|
|
1419
1447
|
});
|
|
@@ -1424,12 +1452,12 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1424
1452
|
client
|
|
1425
1453
|
.listPrompts({}, initRequestOptions || requestOptions)
|
|
1426
1454
|
.then((prompts) => {
|
|
1427
|
-
|
|
1455
|
+
logger.log(`Successfully listed ${prompts.prompts.length} prompts for server: ${name}`);
|
|
1428
1456
|
updateServerPromptsCache(serverInfo, prompts.prompts);
|
|
1429
1457
|
broadcastPromptListChanged();
|
|
1430
1458
|
})
|
|
1431
1459
|
.catch((error) => {
|
|
1432
|
-
|
|
1460
|
+
logger.error('Failed to list prompts for server', {
|
|
1433
1461
|
serverName: name,
|
|
1434
1462
|
error: summarizeErrorForLogging(error),
|
|
1435
1463
|
});
|
|
@@ -1440,12 +1468,12 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1440
1468
|
client
|
|
1441
1469
|
.listResources({}, initRequestOptions || requestOptions)
|
|
1442
1470
|
.then((resources) => {
|
|
1443
|
-
|
|
1471
|
+
logger.log(`Successfully listed ${resources.resources.length} resources for server: ${name}`);
|
|
1444
1472
|
updateServerResourcesCache(serverInfo, resources.resources);
|
|
1445
1473
|
broadcastResourceListChanged();
|
|
1446
1474
|
})
|
|
1447
1475
|
.catch((error) => {
|
|
1448
|
-
|
|
1476
|
+
logger.error('Failed to list resources for server', {
|
|
1449
1477
|
serverName: name,
|
|
1450
1478
|
error: summarizeErrorForLogging(error),
|
|
1451
1479
|
});
|
|
@@ -1471,7 +1499,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1471
1499
|
if (isOAuthError) {
|
|
1472
1500
|
// OAuth provider should have already set the status to 'oauth_required'
|
|
1473
1501
|
// and stored the authorization URL in serverInfo.oauth
|
|
1474
|
-
|
|
1502
|
+
logger.log(`OAuth authorization required for server ${name}. Status should be set to 'oauth_required'.`);
|
|
1475
1503
|
// Make sure status is set correctly
|
|
1476
1504
|
if (serverInfo.status !== 'oauth_required') {
|
|
1477
1505
|
serverInfo.status = 'oauth_required';
|
|
@@ -1479,7 +1507,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1479
1507
|
serverInfo.error = null;
|
|
1480
1508
|
}
|
|
1481
1509
|
else {
|
|
1482
|
-
|
|
1510
|
+
logger.error('Failed to connect client for server', {
|
|
1483
1511
|
serverName: name,
|
|
1484
1512
|
error: summarizeErrorForLogging(error),
|
|
1485
1513
|
});
|
|
@@ -1489,7 +1517,7 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1489
1517
|
setupServerKeepAlive(serverInfo, expandedConf);
|
|
1490
1518
|
}
|
|
1491
1519
|
});
|
|
1492
|
-
|
|
1520
|
+
logger.log(`Initialized client for server: ${name}`);
|
|
1493
1521
|
}
|
|
1494
1522
|
}
|
|
1495
1523
|
catch (error) {
|
|
@@ -1650,14 +1678,14 @@ export const getServerByOAuthState = (state) => {
|
|
|
1650
1678
|
* This will close the existing connection and reinitialize the server
|
|
1651
1679
|
*/
|
|
1652
1680
|
export const reconnectServer = async (serverName) => {
|
|
1653
|
-
|
|
1681
|
+
logger.log(`Reconnecting server: ${serverName}`);
|
|
1654
1682
|
const serverInfo = getServerByName(serverName);
|
|
1655
1683
|
if (!serverInfo) {
|
|
1656
1684
|
throw new Error(`Server not found: ${serverName}`);
|
|
1657
1685
|
}
|
|
1658
1686
|
const serverConfig = await getServerDao().findById(serverName);
|
|
1659
1687
|
if (serverConfig?.enabled === false) {
|
|
1660
|
-
|
|
1688
|
+
logger.log(`Skipping reconnect for disabled server: ${serverName}`);
|
|
1661
1689
|
return;
|
|
1662
1690
|
}
|
|
1663
1691
|
// Close existing connection if any
|
|
@@ -1666,7 +1694,7 @@ export const reconnectServer = async (serverName) => {
|
|
|
1666
1694
|
serverInfo.client.close();
|
|
1667
1695
|
}
|
|
1668
1696
|
catch (error) {
|
|
1669
|
-
|
|
1697
|
+
logger.warn('Error closing client for server', { serverName, error });
|
|
1670
1698
|
}
|
|
1671
1699
|
}
|
|
1672
1700
|
if (serverInfo.transport) {
|
|
@@ -1674,7 +1702,7 @@ export const reconnectServer = async (serverName) => {
|
|
|
1674
1702
|
serverInfo.transport.close();
|
|
1675
1703
|
}
|
|
1676
1704
|
catch (error) {
|
|
1677
|
-
|
|
1705
|
+
logger.warn('Error closing transport for server', { serverName, error });
|
|
1678
1706
|
}
|
|
1679
1707
|
}
|
|
1680
1708
|
if (serverInfo.keepAliveIntervalId) {
|
|
@@ -1683,13 +1711,13 @@ export const reconnectServer = async (serverName) => {
|
|
|
1683
1711
|
}
|
|
1684
1712
|
// Reinitialize the server
|
|
1685
1713
|
await initializeClientsFromSettings(false, serverName);
|
|
1686
|
-
|
|
1714
|
+
logger.log(`Successfully reconnected server: ${serverName}`);
|
|
1687
1715
|
};
|
|
1688
1716
|
// Reinstall server: clear package cache and reconnect.
|
|
1689
1717
|
// For npx: deletes ~/.npm/_npx before reconnect (--ignore-existing removed in npm 7+).
|
|
1690
1718
|
// For uvx: schedules --refresh flag injection on next spawn via pendingReinstalls Set.
|
|
1691
1719
|
export const reinstallServer = async (serverName) => {
|
|
1692
|
-
|
|
1720
|
+
logger.log(`Reinstalling server: ${serverName}`);
|
|
1693
1721
|
const serverInfo = getServerByName(serverName);
|
|
1694
1722
|
if (!serverInfo) {
|
|
1695
1723
|
throw new Error(`Server not found: ${serverName}`);
|
|
@@ -1713,7 +1741,7 @@ export const reinstallServer = async (serverName) => {
|
|
|
1713
1741
|
await clearRunnerCache(command);
|
|
1714
1742
|
// Close and reconnect (will pick up pendingReinstalls flag for uvx)
|
|
1715
1743
|
await reconnectServer(serverName);
|
|
1716
|
-
|
|
1744
|
+
logger.log(`Successfully initiated reinstall for server: ${serverName}`);
|
|
1717
1745
|
}
|
|
1718
1746
|
catch (error) {
|
|
1719
1747
|
// Clean up pendingReinstalls on failure to avoid stale entries
|
|
@@ -1738,6 +1766,42 @@ const filterToolsByConfig = async (serverName, tools) => {
|
|
|
1738
1766
|
const getServerByTool = (toolName) => {
|
|
1739
1767
|
return getVisibleServerInfos().find((serverInfo) => serverInfo.enabled !== false && serverInfo.tools.some((tool) => tool.name === toolName));
|
|
1740
1768
|
};
|
|
1769
|
+
/**
|
|
1770
|
+
* Error thrown when the requested server/tool (or server/prompt) cannot be
|
|
1771
|
+
* used by the current caller. `message` is the single safe text returned to
|
|
1772
|
+
* the caller — intentionally identical whether the target is hidden from them
|
|
1773
|
+
* or does not exist at all, so server/tool existence is never leaked through
|
|
1774
|
+
* distinguishable authorization errors (see #1103). `reason` is an internal
|
|
1775
|
+
* diagnostic recorded only in logs/activity, never surfaced to the caller.
|
|
1776
|
+
*/
|
|
1777
|
+
export class ToolUnavailableError extends Error {
|
|
1778
|
+
constructor(message, reason) {
|
|
1779
|
+
super(message);
|
|
1780
|
+
this.reason = reason;
|
|
1781
|
+
this.name = 'ToolUnavailableError';
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Best-effort internal classification of why a qualified tool/prompt name was
|
|
1786
|
+
* unavailable, for logs/activity only. The externally visible message must not
|
|
1787
|
+
* vary with this result.
|
|
1788
|
+
*/
|
|
1789
|
+
const classifyUnavailableReason = (qualifiedName) => {
|
|
1790
|
+
// Qualified names are `<server><separator><name>`; a bare name has no prefix.
|
|
1791
|
+
const serverName = qualifiedName.split(getNameSeparator())[0];
|
|
1792
|
+
const server = getServerByName(serverName);
|
|
1793
|
+
if (!server) {
|
|
1794
|
+
return 'server-or-name-not-found';
|
|
1795
|
+
}
|
|
1796
|
+
if (server.enabled === false) {
|
|
1797
|
+
return 'server-disabled';
|
|
1798
|
+
}
|
|
1799
|
+
// Server exists and is enabled: distinguish a caller-hidden server from a
|
|
1800
|
+
// tool/prompt that is missing (or group-filtered) on a visible server.
|
|
1801
|
+
return getVisibleServerByName(serverName)
|
|
1802
|
+
? 'tool-or-prompt-not-found'
|
|
1803
|
+
: 'server-hidden-from-caller';
|
|
1804
|
+
};
|
|
1741
1805
|
// Add new server
|
|
1742
1806
|
export const addServer = async (name, config) => {
|
|
1743
1807
|
const server = { name, ...config };
|
|
@@ -1764,7 +1828,7 @@ export const removeServer = async (name) => {
|
|
|
1764
1828
|
await removeServerToolEmbeddings(name);
|
|
1765
1829
|
}
|
|
1766
1830
|
catch (error) {
|
|
1767
|
-
|
|
1831
|
+
logger.warn('Failed to remove embeddings for server', { serverName: name, error });
|
|
1768
1832
|
}
|
|
1769
1833
|
serverInfos = serverInfos.filter((serverInfo) => serverInfo.name !== name);
|
|
1770
1834
|
return { success: true, message: 'Server removed successfully' };
|
|
@@ -1793,7 +1857,7 @@ export const addOrUpdateServer = async (name, config, allowOverride = false) =>
|
|
|
1793
1857
|
return { success: true, message: `Server ${action} successfully` };
|
|
1794
1858
|
}
|
|
1795
1859
|
catch (error) {
|
|
1796
|
-
|
|
1860
|
+
logger.error('Failed to add/update server', { serverName: name, error });
|
|
1797
1861
|
return { success: false, message: 'Failed to add/update server' };
|
|
1798
1862
|
}
|
|
1799
1863
|
};
|
|
@@ -1823,7 +1887,7 @@ const closeServerRuntime = (serverInfo) => {
|
|
|
1823
1887
|
if (serverInfo.keepAliveIntervalId) {
|
|
1824
1888
|
clearInterval(serverInfo.keepAliveIntervalId);
|
|
1825
1889
|
serverInfo.keepAliveIntervalId = undefined;
|
|
1826
|
-
|
|
1890
|
+
logger.log('Cleared MCP server keep-alive interval');
|
|
1827
1891
|
}
|
|
1828
1892
|
const candidateTransport = serverInfo.transport;
|
|
1829
1893
|
const stdioPid = typeof candidateTransport?.pid === 'number' ? candidateTransport.pid : null;
|
|
@@ -1832,7 +1896,7 @@ const closeServerRuntime = (serverInfo) => {
|
|
|
1832
1896
|
serverInfo.client.close();
|
|
1833
1897
|
}
|
|
1834
1898
|
catch {
|
|
1835
|
-
|
|
1899
|
+
logger.warn('Error closing MCP client during runtime shutdown');
|
|
1836
1900
|
}
|
|
1837
1901
|
serverInfo.client = undefined;
|
|
1838
1902
|
}
|
|
@@ -1841,14 +1905,14 @@ const closeServerRuntime = (serverInfo) => {
|
|
|
1841
1905
|
serverInfo.transport.close();
|
|
1842
1906
|
}
|
|
1843
1907
|
catch {
|
|
1844
|
-
|
|
1908
|
+
logger.warn('Error closing MCP transport during runtime shutdown');
|
|
1845
1909
|
}
|
|
1846
1910
|
serverInfo.transport = undefined;
|
|
1847
1911
|
}
|
|
1848
1912
|
if (stdioPid) {
|
|
1849
1913
|
killStdioProcessTree(serverInfo.name, stdioPid);
|
|
1850
1914
|
}
|
|
1851
|
-
|
|
1915
|
+
logger.log('Closed MCP server client and transport');
|
|
1852
1916
|
};
|
|
1853
1917
|
// Close server client and transport (keeps the entry in serverInfos; the next
|
|
1854
1918
|
// initializeClientsFromSettings rebuild drops it if it is no longer configured).
|
|
@@ -1874,7 +1938,7 @@ const shutdownOnDemandServer = (serverInfo) => {
|
|
|
1874
1938
|
// Close runtime (kills process) but intentionally keep tools/prompts/resources
|
|
1875
1939
|
closeServerRuntime(serverInfo);
|
|
1876
1940
|
serverInfo.status = 'disconnected';
|
|
1877
|
-
|
|
1941
|
+
logger.log(`[${serverInfo.name}] On-demand server shut down after idle; ` +
|
|
1878
1942
|
`${serverInfo.tools.length} tools cached for next wake-up`);
|
|
1879
1943
|
};
|
|
1880
1944
|
/**
|
|
@@ -1932,7 +1996,7 @@ const ensureServerReady = async (serverInfo) => {
|
|
|
1932
1996
|
return;
|
|
1933
1997
|
}
|
|
1934
1998
|
const name = rawConfig.name;
|
|
1935
|
-
|
|
1999
|
+
logger.log(`[${name}] Cold-starting on-demand server…`);
|
|
1936
2000
|
const transport = await createTransportFromConfig(name, expandedConf);
|
|
1937
2001
|
const client = createUpstreamMcpClient(name, () => serverInfo);
|
|
1938
2002
|
const serverRequestOptions = expandedConf.options || {};
|
|
@@ -1965,7 +2029,7 @@ const ensureServerReady = async (serverInfo) => {
|
|
|
1965
2029
|
broadcastToolListChanged();
|
|
1966
2030
|
}
|
|
1967
2031
|
catch (error) {
|
|
1968
|
-
|
|
2032
|
+
logger.warn(`[${name}] Failed to list tools during wake`, {
|
|
1969
2033
|
error: summarizeErrorForLogging(error),
|
|
1970
2034
|
});
|
|
1971
2035
|
}
|
|
@@ -1977,7 +2041,7 @@ const ensureServerReady = async (serverInfo) => {
|
|
|
1977
2041
|
updateServerPromptsCache(serverInfo, prompts.prompts);
|
|
1978
2042
|
}
|
|
1979
2043
|
catch (error) {
|
|
1980
|
-
|
|
2044
|
+
logger.warn(`[${name}] Failed to list prompts during wake`, {
|
|
1981
2045
|
error: summarizeErrorForLogging(error),
|
|
1982
2046
|
});
|
|
1983
2047
|
}
|
|
@@ -1989,14 +2053,14 @@ const ensureServerReady = async (serverInfo) => {
|
|
|
1989
2053
|
updateServerResourcesCache(serverInfo, resources.resources);
|
|
1990
2054
|
}
|
|
1991
2055
|
catch (error) {
|
|
1992
|
-
|
|
2056
|
+
logger.warn(`[${name}] Failed to list resources during wake`, {
|
|
1993
2057
|
error: summarizeErrorForLogging(error),
|
|
1994
2058
|
});
|
|
1995
2059
|
}
|
|
1996
2060
|
}
|
|
1997
2061
|
// No-op for stdio transports; only SSE/streamable-http opt in via enableKeepAlive.
|
|
1998
2062
|
setupServerKeepAlive(serverInfo, expandedConf);
|
|
1999
|
-
|
|
2063
|
+
logger.log(`[${name}] On-demand server ready in ${Date.now() - spawnStart}ms` +
|
|
2000
2064
|
` (${toolCount} tools, ${promptCount} prompts, ${resourceCount} resources)`);
|
|
2001
2065
|
})();
|
|
2002
2066
|
serverInfo.spawningPromise = spawnPromise;
|
|
@@ -2035,16 +2099,16 @@ const primeOnDemandServers = () => {
|
|
|
2035
2099
|
si.enabled !== false);
|
|
2036
2100
|
if (targets.length === 0)
|
|
2037
2101
|
return Promise.resolve();
|
|
2038
|
-
|
|
2102
|
+
logger.log(`Priming ${targets.length} on-demand server(s) for tool discovery…`);
|
|
2039
2103
|
return Promise.allSettled(targets.map(async (si) => {
|
|
2040
2104
|
try {
|
|
2041
2105
|
await ensureServerReady(si);
|
|
2042
2106
|
// Sleep the child but keep the cached tool list visible to agents.
|
|
2043
2107
|
shutdownOnDemandServer(si);
|
|
2044
|
-
|
|
2108
|
+
logger.log('On-demand server primed and asleep');
|
|
2045
2109
|
}
|
|
2046
2110
|
catch (error) {
|
|
2047
|
-
|
|
2111
|
+
logger.warn('Failed to prime on-demand server', {
|
|
2048
2112
|
error: summarizeErrorForLogging(error),
|
|
2049
2113
|
});
|
|
2050
2114
|
}
|
|
@@ -2073,7 +2137,28 @@ export const resetServerOAuthConnection = (name) => {
|
|
|
2073
2137
|
// launched through a wrapper like `npx` / `npm exec`, the wrapper does not
|
|
2074
2138
|
// forward signals to its descendants, so the real server process is left
|
|
2075
2139
|
// running as an orphan. Walk the whole tree and force-kill it.
|
|
2140
|
+
//
|
|
2141
|
+
// When the process-lister tool tree-kill needs (`ps` on Linux) is missing —
|
|
2142
|
+
// e.g. slim Docker images without procps — tree-kill's internal spawn fails
|
|
2143
|
+
// with an unhandled 'error' event that would crash MCPHub. Fall back to
|
|
2144
|
+
// signaling just the direct child instead (issue #1072).
|
|
2076
2145
|
function killStdioProcessTree(name, pid) {
|
|
2146
|
+
const safeDirectKill = (signal) => {
|
|
2147
|
+
try {
|
|
2148
|
+
process.kill(pid, signal);
|
|
2149
|
+
}
|
|
2150
|
+
catch (err) {
|
|
2151
|
+
const code = err.code;
|
|
2152
|
+
if (code !== 'ESRCH') {
|
|
2153
|
+
logger.warn('Failed to send signal to process', {
|
|
2154
|
+
serverName: name,
|
|
2155
|
+
pid,
|
|
2156
|
+
signal,
|
|
2157
|
+
err,
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
};
|
|
2077
2162
|
const safeTreeKill = (signal) => {
|
|
2078
2163
|
try {
|
|
2079
2164
|
treeKill(pid, signal, (err) => {
|
|
@@ -2085,7 +2170,7 @@ function killStdioProcessTree(name, pid) {
|
|
|
2085
2170
|
// Pass the user-controlled `name` as a separate argument so a
|
|
2086
2171
|
// server named e.g. "%s" cannot inject format specifiers into the
|
|
2087
2172
|
// log line (CodeQL: use-of-externally-controlled-format-string).
|
|
2088
|
-
|
|
2173
|
+
logger.warn('Failed to send signal to process tree', {
|
|
2089
2174
|
serverName: name,
|
|
2090
2175
|
pid,
|
|
2091
2176
|
signal,
|
|
@@ -2096,7 +2181,7 @@ function killStdioProcessTree(name, pid) {
|
|
|
2096
2181
|
});
|
|
2097
2182
|
}
|
|
2098
2183
|
catch (err) {
|
|
2099
|
-
|
|
2184
|
+
logger.warn('Failed to send signal to process tree', {
|
|
2100
2185
|
serverName: name,
|
|
2101
2186
|
pid,
|
|
2102
2187
|
signal,
|
|
@@ -2104,12 +2189,22 @@ function killStdioProcessTree(name, pid) {
|
|
|
2104
2189
|
});
|
|
2105
2190
|
}
|
|
2106
2191
|
};
|
|
2107
|
-
|
|
2192
|
+
const safeKill = isProcessTreeKillAvailable()
|
|
2193
|
+
? safeTreeKill
|
|
2194
|
+
: (signal) => {
|
|
2195
|
+
logger.warn('Process lister unavailable, killing only the direct child process', {
|
|
2196
|
+
serverName: name,
|
|
2197
|
+
pid,
|
|
2198
|
+
signal,
|
|
2199
|
+
});
|
|
2200
|
+
safeDirectKill(signal);
|
|
2201
|
+
};
|
|
2202
|
+
safeKill('SIGTERM');
|
|
2108
2203
|
setTimeout(() => {
|
|
2109
2204
|
if (!isProcessAlive(pid)) {
|
|
2110
2205
|
return;
|
|
2111
2206
|
}
|
|
2112
|
-
|
|
2207
|
+
safeKill('SIGKILL');
|
|
2113
2208
|
}, STDIO_KILL_GRACE_PERIOD_MS);
|
|
2114
2209
|
}
|
|
2115
2210
|
function isProcessAlive(pid) {
|
|
@@ -2143,10 +2238,10 @@ export const toggleServerStatus = async (name, enabled) => {
|
|
|
2143
2238
|
// Remove tool embeddings when server is disabled (for smart routing consistency)
|
|
2144
2239
|
try {
|
|
2145
2240
|
await removeServerToolEmbeddings(name);
|
|
2146
|
-
|
|
2241
|
+
logger.log(`Removed tool embeddings for disabled server: ${name}`);
|
|
2147
2242
|
}
|
|
2148
2243
|
catch (embeddingError) {
|
|
2149
|
-
|
|
2244
|
+
logger.warn('Failed to remove embeddings for server', {
|
|
2150
2245
|
serverName: name,
|
|
2151
2246
|
error: summarizeErrorForLogging(embeddingError),
|
|
2152
2247
|
});
|
|
@@ -2156,10 +2251,10 @@ export const toggleServerStatus = async (name, enabled) => {
|
|
|
2156
2251
|
// If enabling, reconnect the server to restore connection and sync tool embeddings
|
|
2157
2252
|
try {
|
|
2158
2253
|
await initializeClientsFromSettings(false, name);
|
|
2159
|
-
|
|
2254
|
+
logger.log(`Re-enabled server ${name} and triggered tool embedding sync`);
|
|
2160
2255
|
}
|
|
2161
2256
|
catch (reconnectError) {
|
|
2162
|
-
|
|
2257
|
+
logger.warn('Failed to reconnect server during enable', {
|
|
2163
2258
|
serverName: name,
|
|
2164
2259
|
error: summarizeErrorForLogging(reconnectError),
|
|
2165
2260
|
});
|
|
@@ -2168,7 +2263,7 @@ export const toggleServerStatus = async (name, enabled) => {
|
|
|
2168
2263
|
return { success: true, message: `Server ${enabled ? 'enabled' : 'disabled'} successfully` };
|
|
2169
2264
|
}
|
|
2170
2265
|
catch (error) {
|
|
2171
|
-
|
|
2266
|
+
logger.error('Failed to toggle server status', { serverName: name, error });
|
|
2172
2267
|
return { success: false, message: 'Failed to toggle server status' };
|
|
2173
2268
|
}
|
|
2174
2269
|
};
|
|
@@ -2310,7 +2405,7 @@ const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConf
|
|
|
2310
2405
|
export const handleListToolsRequest = async (_, extra) => {
|
|
2311
2406
|
const sessionId = extra.sessionId || '';
|
|
2312
2407
|
const group = getGroup(sessionId);
|
|
2313
|
-
|
|
2408
|
+
logger.log(`Handling ListToolsRequest for group: ${group}`);
|
|
2314
2409
|
// Special handling for $smart group to return smart routing tools
|
|
2315
2410
|
// Support both $smart and $smart/{group} patterns
|
|
2316
2411
|
if (isSmartRoutingGroup(group)) {
|
|
@@ -2322,9 +2417,7 @@ export const handleListToolsRequest = async (_, extra) => {
|
|
|
2322
2417
|
// so this list reflects the freshly cached tools instead of returning empty.
|
|
2323
2418
|
// No wake is triggered from list itself; the prime handles that. See #1029.
|
|
2324
2419
|
await Promise.allSettled(filteredServerInfos
|
|
2325
|
-
.filter((si) => si.config?.startOnDemand === true &&
|
|
2326
|
-
si.tools.length === 0 &&
|
|
2327
|
-
si.spawningPromise)
|
|
2420
|
+
.filter((si) => si.config?.startOnDemand === true && si.tools.length === 0 && si.spawningPromise)
|
|
2328
2421
|
.map((si) => si.spawningPromise));
|
|
2329
2422
|
const allTools = [];
|
|
2330
2423
|
for (const serverInfo of filteredServerInfos) {
|
|
@@ -2354,7 +2447,7 @@ export const handleListToolsRequest = async (_, extra) => {
|
|
|
2354
2447
|
};
|
|
2355
2448
|
};
|
|
2356
2449
|
export const handleCallToolRequest = async (request, extra) => {
|
|
2357
|
-
|
|
2450
|
+
logger.log('Handling CallToolRequest for tool', summarizeToolRequestForLogging(request.params));
|
|
2358
2451
|
const startTime = Date.now();
|
|
2359
2452
|
const activityLogger = getActivityLoggingService();
|
|
2360
2453
|
// Get request context for activity logging
|
|
@@ -2450,7 +2543,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2450
2543
|
serverInfo.tools.some((tool) => tool.name === toolName));
|
|
2451
2544
|
}
|
|
2452
2545
|
if (!targetServerInfo) {
|
|
2453
|
-
throw new
|
|
2546
|
+
throw new ToolUnavailableError(`Tool not available: ${toolName}`, classifyUnavailableReason(toolName));
|
|
2454
2547
|
}
|
|
2455
2548
|
// If the target is an on-demand server that is not yet running, wake it up now.
|
|
2456
2549
|
// Concurrent callers are serialised via ensureServerReady's singleton promise.
|
|
@@ -2467,7 +2560,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2467
2560
|
// Check if the tool exists on the server
|
|
2468
2561
|
const tool = targetTool ?? findToolOnServer(targetServerInfo, targetToolName, singleServerAppsRoute);
|
|
2469
2562
|
if (!tool) {
|
|
2470
|
-
throw new
|
|
2563
|
+
throw new ToolUnavailableError(`Tool not available: ${toolName}`, 'tool-not-found');
|
|
2471
2564
|
}
|
|
2472
2565
|
assertToolAvailableForRoute(tool, appsRouteContext);
|
|
2473
2566
|
// Handle OpenAPI servers differently
|
|
@@ -2476,7 +2569,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2476
2569
|
const openApiClient = targetServerInfo.openApiClient;
|
|
2477
2570
|
// Use toolArgs if it has properties, otherwise fallback to request.params.arguments
|
|
2478
2571
|
const finalArgs = toolArgs && typeof toolArgs === 'object' ? toolArgs : {};
|
|
2479
|
-
|
|
2572
|
+
logger.log('Invoking OpenAPI tool', {
|
|
2480
2573
|
toolName: targetToolName,
|
|
2481
2574
|
serverName: targetServerInfo.name,
|
|
2482
2575
|
arguments: summarizeArgumentsForLogging(finalArgs),
|
|
@@ -2514,7 +2607,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2514
2607
|
requestContent: finalArgs,
|
|
2515
2608
|
responseContent: result,
|
|
2516
2609
|
});
|
|
2517
|
-
|
|
2610
|
+
logger.log('OpenAPI tool invocation result', {
|
|
2518
2611
|
serverName: targetServerInfo.name,
|
|
2519
2612
|
toolName: cleanToolName,
|
|
2520
2613
|
result: summarizeToolResultForLogging(result),
|
|
@@ -2559,7 +2652,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2559
2652
|
}
|
|
2560
2653
|
// Use toolArgs if it has properties, otherwise fallback to request.params.arguments
|
|
2561
2654
|
const finalArgs = toolArgs && typeof toolArgs === 'object' ? toolArgs : {};
|
|
2562
|
-
|
|
2655
|
+
logger.log('Invoking tool', {
|
|
2563
2656
|
toolName: targetToolName,
|
|
2564
2657
|
serverName: targetServerInfo.name,
|
|
2565
2658
|
arguments: summarizeArgumentsForLogging(finalArgs),
|
|
@@ -2576,7 +2669,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2576
2669
|
requestContent: finalArgs,
|
|
2577
2670
|
responseContent: result,
|
|
2578
2671
|
});
|
|
2579
|
-
|
|
2672
|
+
logger.log('Tool invocation result', {
|
|
2580
2673
|
serverName: targetServerInfo.name,
|
|
2581
2674
|
toolName: cleanToolName,
|
|
2582
2675
|
result: summarizeToolResultForLogging(result),
|
|
@@ -2619,15 +2712,12 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2619
2712
|
: undefined;
|
|
2620
2713
|
const serverInfo = singleServerAppsRoute
|
|
2621
2714
|
? appsRouteContext.serverInfo
|
|
2622
|
-
: (groupTool?.serverInfo ??
|
|
2623
|
-
(lookupGroup ? undefined : getServerByTool(request.params.name)));
|
|
2715
|
+
: (groupTool?.serverInfo ?? (lookupGroup ? undefined : getServerByTool(request.params.name)));
|
|
2624
2716
|
const routeToolName = groupTool?.toolName ?? request.params.name;
|
|
2625
2717
|
const tool = groupTool?.tool ??
|
|
2626
|
-
(serverInfo
|
|
2627
|
-
? findToolOnServer(serverInfo, routeToolName, singleServerAppsRoute)
|
|
2628
|
-
: undefined);
|
|
2718
|
+
(serverInfo ? findToolOnServer(serverInfo, routeToolName, singleServerAppsRoute) : undefined);
|
|
2629
2719
|
if (!serverInfo || !tool) {
|
|
2630
|
-
throw new
|
|
2720
|
+
throw new ToolUnavailableError(`Tool not available: ${request.params.name}`, classifyUnavailableReason(request.params.name));
|
|
2631
2721
|
}
|
|
2632
2722
|
assertToolAvailableForRoute(tool, appsRouteContext);
|
|
2633
2723
|
// Wake the on-demand server before invoking the tool. It may be asleep with
|
|
@@ -2643,7 +2733,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2643
2733
|
const openApiClient = serverInfo.openApiClient;
|
|
2644
2734
|
// Remove server prefix from tool name if present
|
|
2645
2735
|
const cleanToolName = normalizeToolNameForServer(serverInfo.name, routeToolName);
|
|
2646
|
-
|
|
2736
|
+
logger.log('Invoking OpenAPI tool', {
|
|
2647
2737
|
toolName: cleanToolName,
|
|
2648
2738
|
serverName: serverInfo.name,
|
|
2649
2739
|
arguments: summarizeArgumentsForLogging(request.params.arguments),
|
|
@@ -2680,7 +2770,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2680
2770
|
requestContent: finalArgs,
|
|
2681
2771
|
responseContent: result,
|
|
2682
2772
|
});
|
|
2683
|
-
|
|
2773
|
+
logger.log('OpenAPI tool invocation result', {
|
|
2684
2774
|
serverName: serverInfo.name,
|
|
2685
2775
|
toolName: cleanToolName,
|
|
2686
2776
|
result: summarizeToolResultForLogging(result),
|
|
@@ -2731,7 +2821,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2731
2821
|
requestContent: request.params.arguments,
|
|
2732
2822
|
responseContent: result,
|
|
2733
2823
|
});
|
|
2734
|
-
|
|
2824
|
+
logger.log('Tool call result', {
|
|
2735
2825
|
serverName: serverInfo.name,
|
|
2736
2826
|
toolName: cleanToolName,
|
|
2737
2827
|
result: summarizeToolResultForLogging(result),
|
|
@@ -2761,7 +2851,13 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2761
2851
|
});
|
|
2762
2852
|
}
|
|
2763
2853
|
catch (error) {
|
|
2764
|
-
|
|
2854
|
+
const unavailable = error instanceof ToolUnavailableError
|
|
2855
|
+
? { message: error.message, reason: error.reason }
|
|
2856
|
+
: undefined;
|
|
2857
|
+
logger.error('Error handling CallToolRequest', {
|
|
2858
|
+
...summarizeErrorForLogging(error),
|
|
2859
|
+
...(unavailable ? { reason: unavailable.reason } : {}),
|
|
2860
|
+
});
|
|
2765
2861
|
// Log error activity
|
|
2766
2862
|
const duration = Date.now() - startTime;
|
|
2767
2863
|
await settleHostedIfNeeded({
|
|
@@ -2784,9 +2880,15 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2784
2880
|
keyId,
|
|
2785
2881
|
keyName,
|
|
2786
2882
|
sourceIp,
|
|
2787
|
-
|
|
2883
|
+
// The reason is an internal diagnostic; the caller-facing message stays
|
|
2884
|
+
// identical for hidden and nonexistent targets.
|
|
2885
|
+
errorMessage: unavailable
|
|
2886
|
+
? `${unavailable.message} (reason: ${unavailable.reason})`
|
|
2887
|
+
: formatErrorForLogging(error),
|
|
2788
2888
|
});
|
|
2789
|
-
|
|
2889
|
+
// For unavailable-target errors, surface exactly the unified message (no
|
|
2890
|
+
// error-class name, no reason) so hidden vs nonexistent is indistinguishable.
|
|
2891
|
+
const safeErrorText = unavailable ? unavailable.message : formatErrorForLogging(error);
|
|
2790
2892
|
return {
|
|
2791
2893
|
content: [
|
|
2792
2894
|
{
|
|
@@ -2842,7 +2944,7 @@ export const handleGetPromptRequest = async (request, extra) => {
|
|
|
2842
2944
|
serverInfo.prompts.find((prompt) => prompt.name === name));
|
|
2843
2945
|
}
|
|
2844
2946
|
if (!server) {
|
|
2845
|
-
throw new
|
|
2947
|
+
throw new ToolUnavailableError(`Prompt not available: ${name}`, classifyUnavailableReason(name));
|
|
2846
2948
|
}
|
|
2847
2949
|
// Remove server prefix from prompt name if present
|
|
2848
2950
|
const separator = getNameSeparator();
|
|
@@ -2855,20 +2957,27 @@ export const handleGetPromptRequest = async (request, extra) => {
|
|
|
2855
2957
|
arguments: promptArgs,
|
|
2856
2958
|
};
|
|
2857
2959
|
// Log the final promptParams
|
|
2858
|
-
|
|
2960
|
+
logger.log('Calling getPrompt with params', {
|
|
2859
2961
|
name: cleanPromptName || '',
|
|
2860
2962
|
arguments: summarizeArgumentsForLogging(promptArgs),
|
|
2861
2963
|
});
|
|
2862
2964
|
const prompt = await server.client?.getPrompt(promptParams);
|
|
2863
|
-
|
|
2965
|
+
logger.log('Received prompt', summarizePromptForLogging(prompt));
|
|
2864
2966
|
if (!prompt) {
|
|
2865
2967
|
throw new Error(`Prompt not found: ${cleanPromptName}`);
|
|
2866
2968
|
}
|
|
2867
2969
|
return prompt;
|
|
2868
2970
|
}
|
|
2869
2971
|
catch (error) {
|
|
2870
|
-
|
|
2871
|
-
|
|
2972
|
+
const unavailable = error instanceof ToolUnavailableError
|
|
2973
|
+
? { message: error.message, reason: error.reason }
|
|
2974
|
+
: undefined;
|
|
2975
|
+
logger.error('Error handling GetPromptRequest', {
|
|
2976
|
+
...summarizeErrorForLogging(error),
|
|
2977
|
+
...(unavailable ? { reason: unavailable.reason } : {}),
|
|
2978
|
+
});
|
|
2979
|
+
// Surface exactly the unified message for unavailable targets (see #1103).
|
|
2980
|
+
const safeErrorText = unavailable ? unavailable.message : formatErrorForLogging(error);
|
|
2872
2981
|
return {
|
|
2873
2982
|
content: [
|
|
2874
2983
|
{
|
|
@@ -2884,7 +2993,7 @@ export const handleListPromptsRequest = async (_, extra) => {
|
|
|
2884
2993
|
const sessionId = extra.sessionId || '';
|
|
2885
2994
|
const group = getGroup(sessionId);
|
|
2886
2995
|
const lookupGroup = getGroupLookupName(group);
|
|
2887
|
-
|
|
2996
|
+
logger.log(`Handling ListPromptsRequest for group: ${group}`);
|
|
2888
2997
|
// Start with built-in prompts (only enabled ones)
|
|
2889
2998
|
const builtinPrompts = await getBuiltinPromptDao().findEnabled();
|
|
2890
2999
|
const allPrompts = builtinPrompts.map((bp) => normalizePromptForList({
|
|
@@ -2928,7 +3037,7 @@ export const handleListResourcesRequest = async (_, extra) => {
|
|
|
2928
3037
|
const sessionId = extra.sessionId || '';
|
|
2929
3038
|
const group = getGroup(sessionId);
|
|
2930
3039
|
const lookupGroup = getGroupLookupName(group);
|
|
2931
|
-
|
|
3040
|
+
logger.log(`Handling ListResourcesRequest for group: ${group}`);
|
|
2932
3041
|
const appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
|
|
2933
3042
|
// Start with built-in resources (only enabled ones)
|
|
2934
3043
|
const builtinResources = await getBuiltinResourceDao().findEnabled();
|
|
@@ -2973,7 +3082,7 @@ export const handleListResourceTemplatesRequest = async (_, extra) => {
|
|
|
2973
3082
|
const sessionId = extra.sessionId || '';
|
|
2974
3083
|
const group = getGroup(sessionId);
|
|
2975
3084
|
const lookupGroup = getGroupLookupName(group);
|
|
2976
|
-
|
|
3085
|
+
logger.log(`Handling ListResourceTemplatesRequest for group: ${group}`);
|
|
2977
3086
|
const appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
|
|
2978
3087
|
const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(lookupGroup, {
|
|
2979
3088
|
requireClient: true,
|
|
@@ -3076,7 +3185,7 @@ export const handleReadResourceRequest = async (request, extra) => {
|
|
|
3076
3185
|
};
|
|
3077
3186
|
}
|
|
3078
3187
|
catch (error) {
|
|
3079
|
-
|
|
3188
|
+
logger.error('Error handling ReadResourceRequest', summarizeErrorForLogging(error));
|
|
3080
3189
|
const safeErrorText = formatErrorForLogging(error);
|
|
3081
3190
|
return {
|
|
3082
3191
|
contents: [
|