@samanhappy/mcphub 1.0.36 → 1.0.38
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 +47 -17
- package/README.md +44 -36
- package/README.zh.md +45 -35
- package/dist/constants/oauthServerDefaults.js +1 -0
- package/dist/constants/oauthServerDefaults.js.map +1 -1
- package/dist/controllers/groupController.js +63 -14
- package/dist/controllers/groupController.js.map +1 -1
- package/dist/controllers/oauthDynamicRegistrationController.js +6 -32
- package/dist/controllers/oauthDynamicRegistrationController.js.map +1 -1
- package/dist/controllers/serverController.js +46 -3
- package/dist/controllers/serverController.js.map +1 -1
- package/dist/dao/GroupDaoDbImpl.js +18 -0
- package/dist/dao/GroupDaoDbImpl.js.map +1 -1
- package/dist/dao/OAuthClientDaoDbImpl.js +3 -0
- package/dist/dao/OAuthClientDaoDbImpl.js.map +1 -1
- package/dist/db/connection.js +3 -2
- package/dist/db/connection.js.map +1 -1
- package/dist/db/entities/Group.js +9 -1
- package/dist/db/entities/Group.js.map +1 -1
- package/dist/db/entities/OAuthClient.js +4 -0
- package/dist/db/entities/OAuthClient.js.map +1 -1
- package/dist/db/groupSchema.js +29 -0
- package/dist/db/groupSchema.js.map +1 -0
- package/dist/models/OAuth.js +98 -1
- package/dist/models/OAuth.js.map +1 -1
- package/dist/routes/index.js +4 -3
- package/dist/routes/index.js.map +1 -1
- package/dist/services/groupService.js +57 -23
- package/dist/services/groupService.js.map +1 -1
- package/dist/services/logService.js +11 -2
- package/dist/services/logService.js.map +1 -1
- package/dist/services/mcpService.js +177 -17
- package/dist/services/mcpService.js.map +1 -1
- package/dist/services/oauthClientRegistration.js +22 -7
- package/dist/services/oauthClientRegistration.js.map +1 -1
- package/dist/services/oauthSettingsStore.js +3 -0
- package/dist/services/oauthSettingsStore.js.map +1 -1
- package/dist/services/sseService.js +63 -7
- package/dist/services/sseService.js.map +1 -1
- package/dist/utils/cacheUtils.js +133 -5
- package/dist/utils/cacheUtils.js.map +1 -1
- package/dist/utils/groupAccess.js +17 -0
- package/dist/utils/groupAccess.js.map +1 -0
- package/dist/utils/migration.js +3 -0
- package/dist/utils/migration.js.map +1 -1
- package/dist/utils/rateLimit.js +85 -18
- package/dist/utils/rateLimit.js.map +1 -1
- package/dist/utils/smartRouting.js +1 -1
- package/dist/utils/smartRouting.js.map +1 -1
- package/frontend/dist/assets/{ActivityPage-ZRx6b9hm.js → ActivityPage-BZNq-eGE.js} +2 -2
- package/frontend/dist/assets/{ActivityPage-ZRx6b9hm.js.map → ActivityPage-BZNq-eGE.js.map} +1 -1
- package/frontend/dist/assets/{CredentialsPage-C9vnh01H.js → CredentialsPage-7kyDJLsX.js} +2 -2
- package/frontend/dist/assets/{CredentialsPage-C9vnh01H.js.map → CredentialsPage-7kyDJLsX.js.map} +1 -1
- package/frontend/dist/assets/{Dashboard-BDGDQNz1.js → Dashboard-CElNx820.js} +2 -2
- package/frontend/dist/assets/{Dashboard-BDGDQNz1.js.map → Dashboard-CElNx820.js.map} +1 -1
- package/frontend/dist/assets/{EndpointCopy-BY51t0Ks.js → EndpointCopy-8jW5Xtka.js} +2 -2
- package/frontend/dist/assets/{EndpointCopy-BY51t0Ks.js.map → EndpointCopy-8jW5Xtka.js.map} +1 -1
- package/frontend/dist/assets/GroupsPage-dUkvX_iB.js +33 -0
- package/frontend/dist/assets/GroupsPage-dUkvX_iB.js.map +1 -0
- package/frontend/dist/assets/{LoginPage-CZOKlkR_.js → LoginPage-D23s7oM0.js} +2 -2
- package/frontend/dist/assets/{LoginPage-CZOKlkR_.js.map → LoginPage-D23s7oM0.js.map} +1 -1
- package/frontend/dist/assets/{LogsPage-FDWOOIPg.js → LogsPage-BuQMkmV-.js} +2 -2
- package/frontend/dist/assets/{LogsPage-FDWOOIPg.js.map → LogsPage-BuQMkmV-.js.map} +1 -1
- package/frontend/dist/assets/{MarketPage-DKfZgoIs.js → MarketPage-g9vEYzIq.js} +2 -2
- package/frontend/dist/assets/{MarketPage-DKfZgoIs.js.map → MarketPage-g9vEYzIq.js.map} +1 -1
- package/frontend/dist/assets/{OAuthConsentPage-Hk_DJ0dW.js → OAuthConsentPage-BiX1lpJR.js} +2 -2
- package/frontend/dist/assets/{OAuthConsentPage-Hk_DJ0dW.js.map → OAuthConsentPage-BiX1lpJR.js.map} +1 -1
- package/frontend/dist/assets/{PromptsPage-CT0C0_BI.js → PromptsPage-g8SRFY6s.js} +2 -2
- package/frontend/dist/assets/{PromptsPage-CT0C0_BI.js.map → PromptsPage-g8SRFY6s.js.map} +1 -1
- package/frontend/dist/assets/{ResourcesPage-CBgu-N5m.js → ResourcesPage-Ddhxi9Sf.js} +2 -2
- package/frontend/dist/assets/{ResourcesPage-CBgu-N5m.js.map → ResourcesPage-Ddhxi9Sf.js.map} +1 -1
- package/frontend/dist/assets/{ServersPage-CvcP8LFv.js → ServersPage-DAmKvrPV.js} +8 -8
- package/frontend/dist/assets/ServersPage-DAmKvrPV.js.map +1 -0
- package/frontend/dist/assets/SettingsPage-DZXjQZXS.js +12 -0
- package/frontend/dist/assets/SettingsPage-DZXjQZXS.js.map +1 -0
- package/frontend/dist/assets/{StatusDot-uIIW4IuH.js → StatusDot-CdzMqjTi.js} +2 -2
- package/frontend/dist/assets/{StatusDot-uIIW4IuH.js.map → StatusDot-CdzMqjTi.js.map} +1 -1
- package/frontend/dist/assets/{ToggleGroup-BToroeZy.js → ToggleGroup-CJd8X_31.js} +2 -2
- package/frontend/dist/assets/{ToggleGroup-BToroeZy.js.map → ToggleGroup-CJd8X_31.js.map} +1 -1
- package/frontend/dist/assets/{UsersPage-BjgJjf6L.js → UsersPage-CkP_x9Rt.js} +2 -2
- package/frontend/dist/assets/{UsersPage-BjgJjf6L.js.map → UsersPage-CkP_x9Rt.js.map} +1 -1
- package/frontend/dist/assets/index-Bu4XuAH4.js +3 -0
- package/frontend/dist/assets/index-Bu4XuAH4.js.map +1 -0
- package/frontend/dist/assets/{resourceService-sD9dxx3E.js → resourceService-w-cZ31FD.js} +2 -2
- package/frontend/dist/assets/{resourceService-sD9dxx3E.js.map → resourceService-w-cZ31FD.js.map} +1 -1
- package/frontend/dist/assets/{useCostData-QoPbsqyD.js → useCostData-C72NC0PQ.js} +2 -2
- package/frontend/dist/assets/{useCostData-QoPbsqyD.js.map → useCostData-C72NC0PQ.js.map} +1 -1
- package/frontend/dist/assets/useSettingsData-JTEMu74D.js +2 -0
- package/frontend/dist/assets/{useSettingsData-C4JrLqDh.js.map → useSettingsData-JTEMu74D.js.map} +1 -1
- package/frontend/dist/assets/{variableDetection-iFAhGAKs.js → variableDetection-DfhNyFy-.js} +2 -2
- package/frontend/dist/assets/{variableDetection-iFAhGAKs.js.map → variableDetection-DfhNyFy-.js.map} +1 -1
- package/frontend/dist/index.html +1 -1
- package/mcp_settings.json +2 -1
- package/package.json +1 -1
- package/frontend/dist/assets/GroupsPage-DyHyW7NW.js +0 -33
- package/frontend/dist/assets/GroupsPage-DyHyW7NW.js.map +0 -1
- package/frontend/dist/assets/ServersPage-CvcP8LFv.js.map +0 -1
- package/frontend/dist/assets/SettingsPage-DDSVt_xk.js +0 -12
- package/frontend/dist/assets/SettingsPage-DDSVt_xk.js.map +0 -1
- package/frontend/dist/assets/index-DjNOC0Ov.js +0 -3
- package/frontend/dist/assets/index-DjNOC0Ov.js.map +0 -1
- package/frontend/dist/assets/useSettingsData-C4JrLqDh.js +0 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { canAccessGroupRoute } from '../utils/groupAccess.js';
|
|
2
3
|
import { PrincipalRuntimeService } from './principalRuntimeService.js';
|
|
3
4
|
import { UserContextService } from './userContextService.js';
|
|
4
5
|
import { deleteCredentialBindings, missingCredentialError } from './credentialBindingService.js';
|
|
@@ -524,6 +525,82 @@ const normalizeResourceForCache = (resource) => {
|
|
|
524
525
|
};
|
|
525
526
|
// Store all server information
|
|
526
527
|
let serverInfos = [];
|
|
528
|
+
/**
|
|
529
|
+
* How many upstream connections an initialization pass may open at once.
|
|
530
|
+
* Unset, empty or `0` keeps the previous behaviour of connecting every enabled
|
|
531
|
+
* server in parallel. Read per pass so a restart is enough to change it.
|
|
532
|
+
*/
|
|
533
|
+
const resolveStartupConcurrency = () => {
|
|
534
|
+
const raw = process.env.STARTUP_CONNECT_CONCURRENCY?.trim();
|
|
535
|
+
if (!raw) {
|
|
536
|
+
return 0;
|
|
537
|
+
}
|
|
538
|
+
const parsed = Number(raw);
|
|
539
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
540
|
+
logger.warn(`Ignoring invalid STARTUP_CONNECT_CONCURRENCY="${raw}"; expected a non-negative integer. Connecting without a limit.`);
|
|
541
|
+
return 0;
|
|
542
|
+
}
|
|
543
|
+
return parsed;
|
|
544
|
+
};
|
|
545
|
+
/**
|
|
546
|
+
* Rejection reason for a queued connection that was dropped before it started,
|
|
547
|
+
* because the server it belonged to was disabled, removed or superseded while
|
|
548
|
+
* it waited for a slot. Not a connection failure: nothing was spawned and there
|
|
549
|
+
* is nothing wrong with the server, so callers report it as a cancellation
|
|
550
|
+
* rather than marking the server as failed.
|
|
551
|
+
*/
|
|
552
|
+
class QueuedConnectCancelledError extends Error {
|
|
553
|
+
constructor() {
|
|
554
|
+
super('Queued connection was cancelled before it started');
|
|
555
|
+
this.name = 'QueuedConnectCancelledError';
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Defers the *invocation* of a connect rather than wrapping its promise, so a
|
|
560
|
+
* server waiting for a slot has not started its request yet and its timeout
|
|
561
|
+
* budget is not spent queueing. A limit of 0 runs everything immediately.
|
|
562
|
+
*
|
|
563
|
+
* Because work waits here, what the operator wants can change while it waits -
|
|
564
|
+
* a queued server may be disabled, removed or replaced by a later pass. The
|
|
565
|
+
* `isStillWanted` guard is therefore evaluated when the slot is granted, not
|
|
566
|
+
* when the work is queued; a queued entry that is no longer wanted is cancelled
|
|
567
|
+
* and its slot handed straight to the next in line.
|
|
568
|
+
*/
|
|
569
|
+
const createConnectGate = (limit) => {
|
|
570
|
+
if (limit <= 0) {
|
|
571
|
+
// Nothing queues, so nothing can go stale while it waits.
|
|
572
|
+
return { run: (start) => start() };
|
|
573
|
+
}
|
|
574
|
+
let active = 0;
|
|
575
|
+
const waiting = [];
|
|
576
|
+
const startNextWaiting = () => {
|
|
577
|
+
waiting.shift()?.();
|
|
578
|
+
};
|
|
579
|
+
const release = () => {
|
|
580
|
+
active -= 1;
|
|
581
|
+
startNextWaiting();
|
|
582
|
+
};
|
|
583
|
+
return {
|
|
584
|
+
run: (start, isStillWanted) => new Promise((resolve, reject) => {
|
|
585
|
+
const begin = () => {
|
|
586
|
+
if (isStillWanted && !isStillWanted()) {
|
|
587
|
+
// The slot was never taken, so pass it on rather than release it.
|
|
588
|
+
reject(new QueuedConnectCancelledError());
|
|
589
|
+
startNextWaiting();
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
active += 1;
|
|
593
|
+
start().then(resolve, reject).finally(release);
|
|
594
|
+
};
|
|
595
|
+
if (active < limit) {
|
|
596
|
+
begin();
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
waiting.push(begin);
|
|
600
|
+
}
|
|
601
|
+
}),
|
|
602
|
+
};
|
|
603
|
+
};
|
|
527
604
|
// Bumped by every initializeClientsFromSettings pass. Connections are started
|
|
528
605
|
// fire-and-forget and settle long after the pass that started them returned, so
|
|
529
606
|
// a callback needs to know whether a later pass has run before it writes its
|
|
@@ -1205,6 +1282,15 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1205
1282
|
const existingServerInfos = serverInfos;
|
|
1206
1283
|
const nextServerInfos = [];
|
|
1207
1284
|
const generation = ++initGeneration;
|
|
1285
|
+
const concurrency = resolveStartupConcurrency();
|
|
1286
|
+
const connectGate = createConnectGate(concurrency);
|
|
1287
|
+
// Set once this pass swaps its entries into `serverInfos`. Until then a
|
|
1288
|
+
// queued server cannot have been retired by anyone, because the entry that
|
|
1289
|
+
// `closeServer`/`toggleServerStatus` would act on is not published yet.
|
|
1290
|
+
let entriesPublished = false;
|
|
1291
|
+
if (isInit && concurrency > 0) {
|
|
1292
|
+
logger.log(`Connecting upstream servers with a concurrency limit of ${concurrency}`);
|
|
1293
|
+
}
|
|
1208
1294
|
try {
|
|
1209
1295
|
for (const conf of allServers) {
|
|
1210
1296
|
const { name } = conf;
|
|
@@ -1499,7 +1585,33 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1499
1585
|
const current = serverInfos.find((si) => si.name === name);
|
|
1500
1586
|
return current && current.client === client ? current : undefined;
|
|
1501
1587
|
};
|
|
1502
|
-
|
|
1588
|
+
// A queued connection has not spawned anything yet, so cancelling it is
|
|
1589
|
+
// free - and necessary: `closeServerRuntime` clears the client/transport
|
|
1590
|
+
// references that would later own the child process, so connecting after
|
|
1591
|
+
// a disable strands a process nothing can clean up.
|
|
1592
|
+
const connectStillWanted = () => {
|
|
1593
|
+
// Until this pass publishes its entries, nothing else can have retired
|
|
1594
|
+
// this server: the entry a disable would act on does not exist yet.
|
|
1595
|
+
if (!entriesPublished)
|
|
1596
|
+
return true;
|
|
1597
|
+
const current = serverInfos.find((si) => si.name === name);
|
|
1598
|
+
// Gone from the configuration while it waited.
|
|
1599
|
+
if (!current)
|
|
1600
|
+
return false;
|
|
1601
|
+
// Client identity, not the generation, decides whether anyone is still
|
|
1602
|
+
// waiting on this connect - the same rule liveServerInfo() applies to
|
|
1603
|
+
// the result. A later pass that *preserved* this server spreads our
|
|
1604
|
+
// client into its entry and relies on this very connect to complete it
|
|
1605
|
+
// (a scoped reload preserves every other server whatever its status,
|
|
1606
|
+
// see #921), so cancelling on generation alone would strand it on
|
|
1607
|
+
// 'connecting' for good - the failure #1150 was about. A later pass
|
|
1608
|
+
// that rebuilt the server carries a client of its own, and disabling
|
|
1609
|
+
// closes the runtime before replacing the entry, so in both of those
|
|
1610
|
+
// cases the published entry no longer carries our client.
|
|
1611
|
+
return current.client === client && current.enabled !== false;
|
|
1612
|
+
};
|
|
1613
|
+
connectGate
|
|
1614
|
+
.run(() => connectClientWithDiagnostics(client, transport, initRequestOptions || requestOptions), connectStillWanted)
|
|
1503
1615
|
.then(() => {
|
|
1504
1616
|
const info = liveServerInfo();
|
|
1505
1617
|
if (!info) {
|
|
@@ -1592,6 +1704,11 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1592
1704
|
}
|
|
1593
1705
|
})
|
|
1594
1706
|
.catch(async (error) => {
|
|
1707
|
+
if (error instanceof QueuedConnectCancelledError) {
|
|
1708
|
+
logger.log(`Cancelled queued connection for server: ${name} (disabled, removed or superseded while queued)`);
|
|
1709
|
+
closeServerRuntime(serverInfo);
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1595
1712
|
const info = liveServerInfo();
|
|
1596
1713
|
if (!info) {
|
|
1597
1714
|
logger.log(`Discarding superseded connection failure for server: ${name}`);
|
|
@@ -1631,10 +1748,11 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1631
1748
|
throw error;
|
|
1632
1749
|
}
|
|
1633
1750
|
serverInfos = nextServerInfos;
|
|
1751
|
+
entriesPublished = true;
|
|
1634
1752
|
// Populate the tool cache for on-demand stdio servers so their tools are
|
|
1635
1753
|
// visible to agents, then put them back to sleep. Running here (rather than
|
|
1636
1754
|
// only at startup) also covers servers added/enabled/reloaded after startup.
|
|
1637
|
-
const primePromise = primeOnDemandServers();
|
|
1755
|
+
const primePromise = primeOnDemandServers(connectGate);
|
|
1638
1756
|
// At full startup (no serverName) keep prime fire-and-forget so a slow or
|
|
1639
1757
|
// broken on-demand server does not block init. For a targeted reload/edit
|
|
1640
1758
|
// (serverName set), await it so the caller - and the dashboard refresh that
|
|
@@ -1734,9 +1852,14 @@ export const getServersInfo = async (page, limit, user) => {
|
|
|
1734
1852
|
const enabled = serverConfig ? serverConfig.enabled !== false : true;
|
|
1735
1853
|
const resolvedType = inferServerType(serverConfig);
|
|
1736
1854
|
const oauthConnected = Boolean(serverConfig?.oauth?.accessToken || serverConfig?.oauth?.refreshToken);
|
|
1737
|
-
// Add enabled status and custom description to each tool
|
|
1855
|
+
// Add enabled status and custom description to each tool.
|
|
1856
|
+
// toggleTool stores under whatever key the caller passed (prefixed,
|
|
1857
|
+
// as the dashboard sends, or bare, as a direct API call may) with no
|
|
1858
|
+
// normalization — check both so the dashboard reflects a disable set
|
|
1859
|
+
// either way instead of always showing enabled:true for one of them.
|
|
1738
1860
|
const toolsWithEnabled = tools.map((tool) => {
|
|
1739
|
-
const
|
|
1861
|
+
const bareToolName = normalizeToolNameForServer(name, tool.name);
|
|
1862
|
+
const toolConfig = serverConfig?.tools?.[tool.name] ?? serverConfig?.tools?.[bareToolName];
|
|
1740
1863
|
return buildToolWithDescriptionMetadata(tool, toolConfig);
|
|
1741
1864
|
});
|
|
1742
1865
|
const promptsWithEnabled = prompts.map((prompt) => {
|
|
@@ -1871,7 +1994,9 @@ export const reinstallServer = async (serverName) => {
|
|
|
1871
1994
|
try {
|
|
1872
1995
|
// For npx, clear cache directory synchronously before reconnect.
|
|
1873
1996
|
// For uvx, this is a no-op — refresh is handled via --refresh flag injection.
|
|
1874
|
-
|
|
1997
|
+
// Pass the server's own arguments so the npx clear can be scoped to this
|
|
1998
|
+
// server's package instead of discarding every npx server's install.
|
|
1999
|
+
await clearRunnerCache(command, serverConfig.args || []);
|
|
1875
2000
|
// Close and reconnect (will pick up pendingReinstalls flag for uvx)
|
|
1876
2001
|
await reconnectServer(serverName);
|
|
1877
2002
|
logger.log(`Successfully initiated reinstall for server: ${serverName}`);
|
|
@@ -1890,7 +2015,11 @@ const filterToolsByConfig = async (serverName, tools) => {
|
|
|
1890
2015
|
return tools;
|
|
1891
2016
|
}
|
|
1892
2017
|
return tools.filter((tool) => {
|
|
1893
|
-
|
|
2018
|
+
// toggleTool stores the disable under whatever key string the caller used
|
|
2019
|
+
// (bare upstream name or server-prefixed name, never normalized) — check
|
|
2020
|
+
// both, same as findToolOnServer's execution-time gate.
|
|
2021
|
+
const bareToolName = normalizeToolNameForServer(serverName, tool.name);
|
|
2022
|
+
const toolConfig = serverConfig.tools?.[tool.name] ?? serverConfig.tools?.[bareToolName];
|
|
1894
2023
|
// If tool is not in config, it's enabled by default
|
|
1895
2024
|
return toolConfig?.enabled !== false;
|
|
1896
2025
|
});
|
|
@@ -2251,7 +2380,7 @@ const isStdioServer = (conf) => !!conf && (conf.type === 'stdio' || (!conf.type
|
|
|
2251
2380
|
* does not block init. Each server is isolated so one failure does not affect
|
|
2252
2381
|
* the others. See #1029 / #1032.
|
|
2253
2382
|
*/
|
|
2254
|
-
const primeOnDemandServers = () => {
|
|
2383
|
+
const primeOnDemandServers = (gate) => {
|
|
2255
2384
|
const targets = serverInfos.filter((si) => si.config?.startOnDemand === true &&
|
|
2256
2385
|
!hasCredentialTemplate(si.config) &&
|
|
2257
2386
|
isStdioServer(si.config) &&
|
|
@@ -2260,9 +2389,13 @@ const primeOnDemandServers = () => {
|
|
|
2260
2389
|
if (targets.length === 0)
|
|
2261
2390
|
return Promise.resolve();
|
|
2262
2391
|
logger.log(`Priming ${targets.length} on-demand server(s) for tool discovery…`);
|
|
2392
|
+
// Priming happens right after init, so it competes for the same machine.
|
|
2393
|
+
// Share the pass's concurrency limit rather than waking every sleeping server
|
|
2394
|
+
// at once and undoing the pacing above.
|
|
2395
|
+
const run = gate ? gate.run : (start) => start();
|
|
2263
2396
|
return Promise.allSettled(targets.map(async (si) => {
|
|
2264
2397
|
try {
|
|
2265
|
-
await ensureServerReady(si);
|
|
2398
|
+
await run(() => ensureServerReady(si));
|
|
2266
2399
|
// Sleep the child unless a tool call is using it; keep the cached tool list.
|
|
2267
2400
|
shutdownOnDemandServer(si);
|
|
2268
2401
|
logger.log('On-demand server tool cache primed');
|
|
@@ -2482,8 +2615,36 @@ const resolveNameFromGroup = (name, serverName, serverConfig) => {
|
|
|
2482
2615
|
return replacePrefixedServerName(name, getExposedServerName(serverName, serverConfig), serverName);
|
|
2483
2616
|
};
|
|
2484
2617
|
const findToolOnServer = (serverInfo, toolName, allowRawName) => {
|
|
2485
|
-
|
|
2618
|
+
const tool = serverInfo.tools.find((tool) => tool.name === toolName ||
|
|
2486
2619
|
(allowRawName && normalizeToolNameForServer(serverInfo.name, tool.name) === toolName));
|
|
2620
|
+
if (!tool) {
|
|
2621
|
+
return undefined;
|
|
2622
|
+
}
|
|
2623
|
+
// A tool disabled via POST /api/servers/:server/tools/:tool/toggle must be
|
|
2624
|
+
// unresolvable for execution, not just hidden from tools/list — otherwise a
|
|
2625
|
+
// caller who already knows the tool name can invoke it regardless of the
|
|
2626
|
+
// disabled flag.
|
|
2627
|
+
//
|
|
2628
|
+
// The runtime tool cache (serverInfo.tools, built by normalizeToolForCache)
|
|
2629
|
+
// never carries an `enabled` field — that field only exists on the
|
|
2630
|
+
// *projection* built for the dashboard/API (buildToolWithDescriptionMetadata),
|
|
2631
|
+
// which is a different object. So the persisted per-tool config must be
|
|
2632
|
+
// consulted directly here.
|
|
2633
|
+
//
|
|
2634
|
+
// toggleTool (serverController.ts) stores `tools[toolName] = ...` using
|
|
2635
|
+
// whatever string the caller passed as :toolName, with no normalization.
|
|
2636
|
+
// The dashboard sends the already-prefixed `tool.name`, matching what
|
|
2637
|
+
// getServersInfo's toolsWithEnabled already reads — but any other caller
|
|
2638
|
+
// (REST API used directly, scripts, docs examples) can just as validly
|
|
2639
|
+
// pass the bare upstream name, which then never matches a prefixed-only
|
|
2640
|
+
// lookup. Check both so a disable takes effect regardless of which form
|
|
2641
|
+
// was used to set it.
|
|
2642
|
+
const bareToolName = normalizeToolNameForServer(serverInfo.name, tool.name);
|
|
2643
|
+
const toolConfig = serverInfo.config?.tools?.[tool.name] ?? serverInfo.config?.tools?.[bareToolName];
|
|
2644
|
+
if (toolConfig && toolConfig.enabled === false) {
|
|
2645
|
+
return undefined;
|
|
2646
|
+
}
|
|
2647
|
+
return tool;
|
|
2487
2648
|
};
|
|
2488
2649
|
const assertToolAvailableForRoute = (tool, appsRouteContext) => {
|
|
2489
2650
|
if (isAppOnlyTool(tool) && !appsRouteContext.enabled) {
|
|
@@ -3593,13 +3754,8 @@ export const createMcpServer = (name, version, options) => {
|
|
|
3593
3754
|
return server;
|
|
3594
3755
|
};
|
|
3595
3756
|
export const getFilteredServerInfosForGroup = async (group, options) => {
|
|
3596
|
-
// Resolve
|
|
3597
|
-
|
|
3598
|
-
// and applies filterData on groups). Groups don't carry a visibility field,
|
|
3599
|
-
// so admin-owned groups would be filtered out for non-admin users even though
|
|
3600
|
-
// bearer-key auth already authorized access and individual servers inside the
|
|
3601
|
-
// group may be public. Server-level filterData below still enforces per-server
|
|
3602
|
-
// visibility. Fix for #914.
|
|
3757
|
+
// Resolve raw groups so legacy routing remains compatible, then enforce explicit visibility.
|
|
3758
|
+
let matchedGroup = false;
|
|
3603
3759
|
let serverConfigs = [];
|
|
3604
3760
|
if (group) {
|
|
3605
3761
|
const groupDao = getGroupDao();
|
|
@@ -3611,6 +3767,10 @@ export const getFilteredServerInfosForGroup = async (group, options) => {
|
|
|
3611
3767
|
}
|
|
3612
3768
|
}
|
|
3613
3769
|
if (foundGroup) {
|
|
3770
|
+
matchedGroup = true;
|
|
3771
|
+
if (!canAccessGroupRoute(foundGroup)) {
|
|
3772
|
+
return { filteredServerInfos: [], serverConfigsByName: new Map() };
|
|
3773
|
+
}
|
|
3614
3774
|
serverConfigs = normalizeGroupServers(foundGroup.servers || []);
|
|
3615
3775
|
}
|
|
3616
3776
|
}
|
|
@@ -3626,7 +3786,7 @@ export const getFilteredServerInfosForGroup = async (group, options) => {
|
|
|
3626
3786
|
filteredServerInfos.push(serverInfo);
|
|
3627
3787
|
continue;
|
|
3628
3788
|
}
|
|
3629
|
-
if (
|
|
3789
|
+
if (!matchedGroup) {
|
|
3630
3790
|
if (serverInfo.name === group) {
|
|
3631
3791
|
filteredServerInfos.push(serverInfo);
|
|
3632
3792
|
}
|