@samanhappy/mcphub 1.0.32 → 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/clients/openapi.js +439 -72
- package/dist/clients/openapi.js.map +1 -1
- package/dist/config/index.js +24 -2
- package/dist/config/index.js.map +1 -1
- package/dist/constants/oauthServerDefaults.js +5 -0
- package/dist/constants/oauthServerDefaults.js.map +1 -1
- package/dist/controllers/mcpbController.js +6 -13
- package/dist/controllers/mcpbController.js.map +1 -1
- package/dist/controllers/oauthCallbackController.js +26 -0
- package/dist/controllers/oauthCallbackController.js.map +1 -1
- package/dist/controllers/oauthServerController.js +40 -4
- package/dist/controllers/oauthServerController.js.map +1 -1
- package/dist/controllers/serverController.js +108 -31
- package/dist/controllers/serverController.js.map +1 -1
- package/dist/controllers/templateController.js +1 -1
- package/dist/controllers/templateController.js.map +1 -1
- package/dist/routes/index.js +2 -1
- package/dist/routes/index.js.map +1 -1
- package/dist/services/authorizationService.js +28 -0
- package/dist/services/authorizationService.js.map +1 -0
- package/dist/services/cimdClientService.js +195 -0
- package/dist/services/cimdClientService.js.map +1 -0
- package/dist/services/mcpService.js +108 -11
- package/dist/services/mcpService.js.map +1 -1
- package/dist/services/oauthServerService.js +17 -1
- package/dist/services/oauthServerService.js.map +1 -1
- package/dist/services/openApiToolStatsService.js +37 -0
- package/dist/services/openApiToolStatsService.js.map +1 -0
- package/dist/services/serverConfigPresenter.js +85 -0
- package/dist/services/serverConfigPresenter.js.map +1 -0
- package/dist/services/templateService.js +48 -11
- package/dist/services/templateService.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/processTree.js +47 -0
- package/dist/utils/processTree.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/ssrf.js +4 -4
- package/dist/utils/ssrf.js.map +1 -1
- package/dist/utils/systemConfigCache.js +40 -2
- package/dist/utils/systemConfigCache.js.map +1 -1
- package/frontend/dist/assets/{ActivityPage-C6m1hGLq.js → ActivityPage-Du9Nh-Jy.js} +2 -2
- package/frontend/dist/assets/{ActivityPage-C6m1hGLq.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-DFKfjPff.js.map → Dashboard-J4G-AZkI.js.map} +1 -1
- package/frontend/dist/assets/{EndpointCopy-CP_Fb1To.js → EndpointCopy-DFeLcU9A.js} +2 -2
- package/frontend/dist/assets/{EndpointCopy-CP_Fb1To.js.map → EndpointCopy-DFeLcU9A.js.map} +1 -1
- package/frontend/dist/assets/GroupsPage-CGMZBiXd.js +33 -0
- package/frontend/dist/assets/{GroupsPage-Cwfm5q-a.js.map → GroupsPage-CGMZBiXd.js.map} +1 -1
- package/frontend/dist/assets/{LoginPage-CnF44h_n.js → LoginPage-g5pJLBk0.js} +2 -2
- package/frontend/dist/assets/{LoginPage-CnF44h_n.js.map → LoginPage-g5pJLBk0.js.map} +1 -1
- package/frontend/dist/assets/{LogsPage-BFIpICsM.js → LogsPage-Bkv7xhNf.js} +2 -2
- package/frontend/dist/assets/{LogsPage-BFIpICsM.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-zqXUvKMF.js → OAuthConsentPage-CA35XoBK.js} +2 -2
- package/frontend/dist/assets/{OAuthConsentPage-zqXUvKMF.js.map → OAuthConsentPage-CA35XoBK.js.map} +1 -1
- package/frontend/dist/assets/{PromptsPage-Cm8ykiI4.js → PromptsPage-BuDdW69R.js} +2 -2
- package/frontend/dist/assets/{PromptsPage-Cm8ykiI4.js.map → PromptsPage-BuDdW69R.js.map} +1 -1
- package/frontend/dist/assets/{ResourcesPage-UMyO6mQ4.js → ResourcesPage-Cu-eD5e2.js} +2 -2
- package/frontend/dist/assets/{ResourcesPage-UMyO6mQ4.js.map → ResourcesPage-Cu-eD5e2.js.map} +1 -1
- package/frontend/dist/assets/{ServersPage-B0RoM8ez.js → ServersPage-B1Np5x3N.js} +5 -5
- package/frontend/dist/assets/{ServersPage-B0RoM8ez.js.map → ServersPage-B1Np5x3N.js.map} +1 -1
- package/frontend/dist/assets/{SettingsPage-BTB-36-n.js → SettingsPage-BQN8-ZQh.js} +4 -4
- package/frontend/dist/assets/SettingsPage-BQN8-ZQh.js.map +1 -0
- package/frontend/dist/assets/{StatusDot-BAMXeT2x.js → StatusDot-WyvgqplP.js} +2 -2
- package/frontend/dist/assets/{StatusDot-BAMXeT2x.js.map → StatusDot-WyvgqplP.js.map} +1 -1
- package/frontend/dist/assets/{ToggleGroup-Cfgt5lJo.js → ToggleGroup-CmeedsZU.js} +2 -2
- package/frontend/dist/assets/{ToggleGroup-Cfgt5lJo.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/index-BvU2-VT8.js +3 -0
- package/frontend/dist/assets/{index-CvXw771N.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-D2QmlFby.js → resourceService-BFH4w47h.js} +2 -2
- package/frontend/dist/assets/{resourceService-D2QmlFby.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-CeCvZ2nU.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 +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/ConfirmDialog-BCMNzi21.js +0 -2
- package/frontend/dist/assets/ConfirmDialog-BCMNzi21.js.map +0 -1
- package/frontend/dist/assets/Dashboard-DFKfjPff.js +0 -2
- package/frontend/dist/assets/GroupsPage-Cwfm5q-a.js +0 -33
- package/frontend/dist/assets/MarketPage-Du-NqiEl.js +0 -3
- package/frontend/dist/assets/MarketPage-Du-NqiEl.js.map +0 -1
- package/frontend/dist/assets/SettingsPage-BTB-36-n.js.map +0 -1
- package/frontend/dist/assets/UsersPage-DlzYf7Su.js +0 -2
- package/frontend/dist/assets/UsersPage-DlzYf7Su.js.map +0 -1
- package/frontend/dist/assets/contextCost-kp2vZYp7.js +0 -2
- package/frontend/dist/assets/contextCost-kp2vZYp7.js.map +0 -1
- package/frontend/dist/assets/index-CGvImZ3F.css +0 -1
- package/frontend/dist/assets/index-CvXw771N.js +0 -3
- package/frontend/dist/assets/useSettingsData-CeCvZ2nU.js +0 -2
- package/frontend/dist/assets/variableDetection-DKPiHkxr.js +0 -16
- package/frontend/dist/assets/variableDetection-DKPiHkxr.js.map +0 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client ID Metadata Documents (CIMD) support for MCPHub's authorization server.
|
|
3
|
+
*
|
|
4
|
+
* Implements the client side of draft-ietf-oauth-client-id-metadata-document:
|
|
5
|
+
* a client may present an HTTPS URL as its client_id; the authorization server
|
|
6
|
+
* fetches a metadata document from that URL and uses it in place of a
|
|
7
|
+
* registration. CIMD clients are always treated as PUBLIC clients (no shared
|
|
8
|
+
* secret can be established out-of-band), so PKCE with S256 is enforced by the
|
|
9
|
+
* existing public-client policy.
|
|
10
|
+
*
|
|
11
|
+
* Documents are fetched with SSRF protection (private-IP blocklist +
|
|
12
|
+
* redirect validation, shared with upstream MCP connections) and cached with
|
|
13
|
+
* a TTL. Resolution is opt-in via systemConfig.oauthServer.clientIdMetadata.enabled.
|
|
14
|
+
*/
|
|
15
|
+
import { getSystemConfigDao } from '../dao/index.js';
|
|
16
|
+
import { cloneDefaultOAuthServerConfig } from '../constants/oauthServerDefaults.js';
|
|
17
|
+
import { findOAuthClientById } from '../models/OAuth.js';
|
|
18
|
+
import { assertSafeUrl, createRedirectValidatingFetch } from '../utils/ssrf.js';
|
|
19
|
+
import { logger } from '../utils/logger.js';
|
|
20
|
+
const METADATA_FETCH_TIMEOUT_MS = 5000;
|
|
21
|
+
const DEFAULT_CACHE_TTL_MS = 3600000;
|
|
22
|
+
const MAX_DOCUMENT_BYTES = 256 * 1024;
|
|
23
|
+
export const isCimdClientId = (clientId) => {
|
|
24
|
+
return typeof clientId === 'string' && clientId.startsWith('https://');
|
|
25
|
+
};
|
|
26
|
+
const cache = new Map();
|
|
27
|
+
const inflight = new Map();
|
|
28
|
+
let fetchImpl = (...args) => fetch(...args);
|
|
29
|
+
// Test seam: DNS resolution used by the SSRF guard, so tests never hit real DNS.
|
|
30
|
+
let ssrfLookupOverride;
|
|
31
|
+
/** Test seam: replace the fetch implementation used for metadata retrieval. */
|
|
32
|
+
export const setCimdFetchImplForTesting = (impl) => {
|
|
33
|
+
fetchImpl = impl;
|
|
34
|
+
};
|
|
35
|
+
/** Test seam: replace DNS resolution used by the SSRF guard. */
|
|
36
|
+
export const setCimdDnsLookupForTesting = (lookup) => {
|
|
37
|
+
ssrfLookupOverride = lookup;
|
|
38
|
+
};
|
|
39
|
+
export const clearCimdCacheForTesting = () => {
|
|
40
|
+
cache.clear();
|
|
41
|
+
inflight.clear();
|
|
42
|
+
};
|
|
43
|
+
export const getCimdSettings = async () => {
|
|
44
|
+
try {
|
|
45
|
+
const systemConfig = await getSystemConfigDao().get();
|
|
46
|
+
const merged = { ...cloneDefaultOAuthServerConfig(), ...systemConfig?.oauthServer };
|
|
47
|
+
const ttl = typeof merged.clientIdMetadata?.cacheTtlMs === 'number' &&
|
|
48
|
+
merged.clientIdMetadata.cacheTtlMs > 0
|
|
49
|
+
? merged.clientIdMetadata.cacheTtlMs
|
|
50
|
+
: DEFAULT_CACHE_TTL_MS;
|
|
51
|
+
return { enabled: merged.clientIdMetadata?.enabled === true, cacheTtlMs: ttl };
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { enabled: false, cacheTtlMs: DEFAULT_CACHE_TTL_MS };
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const asStringArray = (value) => Array.isArray(value) && value.every((v) => typeof v === 'string') ? value : undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Validate a fetched metadata document and map it onto an IOAuthClient.
|
|
60
|
+
* Returns undefined when the document is not a usable CIMD client.
|
|
61
|
+
*/
|
|
62
|
+
export const mapCimdDocument = (clientIdUrl, document) => {
|
|
63
|
+
if (!document || typeof document !== 'object' || Array.isArray(document)) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
// The document's client_id MUST be present and identical to the URL it was
|
|
67
|
+
// retrieved from (draft-ietf-oauth-client-id-metadata-document, section 4).
|
|
68
|
+
if (document.client_id !== clientIdUrl) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
const redirectUris = asStringArray(document.redirect_uris);
|
|
72
|
+
if (!redirectUris || redirectUris.length === 0 || !redirectUris.every((uri) => uri)) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
// Only public clients can be expressed via CIMD on this server; a document
|
|
76
|
+
// claiming a secret-based auth method cannot be honored safely.
|
|
77
|
+
const authMethod = typeof document.token_endpoint_auth_method === 'string'
|
|
78
|
+
? document.token_endpoint_auth_method
|
|
79
|
+
: 'none';
|
|
80
|
+
if (authMethod !== 'none') {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const applicationType = document.application_type === 'native' ? 'native' : 'web';
|
|
84
|
+
const contacts = asStringArray(document.contacts);
|
|
85
|
+
const optionalUriField = (value) => typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
86
|
+
return {
|
|
87
|
+
clientId: clientIdUrl,
|
|
88
|
+
name: typeof document.client_name === 'string' && document.client_name.length > 0
|
|
89
|
+
? document.client_name
|
|
90
|
+
: clientIdUrl,
|
|
91
|
+
redirectUris,
|
|
92
|
+
grants: ['authorization_code', 'refresh_token'],
|
|
93
|
+
metadata: {
|
|
94
|
+
application_type: applicationType,
|
|
95
|
+
...(contacts ? { contacts } : {}),
|
|
96
|
+
...(optionalUriField(document.logo_uri) ? { logo_uri: optionalUriField(document.logo_uri) } : {}),
|
|
97
|
+
...(optionalUriField(document.client_uri) ? { client_uri: optionalUriField(document.client_uri) } : {}),
|
|
98
|
+
...(optionalUriField(document.policy_uri) ? { policy_uri: optionalUriField(document.policy_uri) } : {}),
|
|
99
|
+
...(optionalUriField(document.tos_uri) ? { tos_uri: optionalUriField(document.tos_uri) } : {}),
|
|
100
|
+
...(optionalUriField(document.jwks_uri) ? { jwks_uri: optionalUriField(document.jwks_uri) } : {}),
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
const fetchCimdDocument = async (clientIdUrl) => {
|
|
105
|
+
await assertSafeUrl(clientIdUrl, {
|
|
106
|
+
allowInternal: false,
|
|
107
|
+
...(ssrfLookupOverride ? { lookup: ssrfLookupOverride } : {}),
|
|
108
|
+
});
|
|
109
|
+
const validatingFetch = createRedirectValidatingFetch((url, init) => fetchImpl(url, init), false);
|
|
110
|
+
const response = await validatingFetch(clientIdUrl, {
|
|
111
|
+
method: 'GET',
|
|
112
|
+
headers: { Accept: 'application/json' },
|
|
113
|
+
signal: AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS),
|
|
114
|
+
redirect: 'manual',
|
|
115
|
+
});
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
logger.warn('CIMD metadata document fetch failed', {
|
|
118
|
+
clientIdUrl,
|
|
119
|
+
status: response.status,
|
|
120
|
+
});
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const contentLength = Number(response.headers.get('content-length') || '0');
|
|
124
|
+
if (contentLength > MAX_DOCUMENT_BYTES) {
|
|
125
|
+
logger.warn('CIMD metadata document exceeds size limit', { clientIdUrl });
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
let document;
|
|
129
|
+
try {
|
|
130
|
+
const text = await response.text();
|
|
131
|
+
if (text.length > MAX_DOCUMENT_BYTES) {
|
|
132
|
+
logger.warn('CIMD metadata document exceeds size limit', { clientIdUrl });
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
document = JSON.parse(text);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
logger.warn('CIMD metadata document is not valid JSON', { clientIdUrl, error });
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
const mapped = mapCimdDocument(clientIdUrl, document);
|
|
142
|
+
if (!mapped) {
|
|
143
|
+
logger.warn('CIMD metadata document failed validation', { clientIdUrl });
|
|
144
|
+
}
|
|
145
|
+
return mapped;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Resolve a URL-shaped client_id to a client by fetching (or caching) its
|
|
149
|
+
* metadata document. Returns undefined for anything that is not a valid,
|
|
150
|
+
* enabled CIMD identifier.
|
|
151
|
+
*/
|
|
152
|
+
export const resolveCimdClient = async (clientId, settings) => {
|
|
153
|
+
const resolvedSettings = settings ?? (await getCimdSettings());
|
|
154
|
+
if (!resolvedSettings.enabled || !isCimdClientId(clientId)) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
const cached = cache.get(clientId);
|
|
159
|
+
if (cached && cached.expiresAt > now) {
|
|
160
|
+
return cached.client;
|
|
161
|
+
}
|
|
162
|
+
const existingFetch = inflight.get(clientId);
|
|
163
|
+
if (existingFetch) {
|
|
164
|
+
return existingFetch;
|
|
165
|
+
}
|
|
166
|
+
const fetchPromise = fetchCimdDocument(clientId)
|
|
167
|
+
.then((client) => {
|
|
168
|
+
if (client) {
|
|
169
|
+
cache.set(clientId, { client, expiresAt: Date.now() + resolvedSettings.cacheTtlMs });
|
|
170
|
+
}
|
|
171
|
+
return client;
|
|
172
|
+
})
|
|
173
|
+
.catch((error) => {
|
|
174
|
+
logger.warn('Failed to resolve CIMD client', { clientId, error });
|
|
175
|
+
return undefined;
|
|
176
|
+
})
|
|
177
|
+
.finally(() => {
|
|
178
|
+
inflight.delete(clientId);
|
|
179
|
+
});
|
|
180
|
+
inflight.set(clientId, fetchPromise);
|
|
181
|
+
return fetchPromise;
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Client lookup used across the authorization server surface: registered
|
|
185
|
+
* clients first, then CIMD resolution when enabled. Drop-in replacement for
|
|
186
|
+
* findOAuthClientById at every call site that must understand CIMD clients.
|
|
187
|
+
*/
|
|
188
|
+
export const findOAuthClientIncludingCimd = async (clientId) => {
|
|
189
|
+
const persisted = await findOAuthClientById(clientId);
|
|
190
|
+
if (persisted) {
|
|
191
|
+
return persisted;
|
|
192
|
+
}
|
|
193
|
+
return resolveCimdClient(clientId);
|
|
194
|
+
};
|
|
195
|
+
//# sourceMappingURL=cimdClientService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cimdClientService.js","sourceRoot":"","sources":["../../src/services/cimdClientService.ts"],"names":[],"mappings":"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"}
|
|
@@ -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';
|
|
@@ -1184,6 +1186,15 @@ export const initializeClientsFromSettings = async (isInit, serverName, options)
|
|
|
1184
1186
|
try {
|
|
1185
1187
|
for (const conf of allServers) {
|
|
1186
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
|
+
}
|
|
1187
1198
|
// Expand environment variables in all configuration values
|
|
1188
1199
|
const expandedConf = replaceEnvVars(conf);
|
|
1189
1200
|
// Skip disabled servers
|
|
@@ -1755,6 +1766,42 @@ const filterToolsByConfig = async (serverName, tools) => {
|
|
|
1755
1766
|
const getServerByTool = (toolName) => {
|
|
1756
1767
|
return getVisibleServerInfos().find((serverInfo) => serverInfo.enabled !== false && serverInfo.tools.some((tool) => tool.name === toolName));
|
|
1757
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
|
+
};
|
|
1758
1805
|
// Add new server
|
|
1759
1806
|
export const addServer = async (name, config) => {
|
|
1760
1807
|
const server = { name, ...config };
|
|
@@ -2090,7 +2137,28 @@ export const resetServerOAuthConnection = (name) => {
|
|
|
2090
2137
|
// launched through a wrapper like `npx` / `npm exec`, the wrapper does not
|
|
2091
2138
|
// forward signals to its descendants, so the real server process is left
|
|
2092
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).
|
|
2093
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
|
+
};
|
|
2094
2162
|
const safeTreeKill = (signal) => {
|
|
2095
2163
|
try {
|
|
2096
2164
|
treeKill(pid, signal, (err) => {
|
|
@@ -2121,12 +2189,22 @@ function killStdioProcessTree(name, pid) {
|
|
|
2121
2189
|
});
|
|
2122
2190
|
}
|
|
2123
2191
|
};
|
|
2124
|
-
|
|
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');
|
|
2125
2203
|
setTimeout(() => {
|
|
2126
2204
|
if (!isProcessAlive(pid)) {
|
|
2127
2205
|
return;
|
|
2128
2206
|
}
|
|
2129
|
-
|
|
2207
|
+
safeKill('SIGKILL');
|
|
2130
2208
|
}, STDIO_KILL_GRACE_PERIOD_MS);
|
|
2131
2209
|
}
|
|
2132
2210
|
function isProcessAlive(pid) {
|
|
@@ -2465,7 +2543,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2465
2543
|
serverInfo.tools.some((tool) => tool.name === toolName));
|
|
2466
2544
|
}
|
|
2467
2545
|
if (!targetServerInfo) {
|
|
2468
|
-
throw new
|
|
2546
|
+
throw new ToolUnavailableError(`Tool not available: ${toolName}`, classifyUnavailableReason(toolName));
|
|
2469
2547
|
}
|
|
2470
2548
|
// If the target is an on-demand server that is not yet running, wake it up now.
|
|
2471
2549
|
// Concurrent callers are serialised via ensureServerReady's singleton promise.
|
|
@@ -2482,7 +2560,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2482
2560
|
// Check if the tool exists on the server
|
|
2483
2561
|
const tool = targetTool ?? findToolOnServer(targetServerInfo, targetToolName, singleServerAppsRoute);
|
|
2484
2562
|
if (!tool) {
|
|
2485
|
-
throw new
|
|
2563
|
+
throw new ToolUnavailableError(`Tool not available: ${toolName}`, 'tool-not-found');
|
|
2486
2564
|
}
|
|
2487
2565
|
assertToolAvailableForRoute(tool, appsRouteContext);
|
|
2488
2566
|
// Handle OpenAPI servers differently
|
|
@@ -2639,7 +2717,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2639
2717
|
const tool = groupTool?.tool ??
|
|
2640
2718
|
(serverInfo ? findToolOnServer(serverInfo, routeToolName, singleServerAppsRoute) : undefined);
|
|
2641
2719
|
if (!serverInfo || !tool) {
|
|
2642
|
-
throw new
|
|
2720
|
+
throw new ToolUnavailableError(`Tool not available: ${request.params.name}`, classifyUnavailableReason(request.params.name));
|
|
2643
2721
|
}
|
|
2644
2722
|
assertToolAvailableForRoute(tool, appsRouteContext);
|
|
2645
2723
|
// Wake the on-demand server before invoking the tool. It may be asleep with
|
|
@@ -2773,7 +2851,13 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2773
2851
|
});
|
|
2774
2852
|
}
|
|
2775
2853
|
catch (error) {
|
|
2776
|
-
|
|
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
|
+
});
|
|
2777
2861
|
// Log error activity
|
|
2778
2862
|
const duration = Date.now() - startTime;
|
|
2779
2863
|
await settleHostedIfNeeded({
|
|
@@ -2796,9 +2880,15 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
2796
2880
|
keyId,
|
|
2797
2881
|
keyName,
|
|
2798
2882
|
sourceIp,
|
|
2799
|
-
|
|
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),
|
|
2800
2888
|
});
|
|
2801
|
-
|
|
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);
|
|
2802
2892
|
return {
|
|
2803
2893
|
content: [
|
|
2804
2894
|
{
|
|
@@ -2854,7 +2944,7 @@ export const handleGetPromptRequest = async (request, extra) => {
|
|
|
2854
2944
|
serverInfo.prompts.find((prompt) => prompt.name === name));
|
|
2855
2945
|
}
|
|
2856
2946
|
if (!server) {
|
|
2857
|
-
throw new
|
|
2947
|
+
throw new ToolUnavailableError(`Prompt not available: ${name}`, classifyUnavailableReason(name));
|
|
2858
2948
|
}
|
|
2859
2949
|
// Remove server prefix from prompt name if present
|
|
2860
2950
|
const separator = getNameSeparator();
|
|
@@ -2879,8 +2969,15 @@ export const handleGetPromptRequest = async (request, extra) => {
|
|
|
2879
2969
|
return prompt;
|
|
2880
2970
|
}
|
|
2881
2971
|
catch (error) {
|
|
2882
|
-
|
|
2883
|
-
|
|
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);
|
|
2884
2981
|
return {
|
|
2885
2982
|
content: [
|
|
2886
2983
|
{
|