@samanhappy/mcphub 1.0.42 → 1.0.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/index.js +19 -9
- package/dist/config/index.js.map +1 -1
- package/dist/controllers/groupController.js +14 -0
- package/dist/controllers/groupController.js.map +1 -1
- package/dist/controllers/oauthCallbackController.js +1 -1
- package/dist/controllers/oauthCallbackController.js.map +1 -1
- package/dist/controllers/serverController.js +102 -3
- package/dist/controllers/serverController.js.map +1 -1
- package/dist/controllers/smartRoutingController.js +15 -5
- package/dist/controllers/smartRoutingController.js.map +1 -1
- package/dist/db/entities/Group.js.map +1 -1
- package/dist/db/repositories/VectorEmbeddingRepository.js +21 -1
- package/dist/db/repositories/VectorEmbeddingRepository.js.map +1 -1
- package/dist/server.js +3 -2
- package/dist/server.js.map +1 -1
- package/dist/services/contextCostService.js +11 -4
- package/dist/services/contextCostService.js.map +1 -1
- package/dist/services/groupService.js +19 -2
- package/dist/services/groupService.js.map +1 -1
- package/dist/services/mcpOAuthProvider.js +51 -10
- package/dist/services/mcpOAuthProvider.js.map +1 -1
- package/dist/services/mcpService.js +228 -40
- package/dist/services/mcpService.js.map +1 -1
- package/dist/services/oauthClientRegistration.js +53 -8
- package/dist/services/oauthClientRegistration.js.map +1 -1
- package/dist/services/oauthSettingsStore.js +10 -3
- package/dist/services/oauthSettingsStore.js.map +1 -1
- package/dist/services/requestContextService.js.map +1 -1
- package/dist/services/smartRoutingService.js +109 -50
- package/dist/services/smartRoutingService.js.map +1 -1
- package/dist/services/sseService.js +61 -3
- package/dist/services/sseService.js.map +1 -1
- package/dist/services/vectorSearchService.js +72 -18
- package/dist/services/vectorSearchService.js.map +1 -1
- package/dist/utils/mcpRequestGroup.js +8 -0
- package/dist/utils/mcpRequestGroup.js.map +1 -0
- package/dist/utils/serverConfigPersistence.js +13 -2
- package/dist/utils/serverConfigPersistence.js.map +1 -1
- package/dist/utils/smartRouting.js +32 -0
- package/dist/utils/smartRouting.js.map +1 -1
- package/frontend/dist/assets/{ActivityPage-DTouw-He.js → ActivityPage-CW73GUD5.js} +2 -2
- package/frontend/dist/assets/{ActivityPage-DTouw-He.js.map → ActivityPage-CW73GUD5.js.map} +1 -1
- package/frontend/dist/assets/{CopyClientConfigDialog-BtO0YcVK.js → CopyClientConfigDialog-o6cE_gwR.js} +2 -2
- package/frontend/dist/assets/{CopyClientConfigDialog-BtO0YcVK.js.map → CopyClientConfigDialog-o6cE_gwR.js.map} +1 -1
- package/frontend/dist/assets/{CredentialsPage-BeSMd9m4.js → CredentialsPage-DtHT7dBz.js} +2 -2
- package/frontend/dist/assets/{CredentialsPage-BeSMd9m4.js.map → CredentialsPage-DtHT7dBz.js.map} +1 -1
- package/frontend/dist/assets/{Dashboard-DuaKaJAK.js → Dashboard-BANagmEB.js} +2 -2
- package/frontend/dist/assets/{Dashboard-DuaKaJAK.js.map → Dashboard-BANagmEB.js.map} +1 -1
- package/frontend/dist/assets/{EndpointCopy-Bos3ZWUc.js → EndpointCopy-DcF8AnZa.js} +2 -2
- package/frontend/dist/assets/{EndpointCopy-Bos3ZWUc.js.map → EndpointCopy-DcF8AnZa.js.map} +1 -1
- package/frontend/dist/assets/GroupsPage-Dcsd8nze.js +33 -0
- package/frontend/dist/assets/GroupsPage-Dcsd8nze.js.map +1 -0
- package/frontend/dist/assets/{LoginPage-DUnjHB-q.js → LoginPage-DxvqW7Nv.js} +2 -2
- package/frontend/dist/assets/{LoginPage-DUnjHB-q.js.map → LoginPage-DxvqW7Nv.js.map} +1 -1
- package/frontend/dist/assets/{LogsPage-BxjYh1MS.js → LogsPage-DIAxm1E8.js} +2 -2
- package/frontend/dist/assets/{LogsPage-BxjYh1MS.js.map → LogsPage-DIAxm1E8.js.map} +1 -1
- package/frontend/dist/assets/{MarketPage-BnxAofuO.js → MarketPage-JBz3na5T.js} +2 -2
- package/frontend/dist/assets/{MarketPage-BnxAofuO.js.map → MarketPage-JBz3na5T.js.map} +1 -1
- package/frontend/dist/assets/{OAuthConsentPage-rAx5ELdp.js → OAuthConsentPage-S5pynPb-.js} +2 -2
- package/frontend/dist/assets/{OAuthConsentPage-rAx5ELdp.js.map → OAuthConsentPage-S5pynPb-.js.map} +1 -1
- package/frontend/dist/assets/{PromptsPage-Bh4Woe2Q.js → PromptsPage-D7CSJJ7D.js} +2 -2
- package/frontend/dist/assets/{PromptsPage-Bh4Woe2Q.js.map → PromptsPage-D7CSJJ7D.js.map} +1 -1
- package/frontend/dist/assets/{ResourcesPage-BREUhWYG.js → ResourcesPage-Dha1wuaP.js} +2 -2
- package/frontend/dist/assets/{ResourcesPage-BREUhWYG.js.map → ResourcesPage-Dha1wuaP.js.map} +1 -1
- package/frontend/dist/assets/{ServersPage-DCqew7Dq.js → ServersPage-DDyCg3F6.js} +4 -4
- package/frontend/dist/assets/{ServersPage-DCqew7Dq.js.map → ServersPage-DDyCg3F6.js.map} +1 -1
- package/frontend/dist/assets/SettingsPage-i0qakyhB.js +12 -0
- package/frontend/dist/assets/SettingsPage-i0qakyhB.js.map +1 -0
- package/frontend/dist/assets/{StatusDot-CYPIH-Iz.js → StatusDot-D4PsbUse.js} +2 -2
- package/frontend/dist/assets/{StatusDot-CYPIH-Iz.js.map → StatusDot-D4PsbUse.js.map} +1 -1
- package/frontend/dist/assets/{ToggleGroup-B4Lt01jM.js → ToggleGroup-DR2is5di.js} +2 -2
- package/frontend/dist/assets/{ToggleGroup-B4Lt01jM.js.map → ToggleGroup-DR2is5di.js.map} +1 -1
- package/frontend/dist/assets/{UsersPage-LvOfLsJc.js → UsersPage-CfCbcfo8.js} +2 -2
- package/frontend/dist/assets/{UsersPage-LvOfLsJc.js.map → UsersPage-CfCbcfo8.js.map} +1 -1
- package/frontend/dist/assets/{clipboard--jmjkvaW.js → clipboard-D07Uwfz5.js} +2 -2
- package/frontend/dist/assets/{clipboard--jmjkvaW.js.map → clipboard-D07Uwfz5.js.map} +1 -1
- package/frontend/dist/assets/{icons-vendor-CNEaJWqW.js → icons-vendor-CvmEvkQ-.js} +69 -64
- package/frontend/dist/assets/icons-vendor-CvmEvkQ-.js.map +1 -0
- package/frontend/dist/assets/index-Cla8rRxG.css +1 -0
- package/frontend/dist/assets/index-DZZHe6u7.js +3 -0
- package/frontend/dist/assets/index-DZZHe6u7.js.map +1 -0
- package/frontend/dist/assets/{resourceService-DkIvmxRC.js → resourceService-bBf7W1JD.js} +2 -2
- package/frontend/dist/assets/{resourceService-DkIvmxRC.js.map → resourceService-bBf7W1JD.js.map} +1 -1
- package/frontend/dist/assets/{useCostData-B-uPi7i8.js → useCostData-CMKWXzc1.js} +2 -2
- package/frontend/dist/assets/{useCostData-B-uPi7i8.js.map → useCostData-CMKWXzc1.js.map} +1 -1
- package/frontend/dist/assets/{variableDetection-RlrBM33Q.js → variableDetection-R8Xv3uW7.js} +2 -2
- package/frontend/dist/assets/{variableDetection-RlrBM33Q.js.map → variableDetection-R8Xv3uW7.js.map} +1 -1
- package/frontend/dist/index.html +3 -3
- package/package.json +1 -1
- package/frontend/dist/assets/GroupsPage-Cv3lZSCh.js +0 -33
- package/frontend/dist/assets/GroupsPage-Cv3lZSCh.js.map +0 -1
- package/frontend/dist/assets/SettingsPage-DGAB-Jy8.js +0 -12
- package/frontend/dist/assets/SettingsPage-DGAB-Jy8.js.map +0 -1
- package/frontend/dist/assets/icons-vendor-CNEaJWqW.js.map +0 -1
- package/frontend/dist/assets/index-Cv7we3z-.css +0 -1
- package/frontend/dist/assets/index-qPkrYLbl.js +0 -3
- package/frontend/dist/assets/index-qPkrYLbl.js.map +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getMcpRequestGroup } from '../utils/mcpRequestGroup.js';
|
|
1
2
|
import { LegacyMcpClient } from '../clients/legacyMcpClient.js';
|
|
2
3
|
import { LEGACY_PROTOCOL_VERSIONS } from '../utils/mcpProtocol.js';
|
|
3
4
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
@@ -24,7 +25,7 @@ import config from '../config/index.js';
|
|
|
24
25
|
import { validateServerName } from '../utils/serverNameValidation.js';
|
|
25
26
|
import { getGroup } from './sseService.js';
|
|
26
27
|
import { getServerConfigInGroup, normalizeGroupServers } from './groupService.js';
|
|
27
|
-
import { removeServerToolEmbeddings, saveToolsAsVectorEmbeddings } from './vectorSearchService.js';
|
|
28
|
+
import { removeServerToolEmbeddings, removeToolEmbeddings, saveToolsAsVectorEmbeddings, } from './vectorSearchService.js';
|
|
28
29
|
import { OpenAPIClient } from '../clients/openapi.js';
|
|
29
30
|
import { RequestContextService } from './requestContextService.js';
|
|
30
31
|
import { getDataService } from './services.js';
|
|
@@ -517,7 +518,24 @@ const applyDescriptionOverridesForEmbedding = async (serverName, tools) => {
|
|
|
517
518
|
: tool;
|
|
518
519
|
});
|
|
519
520
|
};
|
|
520
|
-
|
|
521
|
+
// Tail of the embedding tasks queued per server. A per-user connect and a
|
|
522
|
+
// reindex, or two users connecting at once, often race on the same tool set;
|
|
523
|
+
// queued behind the first run, the second one hits the skip check in
|
|
524
|
+
// saveToolsAsVectorEmbeddings instead of embedding every tool a second time.
|
|
525
|
+
const embeddingSyncTails = new Map();
|
|
526
|
+
// Run `task` after every embedding task already queued for the server. A task
|
|
527
|
+
// must not wait for another queued task of the same server, or both stall.
|
|
528
|
+
const enqueueEmbeddingTask = (serverName, task) => {
|
|
529
|
+
const run = (embeddingSyncTails.get(serverName) ?? Promise.resolve()).then(task);
|
|
530
|
+
const tail = run.catch(() => undefined);
|
|
531
|
+
embeddingSyncTails.set(serverName, tail);
|
|
532
|
+
void tail.then(() => {
|
|
533
|
+
if (embeddingSyncTails.get(serverName) === tail)
|
|
534
|
+
embeddingSyncTails.delete(serverName);
|
|
535
|
+
});
|
|
536
|
+
return run;
|
|
537
|
+
};
|
|
538
|
+
export const syncToolsAsVectorEmbeddings = (serverName, tools, options) => enqueueEmbeddingTask(serverName, async () => {
|
|
521
539
|
const toolsWithOverrides = await applyDescriptionOverridesForEmbedding(serverName, tools);
|
|
522
540
|
const modelVisibleTools = filterModelVisibleTools(toolsWithOverrides);
|
|
523
541
|
if (modelVisibleTools.length === 0) {
|
|
@@ -527,6 +545,38 @@ const syncToolsAsVectorEmbeddings = async (serverName, tools, options) => {
|
|
|
527
545
|
return;
|
|
528
546
|
}
|
|
529
547
|
await saveToolsAsVectorEmbeddings(serverName, modelVisibleTools, options);
|
|
548
|
+
});
|
|
549
|
+
/**
|
|
550
|
+
* Embed the tools of a credential server, which never connects globally.
|
|
551
|
+
* Tools disabled in the server's tools config are never embedded: search
|
|
552
|
+
* filters them out of the hits anyway, but only after they have taken result
|
|
553
|
+
* slots. A toggle reaches the index without extra wiring: the tools config is
|
|
554
|
+
* part of the runtime revision, so the next acquire reconnects and re-syncs.
|
|
555
|
+
*
|
|
556
|
+
* Two callers, two scopes:
|
|
557
|
+
* - `partial: true` (every per-user connect): the list is what one user's
|
|
558
|
+
* credentials show, and users may see different tools. The rows of the
|
|
559
|
+
* listed, enabled tools are added or refreshed and nothing else is pruned,
|
|
560
|
+
* so tools only other users see stay indexed. The listed tools that are
|
|
561
|
+
* disabled are removed by name, queued before the sync.
|
|
562
|
+
* - otherwise (the reindex endpoint, which passes the union of every binding's
|
|
563
|
+
* tools): the list is complete, so rows of tools outside it are pruned.
|
|
564
|
+
*
|
|
565
|
+
* Resolves to the number of tools handed to the vector store.
|
|
566
|
+
*/
|
|
567
|
+
export const syncCredentialServerToolEmbeddings = async (serverName, tools, options) => {
|
|
568
|
+
const enabledTools = await filterToolsByConfig(serverName, tools);
|
|
569
|
+
if (options?.partial) {
|
|
570
|
+
const enabledNames = new Set(enabledTools.map((tool) => tool.name));
|
|
571
|
+
const disabledNames = tools
|
|
572
|
+
.map((tool) => tool.name)
|
|
573
|
+
.filter((toolName) => !enabledNames.has(toolName));
|
|
574
|
+
if (disabledNames.length > 0) {
|
|
575
|
+
void enqueueEmbeddingTask(serverName, () => removeToolEmbeddings(serverName, disabledNames)).catch(() => undefined);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
await syncToolsAsVectorEmbeddings(serverName, enabledTools, options);
|
|
579
|
+
return enabledTools.length;
|
|
530
580
|
};
|
|
531
581
|
// Normalize prompt payload to satisfy MCP ListPrompts response schema
|
|
532
582
|
const normalizePromptForList = (prompt) => {
|
|
@@ -652,6 +702,65 @@ const getVisibleServerByName = (name) => {
|
|
|
652
702
|
const sessionIsolatedClients = new Map();
|
|
653
703
|
// Locks to prevent concurrent creation of the same isolated client
|
|
654
704
|
const isolatedClientCreationLocks = new Map();
|
|
705
|
+
// Explicit application state for modern clients, independent of MCP transports.
|
|
706
|
+
// Active calls pin entries; idle state expires and admission is bounded.
|
|
707
|
+
const CLIENT_STATE_IDLE_MS = 30 * 60 * 1000;
|
|
708
|
+
const MAX_CLIENT_STATES = 1000;
|
|
709
|
+
const clientStates = new Map();
|
|
710
|
+
let clientStateSweep;
|
|
711
|
+
const removeClientState = (id) => {
|
|
712
|
+
const state = clientStates.get(id);
|
|
713
|
+
if (!state)
|
|
714
|
+
return;
|
|
715
|
+
clientStates.delete(id);
|
|
716
|
+
cleanupIsolatedSession(id);
|
|
717
|
+
for (const client of state.cookieClients)
|
|
718
|
+
client.clearSessionCookies(id);
|
|
719
|
+
};
|
|
720
|
+
const acquireClientState = (serverInfo) => {
|
|
721
|
+
const contextService = RequestContextService.getInstance();
|
|
722
|
+
const scope = contextService.getRequestContext()?.clientStateScope;
|
|
723
|
+
const handle = contextService.getHeader('x-mcphub-state-id');
|
|
724
|
+
if (!scope ||
|
|
725
|
+
typeof handle !== 'string' ||
|
|
726
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(handle)) {
|
|
727
|
+
throw new Error('Stateful tools on stateless MCP require authenticated bearer access and X-MCPHub-State-Id (UUID v4)');
|
|
728
|
+
}
|
|
729
|
+
const id = `client-state:${scope}:${handle.toLowerCase()}`;
|
|
730
|
+
let state = clientStates.get(id);
|
|
731
|
+
if (state && state.active === 0 && Date.now() - state.lastUsed >= CLIENT_STATE_IDLE_MS) {
|
|
732
|
+
removeClientState(id);
|
|
733
|
+
state = undefined;
|
|
734
|
+
}
|
|
735
|
+
if (!state) {
|
|
736
|
+
if (clientStates.size >= MAX_CLIENT_STATES) {
|
|
737
|
+
throw new Error('Client state capacity reached; retry after idle state expires');
|
|
738
|
+
}
|
|
739
|
+
state = { active: 0, lastUsed: Date.now(), cookieClients: new Set() };
|
|
740
|
+
clientStates.set(id, state);
|
|
741
|
+
}
|
|
742
|
+
if (!clientStateSweep) {
|
|
743
|
+
clientStateSweep = setInterval(() => {
|
|
744
|
+
for (const [key, entry] of clientStates) {
|
|
745
|
+
if (entry.active === 0 && Date.now() - entry.lastUsed >= CLIENT_STATE_IDLE_MS) {
|
|
746
|
+
removeClientState(key);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}, 60 * 1000);
|
|
750
|
+
clientStateSweep.unref();
|
|
751
|
+
}
|
|
752
|
+
if (serverInfo.openApiClient)
|
|
753
|
+
state.cookieClients.add(serverInfo.openApiClient);
|
|
754
|
+
state.active++;
|
|
755
|
+
const entry = state;
|
|
756
|
+
return {
|
|
757
|
+
id,
|
|
758
|
+
release: () => {
|
|
759
|
+
entry.active--;
|
|
760
|
+
entry.lastUsed = Date.now();
|
|
761
|
+
},
|
|
762
|
+
};
|
|
763
|
+
};
|
|
655
764
|
export const connectClientWithDiagnostics = async (client, transport, options) => {
|
|
656
765
|
try {
|
|
657
766
|
await client.connect(transport, options);
|
|
@@ -789,6 +898,11 @@ export const connected = () => {
|
|
|
789
898
|
// Global cleanup function to close all connections
|
|
790
899
|
export const cleanupAllServers = () => {
|
|
791
900
|
stopPackageUpdateSweep();
|
|
901
|
+
if (clientStateSweep)
|
|
902
|
+
clearInterval(clientStateSweep);
|
|
903
|
+
clientStateSweep = undefined;
|
|
904
|
+
for (const id of clientStates.keys())
|
|
905
|
+
removeClientState(id);
|
|
792
906
|
principalRuntimes.invalidate();
|
|
793
907
|
for (const serverInfo of serverInfos) {
|
|
794
908
|
try {
|
|
@@ -2880,17 +2994,7 @@ const projectToolForDownstream = (serverName, tool, appsRouteContext, serverConf
|
|
|
2880
2994
|
name: projectNameForGroup(projectedTool.name, serverName, serverConfig),
|
|
2881
2995
|
};
|
|
2882
2996
|
};
|
|
2883
|
-
const
|
|
2884
|
-
const sessionId = extra.sessionId || '';
|
|
2885
|
-
const group = getGroup(sessionId);
|
|
2886
|
-
logger.log(`Handling ListToolsRequest for group: ${group}`);
|
|
2887
|
-
// Special handling for $smart group to return smart routing tools
|
|
2888
|
-
// Support both $smart and $smart/{group} patterns
|
|
2889
|
-
if (isSmartRoutingGroup(group)) {
|
|
2890
|
-
return getSmartRoutingTools(group);
|
|
2891
|
-
}
|
|
2892
|
-
const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(group);
|
|
2893
|
-
const appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
|
|
2997
|
+
const listGroupTools = async (group, filteredServerInfos, serverConfigsByName, appsRouteContext) => {
|
|
2894
2998
|
// If the startup prime of an on-demand server is still in flight, wait for it
|
|
2895
2999
|
// so this list reflects the freshly cached tools instead of returning empty.
|
|
2896
3000
|
// No wake is triggered from list itself; the prime handles that. See #1029.
|
|
@@ -2920,8 +3024,60 @@ const handleListToolsRequestImpl = async (_, extra) => {
|
|
|
2920
3024
|
}));
|
|
2921
3025
|
}
|
|
2922
3026
|
}
|
|
3027
|
+
return allTools;
|
|
3028
|
+
};
|
|
3029
|
+
// Names the call handler always routes to a meta-tool on a Smart Routing session,
|
|
3030
|
+
// whichever of them the current mode lists (see handleCallToolRequestImpl)
|
|
3031
|
+
const SMART_ROUTING_META_TOOL_NAMES = new Set(['search_tools', 'describe_tool', 'call_tool']);
|
|
3032
|
+
// Tools a $smart/<group> lists next to its meta-tools: each member's pinnedTools,
|
|
3033
|
+
// narrowed to its tools selection and then run through the same filtering and
|
|
3034
|
+
// projection as a direct group listing.
|
|
3035
|
+
const getPinnedSmartRoutingTools = async (group) => {
|
|
3036
|
+
const lookupGroup = getGroupLookupName(group);
|
|
3037
|
+
if (!lookupGroup) {
|
|
3038
|
+
return [];
|
|
3039
|
+
}
|
|
3040
|
+
const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(lookupGroup);
|
|
3041
|
+
const pinnedConfigsByName = new Map();
|
|
3042
|
+
for (const [serverName, serverConfig] of serverConfigsByName) {
|
|
3043
|
+
const selection = serverConfig.tools;
|
|
3044
|
+
const pinnedTools = (serverConfig.pinnedTools ?? []).filter((toolName) => !Array.isArray(selection) || selection.includes(toolName));
|
|
3045
|
+
if (pinnedTools.length > 0) {
|
|
3046
|
+
pinnedConfigsByName.set(serverName, { ...serverConfig, tools: pinnedTools });
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
if (pinnedConfigsByName.size === 0) {
|
|
3050
|
+
return [];
|
|
3051
|
+
}
|
|
3052
|
+
return listGroupTools(lookupGroup, filteredServerInfos.filter((serverInfo) => pinnedConfigsByName.has(serverInfo.name)), pinnedConfigsByName, { enabled: false });
|
|
3053
|
+
};
|
|
3054
|
+
const handleListToolsRequestImpl = async (_, extra) => {
|
|
3055
|
+
const sessionId = extra.sessionId || '';
|
|
3056
|
+
const group = getMcpRequestGroup(extra);
|
|
3057
|
+
logger.log(`Handling ListToolsRequest for group: ${group}`);
|
|
3058
|
+
// Special handling for $smart group to return smart routing tools
|
|
3059
|
+
// Support both $smart and $smart/{group} patterns
|
|
3060
|
+
if (isSmartRoutingGroup(group)) {
|
|
3061
|
+
const smartRoutingTools = await getSmartRoutingTools(group);
|
|
3062
|
+
let pinnedTools = [];
|
|
3063
|
+
try {
|
|
3064
|
+
// A pin named like a meta-tool (e.g. alias "describe" + separator "_" +
|
|
3065
|
+
// tool "tool") would be intercepted by it on call, even in a mode that
|
|
3066
|
+
// does not list that meta-tool, so it is not listed at all
|
|
3067
|
+
pinnedTools = (await getPinnedSmartRoutingTools(group)).filter((tool) => !SMART_ROUTING_META_TOOL_NAMES.has(tool.name));
|
|
3068
|
+
}
|
|
3069
|
+
catch (error) {
|
|
3070
|
+
// Pins are an optimisation: failing to resolve them must not cost the meta-tools
|
|
3071
|
+
logger.warn(`Failed to list pinned tools for ${group}; returning meta-tools only`, error);
|
|
3072
|
+
}
|
|
3073
|
+
return pinnedTools.length > 0
|
|
3074
|
+
? { tools: [...smartRoutingTools.tools, ...pinnedTools] }
|
|
3075
|
+
: smartRoutingTools;
|
|
3076
|
+
}
|
|
3077
|
+
const { filteredServerInfos, serverConfigsByName } = await getFilteredServerInfosForGroup(group);
|
|
3078
|
+
const appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
|
|
2923
3079
|
return {
|
|
2924
|
-
tools:
|
|
3080
|
+
tools: await listGroupTools(group, filteredServerInfos, serverConfigsByName, appsRouteContext),
|
|
2925
3081
|
};
|
|
2926
3082
|
};
|
|
2927
3083
|
const handleCallToolRequestImpl = async (request, extra) => {
|
|
@@ -2946,7 +3102,7 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
2946
3102
|
].find((id) => typeof id === 'string' && id.length > 0 && !isSyntheticSessionFallback(id));
|
|
2947
3103
|
// Extract group and key info from request context (set by SSE/HTTP handlers)
|
|
2948
3104
|
// Fallback to extra for backward compatibility (e.g., direct API calls)
|
|
2949
|
-
const group =
|
|
3105
|
+
const group = getMcpRequestGroup(extra);
|
|
2950
3106
|
const username = requestContextService.getUsernameContext() ||
|
|
2951
3107
|
extra?.username ||
|
|
2952
3108
|
(requestContextService.getKeyKindContext() === 'system' ? 'system' : undefined) ||
|
|
@@ -2957,6 +3113,13 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
2957
3113
|
const sourceIp = requestContextService.getRequestContext()?.remoteAddress || undefined;
|
|
2958
3114
|
let hostedReservation = null;
|
|
2959
3115
|
let releaseOnDemandToolCall;
|
|
3116
|
+
let clientStateLease;
|
|
3117
|
+
const getToolStateId = (info, legacyId) => {
|
|
3118
|
+
if (!requestContextService.getRequestContext()?.stateless)
|
|
3119
|
+
return legacyId;
|
|
3120
|
+
clientStateLease ?? (clientStateLease = acquireClientState(info));
|
|
3121
|
+
return clientStateLease.id;
|
|
3122
|
+
};
|
|
2960
3123
|
const reserveHostedIfNeeded = async (serverName, toolName) => {
|
|
2961
3124
|
const hostedAuth = requestContextService.getHostedAuthContext();
|
|
2962
3125
|
assertHostedToolAllowed(hostedAuth, serverName, toolName);
|
|
@@ -2977,12 +3140,12 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
2977
3140
|
// Special handling for smart routing tools
|
|
2978
3141
|
if (request.params.name === 'search_tools') {
|
|
2979
3142
|
const { query, limit = 10 } = request.params.arguments || {};
|
|
2980
|
-
return await handleSearchToolsRequest(query, limit, sessionId);
|
|
3143
|
+
return await handleSearchToolsRequest(query, limit, sessionId, group);
|
|
2981
3144
|
}
|
|
2982
3145
|
// Special handling for describe_tool (progressive disclosure mode)
|
|
2983
3146
|
if (request.params.name === 'describe_tool') {
|
|
2984
3147
|
const { toolName } = request.params.arguments || {};
|
|
2985
|
-
return await handleDescribeToolRequest(toolName, sessionId);
|
|
3148
|
+
return await handleDescribeToolRequest(toolName, sessionId, group);
|
|
2986
3149
|
}
|
|
2987
3150
|
// Special handling for call_tool
|
|
2988
3151
|
if (request.params.name === 'call_tool') {
|
|
@@ -3080,8 +3243,14 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3080
3243
|
}
|
|
3081
3244
|
}
|
|
3082
3245
|
}
|
|
3246
|
+
const stateId = targetServerInfo.config?.openapi?.cookieSession
|
|
3247
|
+
? getToolStateId(targetServerInfo, cookieSessionId)
|
|
3248
|
+
: cookieSessionId;
|
|
3249
|
+
if (targetServerInfo.config?.openapi?.cookieSession && !stateId) {
|
|
3250
|
+
throw new Error(`OpenAPI server '${targetServerInfo.name}' requires cookie-session state and is not yet available on stateless MCP requests`);
|
|
3251
|
+
}
|
|
3083
3252
|
await reserveHostedIfNeeded(targetServerInfo.name, cleanToolName);
|
|
3084
|
-
const result = await openApiClient.callTool(cleanToolName, finalArgs, passthroughHeaders, false,
|
|
3253
|
+
const result = await openApiClient.callTool(cleanToolName, finalArgs, passthroughHeaders, false, stateId);
|
|
3085
3254
|
await settleHostedIfNeeded({
|
|
3086
3255
|
success: true,
|
|
3087
3256
|
requestContent: finalArgs,
|
|
@@ -3125,10 +3294,17 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3125
3294
|
// For servers with perSessionClient: true, use a per-session dedicated client
|
|
3126
3295
|
let isolatedCtx;
|
|
3127
3296
|
if (targetServerInfo.config?.perSessionClient &&
|
|
3128
|
-
!hasCredentialTemplate(targetServerInfo.config)
|
|
3129
|
-
sessionId)
|
|
3130
|
-
|
|
3131
|
-
|
|
3297
|
+
!hasCredentialTemplate(targetServerInfo.config)) {
|
|
3298
|
+
const stateId = getToolStateId(targetServerInfo, sessionId);
|
|
3299
|
+
if (!stateId) {
|
|
3300
|
+
throw new Error(`Server '${targetServerInfo.name}' requires per-session client isolation and is not yet available on stateless MCP requests`);
|
|
3301
|
+
}
|
|
3302
|
+
const isolated = await getOrCreateIsolatedClient(stateId, targetServerInfo);
|
|
3303
|
+
isolatedCtx = {
|
|
3304
|
+
sessionId: stateId,
|
|
3305
|
+
client: isolated.client,
|
|
3306
|
+
transport: isolated.transport,
|
|
3307
|
+
};
|
|
3132
3308
|
}
|
|
3133
3309
|
else if (!targetServerInfo.client) {
|
|
3134
3310
|
throw new Error(`Client not found for server: ${targetServerInfo.name}`);
|
|
@@ -3246,8 +3422,14 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3246
3422
|
}
|
|
3247
3423
|
}
|
|
3248
3424
|
const finalArgs = request.params.arguments || {};
|
|
3425
|
+
const stateId = serverInfo.config?.openapi?.cookieSession
|
|
3426
|
+
? getToolStateId(serverInfo, cookieSessionId)
|
|
3427
|
+
: cookieSessionId;
|
|
3428
|
+
if (serverInfo.config?.openapi?.cookieSession && !stateId) {
|
|
3429
|
+
throw new Error(`OpenAPI server '${serverInfo.name}' requires cookie-session state and is not yet available on stateless MCP requests`);
|
|
3430
|
+
}
|
|
3249
3431
|
await reserveHostedIfNeeded(serverInfo.name, cleanToolName);
|
|
3250
|
-
const result = await openApiClient.callTool(cleanToolName, finalArgs, passthroughHeaders, false,
|
|
3432
|
+
const result = await openApiClient.callTool(cleanToolName, finalArgs, passthroughHeaders, false, stateId);
|
|
3251
3433
|
await settleHostedIfNeeded({
|
|
3252
3434
|
success: true,
|
|
3253
3435
|
requestContent: finalArgs,
|
|
@@ -3290,11 +3472,13 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3290
3472
|
// Handle MCP servers
|
|
3291
3473
|
// For servers with perSessionClient: true, use a per-session dedicated client
|
|
3292
3474
|
let isolatedCtx;
|
|
3293
|
-
if (serverInfo.config?.perSessionClient &&
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3475
|
+
if (serverInfo.config?.perSessionClient && !hasCredentialTemplate(serverInfo.config)) {
|
|
3476
|
+
const stateId = getToolStateId(serverInfo, sessionId);
|
|
3477
|
+
if (!stateId) {
|
|
3478
|
+
throw new Error(`Server '${serverInfo.name}' requires per-session client isolation and is not yet available on stateless MCP requests`);
|
|
3479
|
+
}
|
|
3480
|
+
const isolated = await getOrCreateIsolatedClient(stateId, serverInfo);
|
|
3481
|
+
isolatedCtx = { sessionId: stateId, client: isolated.client, transport: isolated.transport };
|
|
3298
3482
|
}
|
|
3299
3483
|
else if (!serverInfo.client) {
|
|
3300
3484
|
throw new Error(`Client not found for server: ${serverInfo.name}`);
|
|
@@ -3386,14 +3570,14 @@ const handleCallToolRequestImpl = async (request, extra) => {
|
|
|
3386
3570
|
};
|
|
3387
3571
|
}
|
|
3388
3572
|
finally {
|
|
3573
|
+
clientStateLease?.release();
|
|
3389
3574
|
releaseOnDemandToolCall?.();
|
|
3390
3575
|
}
|
|
3391
3576
|
};
|
|
3392
3577
|
const handleGetPromptRequestImpl = async (request, extra) => {
|
|
3393
3578
|
try {
|
|
3394
3579
|
const { name, arguments: promptArgs } = request.params;
|
|
3395
|
-
const
|
|
3396
|
-
const group = extra?.group || getGroup(sessionId) || undefined;
|
|
3580
|
+
const group = getMcpRequestGroup(extra);
|
|
3397
3581
|
// Check built-in prompts first
|
|
3398
3582
|
const builtinPrompt = await getBuiltinPromptDao().findByName(name);
|
|
3399
3583
|
if (builtinPrompt && builtinPrompt.enabled !== false) {
|
|
@@ -3479,8 +3663,7 @@ const handleGetPromptRequestImpl = async (request, extra) => {
|
|
|
3479
3663
|
}
|
|
3480
3664
|
};
|
|
3481
3665
|
const handleListPromptsRequestImpl = async (_, extra) => {
|
|
3482
|
-
const
|
|
3483
|
-
const group = getGroup(sessionId);
|
|
3666
|
+
const group = getMcpRequestGroup(extra);
|
|
3484
3667
|
const lookupGroup = getGroupLookupName(group);
|
|
3485
3668
|
logger.log(`Handling ListPromptsRequest for group: ${group}`);
|
|
3486
3669
|
// Start with built-in prompts (only enabled ones)
|
|
@@ -3524,7 +3707,7 @@ const handleListPromptsRequestImpl = async (_, extra) => {
|
|
|
3524
3707
|
};
|
|
3525
3708
|
const handleListResourcesRequestImpl = async (_, extra) => {
|
|
3526
3709
|
const sessionId = extra.sessionId || '';
|
|
3527
|
-
const group =
|
|
3710
|
+
const group = getMcpRequestGroup(extra);
|
|
3528
3711
|
const lookupGroup = getGroupLookupName(group);
|
|
3529
3712
|
logger.log(`Handling ListResourcesRequest for group: ${group}`);
|
|
3530
3713
|
const appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
|
|
@@ -3569,7 +3752,7 @@ const handleListResourcesRequestImpl = async (_, extra) => {
|
|
|
3569
3752
|
};
|
|
3570
3753
|
const handleListResourceTemplatesRequestImpl = async (_, extra) => {
|
|
3571
3754
|
const sessionId = extra.sessionId || '';
|
|
3572
|
-
const group =
|
|
3755
|
+
const group = getMcpRequestGroup(extra);
|
|
3573
3756
|
const lookupGroup = getGroupLookupName(group);
|
|
3574
3757
|
logger.log(`Handling ListResourceTemplatesRequest for group: ${group}`);
|
|
3575
3758
|
const appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
|
|
@@ -3594,7 +3777,7 @@ const handleReadResourceRequestImpl = async (request, extra) => {
|
|
|
3594
3777
|
try {
|
|
3595
3778
|
const { uri } = request.params;
|
|
3596
3779
|
const sessionId = extra.sessionId || '';
|
|
3597
|
-
const group =
|
|
3780
|
+
const group = getMcpRequestGroup(extra);
|
|
3598
3781
|
const lookupGroup = getGroupLookupName(group);
|
|
3599
3782
|
const appsRouteContext = await getMcpAppsRouteContext(sessionId, group);
|
|
3600
3783
|
// Check built-in resources first
|
|
@@ -3687,7 +3870,9 @@ const handleReadResourceRequestImpl = async (request, extra) => {
|
|
|
3687
3870
|
};
|
|
3688
3871
|
}
|
|
3689
3872
|
};
|
|
3690
|
-
// Personal runtimes never enter serverInfos
|
|
3873
|
+
// Personal runtimes never enter serverInfos or config exports. Their tool list
|
|
3874
|
+
// is added to the rows under the server name, the index the reindex endpoint
|
|
3875
|
+
// rebuilds from every binding.
|
|
3691
3876
|
const createPrincipalRuntime = async (name, resolvedConfig) => {
|
|
3692
3877
|
const info = {
|
|
3693
3878
|
name,
|
|
@@ -3769,6 +3954,12 @@ const createPrincipalRuntime = async (name, resolvedConfig) => {
|
|
|
3769
3954
|
};
|
|
3770
3955
|
}
|
|
3771
3956
|
info.status = 'connected';
|
|
3957
|
+
// A credential server never connects globally, so this is the only place
|
|
3958
|
+
// its tools are seen outside a reindex. One user's list is not the whole
|
|
3959
|
+
// server's, so this only adds and refreshes rows; unchanged ones are skipped.
|
|
3960
|
+
syncCredentialServerToolEmbeddings(name, info.tools, { partial: true }).catch(() => {
|
|
3961
|
+
logger.warn(`[EMBED_SYNC_ERROR] Failed to sync tool embeddings for "${name}"`);
|
|
3962
|
+
});
|
|
3772
3963
|
return info;
|
|
3773
3964
|
}
|
|
3774
3965
|
catch (error) {
|
|
@@ -3815,10 +4006,7 @@ const withPrincipalServers = (handler, operation) => async (request, extra) => {
|
|
|
3815
4006
|
if (builtin && builtin.enabled !== false)
|
|
3816
4007
|
return handler(request, extra);
|
|
3817
4008
|
}
|
|
3818
|
-
const group =
|
|
3819
|
-
extra?.group ||
|
|
3820
|
-
getGroup(extra?.sessionId || '') ||
|
|
3821
|
-
undefined;
|
|
4009
|
+
const group = getMcpRequestGroup(extra);
|
|
3822
4010
|
const { filteredServerInfos: candidates, serverConfigsByName } = await getFilteredServerInfosForGroup(getGroupLookupName(group));
|
|
3823
4011
|
const requestedName = ['call_tool', 'describe_tool'].includes(request?.params?.name)
|
|
3824
4012
|
? request.params.arguments?.toolName
|