@samanhappy/mcphub 0.9.15 → 0.10.0
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 +67 -0
- package/README.zh.md +65 -0
- package/dist/config/index.js +17 -4
- package/dist/config/index.js.map +1 -1
- package/dist/controllers/authController.js +16 -0
- package/dist/controllers/authController.js.map +1 -1
- package/dist/controllers/oauthCallbackController.js +276 -0
- package/dist/controllers/oauthCallbackController.js.map +1 -0
- package/dist/controllers/serverController.js +1 -1
- package/dist/controllers/serverController.js.map +1 -1
- package/dist/controllers/userController.js +23 -1
- package/dist/controllers/userController.js.map +1 -1
- package/dist/routes/index.js +3 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/server.js +10 -0
- package/dist/server.js.map +1 -1
- package/dist/services/mcpOAuthProvider.js +472 -0
- package/dist/services/mcpOAuthProvider.js.map +1 -0
- package/dist/services/mcpService.js +328 -199
- package/dist/services/mcpService.js.map +1 -1
- package/dist/services/oauthClientRegistration.js +444 -0
- package/dist/services/oauthClientRegistration.js.map +1 -0
- package/dist/services/oauthService.js +216 -0
- package/dist/services/oauthService.js.map +1 -0
- package/dist/services/oauthSettingsStore.js +106 -0
- package/dist/services/oauthSettingsStore.js.map +1 -0
- package/dist/utils/passwordValidation.js +38 -0
- package/dist/utils/passwordValidation.js.map +1 -0
- package/dist/utils/path.js.map +1 -1
- package/frontend/dist/assets/index-BP5IZhlg.js +251 -0
- package/frontend/dist/assets/index-BP5IZhlg.js.map +1 -0
- package/frontend/dist/assets/index-C_58ZhSt.css +1 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +3 -2
- package/frontend/dist/assets/index-BLUhSkL4.js +0 -217
- package/frontend/dist/assets/index-BLUhSkL4.js.map +0 -1
- package/frontend/dist/assets/index-D8hgHrZ3.css +0 -1
|
@@ -4,7 +4,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema
|
|
|
4
4
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
5
5
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
6
6
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
7
|
-
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
7
|
+
import { StreamableHTTPClientTransport, } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
8
8
|
import { loadSettings, expandEnvVars, replaceEnvVars, getNameSeparator } from '../config/index.js';
|
|
9
9
|
import config from '../config/index.js';
|
|
10
10
|
import { getGroup } from './sseService.js';
|
|
@@ -14,6 +14,8 @@ import { OpenAPIClient } from '../clients/openapi.js';
|
|
|
14
14
|
import { RequestContextService } from './requestContextService.js';
|
|
15
15
|
import { getDataService } from './services.js';
|
|
16
16
|
import { getServerDao } from '../dao/index.js';
|
|
17
|
+
import { initializeAllOAuthClients } from './oauthService.js';
|
|
18
|
+
import { createOAuthProvider } from './mcpOAuthProvider.js';
|
|
17
19
|
const servers = {};
|
|
18
20
|
const serverDao = getServerDao();
|
|
19
21
|
// Helper function to set up keep-alive ping for SSE connections
|
|
@@ -43,6 +45,9 @@ const setupKeepAlive = (serverInfo, serverConfig) => {
|
|
|
43
45
|
console.log(`Keep-alive ping set up for server ${serverInfo.name} with interval ${interval / 1000} seconds`);
|
|
44
46
|
};
|
|
45
47
|
export const initUpstreamServers = async () => {
|
|
48
|
+
// Initialize OAuth clients for servers with dynamic registration
|
|
49
|
+
await initializeAllOAuthClients();
|
|
50
|
+
// Register all tools from upstream servers
|
|
46
51
|
await registerAllTools(true);
|
|
47
52
|
};
|
|
48
53
|
export const getMcpServer = (sessionId, group) => {
|
|
@@ -128,28 +133,42 @@ export const cleanupAllServers = () => {
|
|
|
128
133
|
});
|
|
129
134
|
};
|
|
130
135
|
// Helper function to create transport based on server configuration
|
|
131
|
-
const createTransportFromConfig = (name, conf) => {
|
|
136
|
+
export const createTransportFromConfig = async (name, conf) => {
|
|
132
137
|
let transport;
|
|
133
138
|
if (conf.type === 'streamable-http') {
|
|
134
139
|
const options = {};
|
|
135
|
-
|
|
140
|
+
const headers = conf.headers ? replaceEnvVars(conf.headers) : {};
|
|
141
|
+
if (Object.keys(headers).length > 0) {
|
|
136
142
|
options.requestInit = {
|
|
137
|
-
headers
|
|
143
|
+
headers,
|
|
138
144
|
};
|
|
139
145
|
}
|
|
146
|
+
// Create OAuth provider if configured - SDK will handle authentication automatically
|
|
147
|
+
const authProvider = await createOAuthProvider(name, conf);
|
|
148
|
+
if (authProvider) {
|
|
149
|
+
options.authProvider = authProvider;
|
|
150
|
+
console.log(`OAuth provider configured for server: ${name}`);
|
|
151
|
+
}
|
|
140
152
|
transport = new StreamableHTTPClientTransport(new URL(conf.url || ''), options);
|
|
141
153
|
}
|
|
142
154
|
else if (conf.url) {
|
|
143
155
|
// SSE transport
|
|
144
156
|
const options = {};
|
|
145
|
-
|
|
157
|
+
const headers = conf.headers ? replaceEnvVars(conf.headers) : {};
|
|
158
|
+
if (Object.keys(headers).length > 0) {
|
|
146
159
|
options.eventSourceInit = {
|
|
147
|
-
headers
|
|
160
|
+
headers,
|
|
148
161
|
};
|
|
149
162
|
options.requestInit = {
|
|
150
|
-
headers
|
|
163
|
+
headers,
|
|
151
164
|
};
|
|
152
165
|
}
|
|
166
|
+
// Create OAuth provider if configured - SDK will handle authentication automatically
|
|
167
|
+
const authProvider = await createOAuthProvider(name, conf);
|
|
168
|
+
if (authProvider) {
|
|
169
|
+
options.authProvider = authProvider;
|
|
170
|
+
console.log(`OAuth provider configured for server: ${name}`);
|
|
171
|
+
}
|
|
153
172
|
transport = new SSEClientTransport(new URL(conf.url), options);
|
|
154
173
|
}
|
|
155
174
|
else if (conf.command && conf.args) {
|
|
@@ -173,6 +192,7 @@ const createTransportFromConfig = (name, conf) => {
|
|
|
173
192
|
conf.command === 'node')) {
|
|
174
193
|
env['npm_config_registry'] = settings.systemConfig.install.npmRegistry;
|
|
175
194
|
}
|
|
195
|
+
// Expand environment variables in command
|
|
176
196
|
transport = new StdioClientTransport({
|
|
177
197
|
cwd: os.homedir(),
|
|
178
198
|
command: conf.command,
|
|
@@ -204,8 +224,11 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
|
|
|
204
224
|
const isHttp40xError = error?.message?.startsWith?.('Error POSTing to endpoint (HTTP 40');
|
|
205
225
|
// Only retry for StreamableHTTPClientTransport
|
|
206
226
|
const isStreamableHttp = serverInfo.transport instanceof StreamableHTTPClientTransport;
|
|
207
|
-
|
|
208
|
-
|
|
227
|
+
const isSSE = serverInfo.transport instanceof SSEClientTransport;
|
|
228
|
+
if (attempt < maxRetries &&
|
|
229
|
+
serverInfo.transport &&
|
|
230
|
+
((isStreamableHttp && isHttp40xError) || isSSE)) {
|
|
231
|
+
console.warn(`${isHttp40xError ? 'HTTP 40x error' : 'error'} detected for ${isStreamableHttp ? 'StreamableHTTP' : 'SSE'} server ${serverInfo.name}, attempting reconnection (attempt ${attempt + 1}/${maxRetries + 1})`);
|
|
209
232
|
try {
|
|
210
233
|
// Close existing connection
|
|
211
234
|
if (serverInfo.keepAliveIntervalId) {
|
|
@@ -219,7 +242,7 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
|
|
|
219
242
|
throw new Error(`Server configuration not found for: ${serverInfo.name}`);
|
|
220
243
|
}
|
|
221
244
|
// Recreate transport using helper function
|
|
222
|
-
const newTransport = createTransportFromConfig(serverInfo.name, server);
|
|
245
|
+
const newTransport = await createTransportFromConfig(serverInfo.name, server);
|
|
223
246
|
// Create new client
|
|
224
247
|
const client = new Client({
|
|
225
248
|
name: `mcp-client-${serverInfo.name}`,
|
|
@@ -279,194 +302,230 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
|
|
|
279
302
|
export const initializeClientsFromSettings = async (isInit, serverName) => {
|
|
280
303
|
const allServers = await serverDao.findAll();
|
|
281
304
|
const existingServerInfos = serverInfos;
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
error: null,
|
|
293
|
-
tools: [],
|
|
294
|
-
prompts: [],
|
|
295
|
-
createTime: Date.now(),
|
|
296
|
-
enabled: false,
|
|
297
|
-
});
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
// Check if server is already connected
|
|
301
|
-
const existingServer = existingServerInfos.find((s) => s.name === name && s.status === 'connected');
|
|
302
|
-
if (existingServer && (!serverName || serverName !== name)) {
|
|
303
|
-
serverInfos.push({
|
|
304
|
-
...existingServer,
|
|
305
|
-
enabled: conf.enabled === undefined ? true : conf.enabled,
|
|
306
|
-
});
|
|
307
|
-
console.log(`Server '${name}' is already connected.`);
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
let transport;
|
|
311
|
-
let openApiClient;
|
|
312
|
-
if (conf.type === 'openapi') {
|
|
313
|
-
// Handle OpenAPI type servers
|
|
314
|
-
if (!conf.openapi?.url && !conf.openapi?.schema) {
|
|
315
|
-
console.warn(`Skipping OpenAPI server '${name}': missing OpenAPI specification URL or schema`);
|
|
316
|
-
serverInfos.push({
|
|
305
|
+
const nextServerInfos = [];
|
|
306
|
+
try {
|
|
307
|
+
for (const conf of allServers) {
|
|
308
|
+
const { name } = conf;
|
|
309
|
+
// Expand environment variables in all configuration values
|
|
310
|
+
const expandedConf = replaceEnvVars(conf);
|
|
311
|
+
// Skip disabled servers
|
|
312
|
+
if (expandedConf.enabled === false) {
|
|
313
|
+
console.log(`Skipping disabled server: ${name}`);
|
|
314
|
+
nextServerInfos.push({
|
|
317
315
|
name,
|
|
318
|
-
owner:
|
|
316
|
+
owner: expandedConf.owner,
|
|
319
317
|
status: 'disconnected',
|
|
320
|
-
error:
|
|
318
|
+
error: null,
|
|
321
319
|
tools: [],
|
|
322
320
|
prompts: [],
|
|
323
321
|
createTime: Date.now(),
|
|
322
|
+
enabled: false,
|
|
323
|
+
});
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
// Check if server is already connected
|
|
327
|
+
const existingServer = existingServerInfos.find((s) => s.name === name && s.status === 'connected');
|
|
328
|
+
if (existingServer && (!serverName || serverName !== name)) {
|
|
329
|
+
nextServerInfos.push({
|
|
330
|
+
...existingServer,
|
|
331
|
+
enabled: expandedConf.enabled === undefined ? true : expandedConf.enabled,
|
|
324
332
|
});
|
|
333
|
+
console.log(`Server '${name}' is already connected.`);
|
|
325
334
|
continue;
|
|
326
335
|
}
|
|
336
|
+
let transport;
|
|
337
|
+
let openApiClient;
|
|
338
|
+
if (expandedConf.type === 'openapi') {
|
|
339
|
+
// Handle OpenAPI type servers
|
|
340
|
+
if (!expandedConf.openapi?.url && !expandedConf.openapi?.schema) {
|
|
341
|
+
console.warn(`Skipping OpenAPI server '${name}': missing OpenAPI specification URL or schema`);
|
|
342
|
+
nextServerInfos.push({
|
|
343
|
+
name,
|
|
344
|
+
owner: expandedConf.owner,
|
|
345
|
+
status: 'disconnected',
|
|
346
|
+
error: 'Missing OpenAPI specification URL or schema',
|
|
347
|
+
tools: [],
|
|
348
|
+
prompts: [],
|
|
349
|
+
createTime: Date.now(),
|
|
350
|
+
});
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
// Create server info first and keep reference to it
|
|
354
|
+
const serverInfo = {
|
|
355
|
+
name,
|
|
356
|
+
owner: expandedConf.owner,
|
|
357
|
+
status: 'connecting',
|
|
358
|
+
error: null,
|
|
359
|
+
tools: [],
|
|
360
|
+
prompts: [],
|
|
361
|
+
createTime: Date.now(),
|
|
362
|
+
enabled: expandedConf.enabled === undefined ? true : expandedConf.enabled,
|
|
363
|
+
config: expandedConf, // Store reference to expanded config for OpenAPI passthrough headers
|
|
364
|
+
};
|
|
365
|
+
nextServerInfos.push(serverInfo);
|
|
366
|
+
try {
|
|
367
|
+
// Create OpenAPI client instance
|
|
368
|
+
openApiClient = new OpenAPIClient(expandedConf);
|
|
369
|
+
console.log(`Initializing OpenAPI server: ${name}...`);
|
|
370
|
+
// Perform async initialization
|
|
371
|
+
await openApiClient.initialize();
|
|
372
|
+
// Convert OpenAPI tools to MCP tool format
|
|
373
|
+
const openApiTools = openApiClient.getTools();
|
|
374
|
+
const mcpTools = openApiTools.map((tool) => ({
|
|
375
|
+
name: `${name}${getNameSeparator()}${tool.name}`,
|
|
376
|
+
description: tool.description,
|
|
377
|
+
inputSchema: cleanInputSchema(tool.inputSchema),
|
|
378
|
+
}));
|
|
379
|
+
// Update server info with successful initialization
|
|
380
|
+
serverInfo.status = 'connected';
|
|
381
|
+
serverInfo.tools = mcpTools;
|
|
382
|
+
serverInfo.openApiClient = openApiClient;
|
|
383
|
+
console.log(`Successfully initialized OpenAPI server: ${name} with ${mcpTools.length} tools`);
|
|
384
|
+
// Save tools as vector embeddings for search
|
|
385
|
+
saveToolsAsVectorEmbeddings(name, mcpTools);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
console.error(`Failed to initialize OpenAPI server ${name}:`, error);
|
|
390
|
+
// Update the already pushed server info with error status
|
|
391
|
+
serverInfo.status = 'disconnected';
|
|
392
|
+
serverInfo.error = `Failed to initialize OpenAPI server: ${error}`;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
transport = await createTransportFromConfig(name, expandedConf);
|
|
398
|
+
}
|
|
399
|
+
const client = new Client({
|
|
400
|
+
name: `mcp-client-${name}`,
|
|
401
|
+
version: '1.0.0',
|
|
402
|
+
}, {
|
|
403
|
+
capabilities: {
|
|
404
|
+
prompts: {},
|
|
405
|
+
resources: {},
|
|
406
|
+
tools: {},
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
const initRequestOptions = isInit
|
|
410
|
+
? {
|
|
411
|
+
timeout: Number(config.initTimeout) || 60000,
|
|
412
|
+
}
|
|
413
|
+
: undefined;
|
|
414
|
+
// Get request options from server configuration, with fallbacks
|
|
415
|
+
const serverRequestOptions = expandedConf.options || {};
|
|
416
|
+
const requestOptions = {
|
|
417
|
+
timeout: serverRequestOptions.timeout || 60000,
|
|
418
|
+
resetTimeoutOnProgress: serverRequestOptions.resetTimeoutOnProgress || false,
|
|
419
|
+
maxTotalTimeout: serverRequestOptions.maxTotalTimeout,
|
|
420
|
+
};
|
|
327
421
|
// Create server info first and keep reference to it
|
|
328
422
|
const serverInfo = {
|
|
329
423
|
name,
|
|
330
|
-
owner:
|
|
424
|
+
owner: expandedConf.owner,
|
|
331
425
|
status: 'connecting',
|
|
332
426
|
error: null,
|
|
333
427
|
tools: [],
|
|
334
428
|
prompts: [],
|
|
429
|
+
client,
|
|
430
|
+
transport,
|
|
431
|
+
options: requestOptions,
|
|
335
432
|
createTime: Date.now(),
|
|
336
|
-
|
|
337
|
-
config: conf, // Store reference to original config for OpenAPI passthrough headers
|
|
433
|
+
config: expandedConf, // Store reference to expanded config
|
|
338
434
|
};
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
openApiClient = new OpenAPIClient(conf);
|
|
343
|
-
console.log(`Initializing OpenAPI server: ${name}...`);
|
|
344
|
-
// Perform async initialization
|
|
345
|
-
await openApiClient.initialize();
|
|
346
|
-
// Convert OpenAPI tools to MCP tool format
|
|
347
|
-
const openApiTools = openApiClient.getTools();
|
|
348
|
-
const mcpTools = openApiTools.map((tool) => ({
|
|
349
|
-
name: `${name}${getNameSeparator()}${tool.name}`,
|
|
350
|
-
description: tool.description,
|
|
351
|
-
inputSchema: cleanInputSchema(tool.inputSchema),
|
|
352
|
-
}));
|
|
353
|
-
// Update server info with successful initialization
|
|
354
|
-
serverInfo.status = 'connected';
|
|
355
|
-
serverInfo.tools = mcpTools;
|
|
356
|
-
serverInfo.openApiClient = openApiClient;
|
|
357
|
-
console.log(`Successfully initialized OpenAPI server: ${name} with ${mcpTools.length} tools`);
|
|
358
|
-
// Save tools as vector embeddings for search
|
|
359
|
-
saveToolsAsVectorEmbeddings(name, mcpTools);
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
catch (error) {
|
|
363
|
-
console.error(`Failed to initialize OpenAPI server ${name}:`, error);
|
|
364
|
-
// Update the already pushed server info with error status
|
|
365
|
-
serverInfo.status = 'disconnected';
|
|
366
|
-
serverInfo.error = `Failed to initialize OpenAPI server: ${error}`;
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
else {
|
|
371
|
-
transport = createTransportFromConfig(name, conf);
|
|
372
|
-
}
|
|
373
|
-
const client = new Client({
|
|
374
|
-
name: `mcp-client-${name}`,
|
|
375
|
-
version: '1.0.0',
|
|
376
|
-
}, {
|
|
377
|
-
capabilities: {
|
|
378
|
-
prompts: {},
|
|
379
|
-
resources: {},
|
|
380
|
-
tools: {},
|
|
381
|
-
},
|
|
382
|
-
});
|
|
383
|
-
const initRequestOptions = isInit
|
|
384
|
-
? {
|
|
385
|
-
timeout: Number(config.initTimeout) || 60000,
|
|
386
|
-
}
|
|
387
|
-
: undefined;
|
|
388
|
-
// Get request options from server configuration, with fallbacks
|
|
389
|
-
const serverRequestOptions = conf.options || {};
|
|
390
|
-
const requestOptions = {
|
|
391
|
-
timeout: serverRequestOptions.timeout || 60000,
|
|
392
|
-
resetTimeoutOnProgress: serverRequestOptions.resetTimeoutOnProgress || false,
|
|
393
|
-
maxTotalTimeout: serverRequestOptions.maxTotalTimeout,
|
|
394
|
-
};
|
|
395
|
-
// Create server info first and keep reference to it
|
|
396
|
-
const serverInfo = {
|
|
397
|
-
name,
|
|
398
|
-
owner: conf.owner,
|
|
399
|
-
status: 'connecting',
|
|
400
|
-
error: null,
|
|
401
|
-
tools: [],
|
|
402
|
-
prompts: [],
|
|
403
|
-
client,
|
|
404
|
-
transport,
|
|
405
|
-
options: requestOptions,
|
|
406
|
-
createTime: Date.now(),
|
|
407
|
-
config: conf, // Store reference to original config
|
|
408
|
-
};
|
|
409
|
-
serverInfos.push(serverInfo);
|
|
410
|
-
client
|
|
411
|
-
.connect(transport, initRequestOptions || requestOptions)
|
|
412
|
-
.then(() => {
|
|
413
|
-
console.log(`Successfully connected client for server: ${name}`);
|
|
414
|
-
const capabilities = client.getServerCapabilities();
|
|
415
|
-
console.log(`Server capabilities: ${JSON.stringify(capabilities)}`);
|
|
416
|
-
let dataError = null;
|
|
417
|
-
if (capabilities?.tools) {
|
|
418
|
-
client
|
|
419
|
-
.listTools({}, initRequestOptions || requestOptions)
|
|
420
|
-
.then((tools) => {
|
|
421
|
-
console.log(`Successfully listed ${tools.tools.length} tools for server: ${name}`);
|
|
422
|
-
serverInfo.tools = tools.tools.map((tool) => ({
|
|
423
|
-
name: `${name}${getNameSeparator()}${tool.name}`,
|
|
424
|
-
description: tool.description || '',
|
|
425
|
-
inputSchema: cleanInputSchema(tool.inputSchema || {}),
|
|
426
|
-
}));
|
|
427
|
-
// Save tools as vector embeddings for search
|
|
428
|
-
saveToolsAsVectorEmbeddings(name, serverInfo.tools);
|
|
429
|
-
})
|
|
430
|
-
.catch((error) => {
|
|
431
|
-
console.error(`Failed to list tools for server ${name} by error: ${error} with stack: ${error.stack}`);
|
|
432
|
-
dataError = error;
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
if (capabilities?.prompts) {
|
|
436
|
-
client
|
|
437
|
-
.listPrompts({}, initRequestOptions || requestOptions)
|
|
438
|
-
.then((prompts) => {
|
|
439
|
-
console.log(`Successfully listed ${prompts.prompts.length} prompts for server: ${name}`);
|
|
440
|
-
serverInfo.prompts = prompts.prompts.map((prompt) => ({
|
|
441
|
-
name: `${name}${getNameSeparator()}${prompt.name}`,
|
|
442
|
-
title: prompt.title,
|
|
443
|
-
description: prompt.description,
|
|
444
|
-
arguments: prompt.arguments,
|
|
445
|
-
}));
|
|
446
|
-
})
|
|
447
|
-
.catch((error) => {
|
|
448
|
-
console.error(`Failed to list prompts for server ${name} by error: ${error} with stack: ${error.stack}`);
|
|
449
|
-
dataError = error;
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
if (!dataError) {
|
|
453
|
-
serverInfo.status = 'connected';
|
|
435
|
+
const pendingAuth = expandedConf.oauth?.pendingAuthorization;
|
|
436
|
+
if (pendingAuth) {
|
|
437
|
+
serverInfo.status = 'oauth_required';
|
|
454
438
|
serverInfo.error = null;
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
serverInfo.error = `Failed to list data: ${dataError} `;
|
|
439
|
+
serverInfo.oauth = {
|
|
440
|
+
authorizationUrl: pendingAuth.authorizationUrl,
|
|
441
|
+
state: pendingAuth.state,
|
|
442
|
+
codeVerifier: pendingAuth.codeVerifier,
|
|
443
|
+
};
|
|
461
444
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
445
|
+
nextServerInfos.push(serverInfo);
|
|
446
|
+
client
|
|
447
|
+
.connect(transport, initRequestOptions || requestOptions)
|
|
448
|
+
.then(() => {
|
|
449
|
+
console.log(`Successfully connected client for server: ${name}`);
|
|
450
|
+
const capabilities = client.getServerCapabilities();
|
|
451
|
+
console.log(`Server capabilities: ${JSON.stringify(capabilities)}`);
|
|
452
|
+
let dataError = null;
|
|
453
|
+
if (capabilities?.tools) {
|
|
454
|
+
client
|
|
455
|
+
.listTools({}, initRequestOptions || requestOptions)
|
|
456
|
+
.then((tools) => {
|
|
457
|
+
console.log(`Successfully listed ${tools.tools.length} tools for server: ${name}`);
|
|
458
|
+
serverInfo.tools = tools.tools.map((tool) => ({
|
|
459
|
+
name: `${name}${getNameSeparator()}${tool.name}`,
|
|
460
|
+
description: tool.description || '',
|
|
461
|
+
inputSchema: cleanInputSchema(tool.inputSchema || {}),
|
|
462
|
+
}));
|
|
463
|
+
// Save tools as vector embeddings for search
|
|
464
|
+
saveToolsAsVectorEmbeddings(name, serverInfo.tools);
|
|
465
|
+
})
|
|
466
|
+
.catch((error) => {
|
|
467
|
+
console.error(`Failed to list tools for server ${name} by error: ${error} with stack: ${error.stack}`);
|
|
468
|
+
dataError = error;
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
if (capabilities?.prompts) {
|
|
472
|
+
client
|
|
473
|
+
.listPrompts({}, initRequestOptions || requestOptions)
|
|
474
|
+
.then((prompts) => {
|
|
475
|
+
console.log(`Successfully listed ${prompts.prompts.length} prompts for server: ${name}`);
|
|
476
|
+
serverInfo.prompts = prompts.prompts.map((prompt) => ({
|
|
477
|
+
name: `${name}${getNameSeparator()}${prompt.name}`,
|
|
478
|
+
title: prompt.title,
|
|
479
|
+
description: prompt.description,
|
|
480
|
+
arguments: prompt.arguments,
|
|
481
|
+
}));
|
|
482
|
+
})
|
|
483
|
+
.catch((error) => {
|
|
484
|
+
console.error(`Failed to list prompts for server ${name} by error: ${error} with stack: ${error.stack}`);
|
|
485
|
+
dataError = error;
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
if (!dataError) {
|
|
489
|
+
serverInfo.status = 'connected';
|
|
490
|
+
serverInfo.error = null;
|
|
491
|
+
// Set up keep-alive ping for SSE connections
|
|
492
|
+
setupKeepAlive(serverInfo, expandedConf);
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
serverInfo.status = 'disconnected';
|
|
496
|
+
serverInfo.error = `Failed to list data: ${dataError} `;
|
|
497
|
+
}
|
|
498
|
+
})
|
|
499
|
+
.catch(async (error) => {
|
|
500
|
+
// Check if this is an OAuth authorization error
|
|
501
|
+
const isOAuthError = error?.message?.includes('OAuth authorization required') ||
|
|
502
|
+
error?.message?.includes('Authorization required');
|
|
503
|
+
if (isOAuthError) {
|
|
504
|
+
// OAuth provider should have already set the status to 'oauth_required'
|
|
505
|
+
// and stored the authorization URL in serverInfo.oauth
|
|
506
|
+
console.log(`OAuth authorization required for server ${name}. Status should be set to 'oauth_required'.`);
|
|
507
|
+
// Make sure status is set correctly
|
|
508
|
+
if (serverInfo.status !== 'oauth_required') {
|
|
509
|
+
serverInfo.status = 'oauth_required';
|
|
510
|
+
}
|
|
511
|
+
serverInfo.error = null;
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
console.error(`Failed to connect client for server ${name} by error: ${error} with stack: ${error.stack}`);
|
|
515
|
+
// Other connection errors
|
|
516
|
+
serverInfo.status = 'disconnected';
|
|
517
|
+
serverInfo.error = `Failed to connect: ${error.stack} `;
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
console.log(`Initialized client for server: ${name}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
// Restore previous state if initialization fails to avoid exposing an empty server list
|
|
525
|
+
serverInfos = existingServerInfos;
|
|
526
|
+
throw error;
|
|
469
527
|
}
|
|
528
|
+
serverInfos = nextServerInfos;
|
|
470
529
|
return serverInfos;
|
|
471
530
|
};
|
|
472
531
|
// Register all MCP tools
|
|
@@ -480,7 +539,7 @@ export const getServersInfo = async () => {
|
|
|
480
539
|
const filterServerInfos = dataService.filterData
|
|
481
540
|
? dataService.filterData(serverInfos)
|
|
482
541
|
: serverInfos;
|
|
483
|
-
const infos = filterServerInfos.map(({ name, status, tools, prompts, createTime, error }) => {
|
|
542
|
+
const infos = filterServerInfos.map(({ name, status, tools, prompts, createTime, error, oauth }) => {
|
|
484
543
|
const serverConfig = allServers.find((server) => server.name === name);
|
|
485
544
|
const enabled = serverConfig ? serverConfig.enabled !== false : true;
|
|
486
545
|
// Add enabled status and custom description to each tool
|
|
@@ -508,6 +567,13 @@ export const getServersInfo = async () => {
|
|
|
508
567
|
prompts: promptsWithEnabled,
|
|
509
568
|
createTime,
|
|
510
569
|
enabled,
|
|
570
|
+
oauth: oauth
|
|
571
|
+
? {
|
|
572
|
+
authorizationUrl: oauth.authorizationUrl,
|
|
573
|
+
state: oauth.state,
|
|
574
|
+
// Don't expose codeVerifier to frontend for security
|
|
575
|
+
}
|
|
576
|
+
: undefined,
|
|
511
577
|
};
|
|
512
578
|
});
|
|
513
579
|
infos.sort((a, b) => {
|
|
@@ -521,6 +587,45 @@ export const getServersInfo = async () => {
|
|
|
521
587
|
export const getServerByName = (name) => {
|
|
522
588
|
return serverInfos.find((serverInfo) => serverInfo.name === name);
|
|
523
589
|
};
|
|
590
|
+
// Get server by OAuth state parameter
|
|
591
|
+
export const getServerByOAuthState = (state) => {
|
|
592
|
+
return serverInfos.find((serverInfo) => serverInfo.oauth?.state === state);
|
|
593
|
+
};
|
|
594
|
+
/**
|
|
595
|
+
* Reconnect a server after OAuth authorization or configuration change
|
|
596
|
+
* This will close the existing connection and reinitialize the server
|
|
597
|
+
*/
|
|
598
|
+
export const reconnectServer = async (serverName) => {
|
|
599
|
+
console.log(`Reconnecting server: ${serverName}`);
|
|
600
|
+
const serverInfo = getServerByName(serverName);
|
|
601
|
+
if (!serverInfo) {
|
|
602
|
+
throw new Error(`Server not found: ${serverName}`);
|
|
603
|
+
}
|
|
604
|
+
// Close existing connection if any
|
|
605
|
+
if (serverInfo.client) {
|
|
606
|
+
try {
|
|
607
|
+
serverInfo.client.close();
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
console.warn(`Error closing client for server ${serverName}:`, error);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (serverInfo.transport) {
|
|
614
|
+
try {
|
|
615
|
+
serverInfo.transport.close();
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
console.warn(`Error closing transport for server ${serverName}:`, error);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (serverInfo.keepAliveIntervalId) {
|
|
622
|
+
clearInterval(serverInfo.keepAliveIntervalId);
|
|
623
|
+
serverInfo.keepAliveIntervalId = undefined;
|
|
624
|
+
}
|
|
625
|
+
// Reinitialize the server
|
|
626
|
+
await initializeClientsFromSettings(false, serverName);
|
|
627
|
+
console.log(`Successfully reconnected server: ${serverName}`);
|
|
628
|
+
};
|
|
524
629
|
// Filter tools by server configuration
|
|
525
630
|
const filterToolsByConfig = async (serverName, tools) => {
|
|
526
631
|
const serverConfig = await serverDao.findById(serverName);
|
|
@@ -632,28 +737,39 @@ export const handleListToolsRequest = async (_, extra) => {
|
|
|
632
737
|
const group = getGroup(sessionId);
|
|
633
738
|
console.log(`Handling ListToolsRequest for group: ${group}`);
|
|
634
739
|
// Special handling for $smart group to return special tools
|
|
635
|
-
|
|
740
|
+
// Support both $smart and $smart/{group} patterns
|
|
741
|
+
if (group === '$smart' || group?.startsWith('$smart/')) {
|
|
742
|
+
// Extract target group if pattern is $smart/{group}
|
|
743
|
+
const targetGroup = group?.startsWith('$smart/') ? group.substring(7) : undefined;
|
|
744
|
+
// Get info about available servers, filtered by target group if specified
|
|
745
|
+
let availableServers = serverInfos.filter((server) => server.status === 'connected' && server.enabled !== false);
|
|
746
|
+
// If a target group is specified, filter servers to only those in the group
|
|
747
|
+
if (targetGroup) {
|
|
748
|
+
const serversInGroup = getServersInGroup(targetGroup);
|
|
749
|
+
if (serversInGroup && serversInGroup.length > 0) {
|
|
750
|
+
availableServers = availableServers.filter((server) => serversInGroup.includes(server.name));
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
// Create simple server information with only server names
|
|
754
|
+
const serversList = availableServers
|
|
755
|
+
.map((server) => {
|
|
756
|
+
return `${server.name}`;
|
|
757
|
+
})
|
|
758
|
+
.join(', ');
|
|
759
|
+
const scopeDescription = targetGroup
|
|
760
|
+
? `servers in the "${targetGroup}" group`
|
|
761
|
+
: 'all available servers';
|
|
636
762
|
return {
|
|
637
763
|
tools: [
|
|
638
764
|
{
|
|
639
765
|
name: 'search_tools',
|
|
640
|
-
description:
|
|
641
|
-
// Get info about available servers
|
|
642
|
-
const availableServers = serverInfos.filter((server) => server.status === 'connected' && server.enabled !== false);
|
|
643
|
-
// Create simple server information with only server names
|
|
644
|
-
const serversList = availableServers
|
|
645
|
-
.map((server) => {
|
|
646
|
-
return `${server.name}`;
|
|
647
|
-
})
|
|
648
|
-
.join(', ');
|
|
649
|
-
return `STEP 1 of 2: Use this tool FIRST to discover and search for relevant tools across all available servers. This tool and call_tool work together as a two-step process: 1) search_tools to find what you need, 2) call_tool to execute it.
|
|
766
|
+
description: `STEP 1 of 2: Use this tool FIRST to discover and search for relevant tools across ${scopeDescription}. This tool and call_tool work together as a two-step process: 1) search_tools to find what you need, 2) call_tool to execute it.
|
|
650
767
|
|
|
651
768
|
For optimal results, use specific queries matching your exact needs. Call this tool multiple times with different queries for different parts of complex tasks. Example queries: "image generation tools", "code review tools", "data analysis", "translation capabilities", etc. Results are sorted by relevance using vector similarity.
|
|
652
769
|
|
|
653
770
|
After finding relevant tools, you MUST use the call_tool to actually execute them. The search_tools only finds tools - it doesn't execute them.
|
|
654
771
|
|
|
655
|
-
Available servers: ${serversList}
|
|
656
|
-
})(),
|
|
772
|
+
Available servers: ${serversList}`,
|
|
657
773
|
inputSchema: {
|
|
658
774
|
type: 'object',
|
|
659
775
|
properties: {
|
|
@@ -754,7 +870,24 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
754
870
|
thresholdNum = 0.4;
|
|
755
871
|
}
|
|
756
872
|
console.log(`Using similarity threshold: ${thresholdNum} for query: "${query}"`);
|
|
757
|
-
|
|
873
|
+
// Determine server filtering based on group
|
|
874
|
+
const sessionId = extra.sessionId || '';
|
|
875
|
+
const group = getGroup(sessionId);
|
|
876
|
+
let servers = undefined; // No server filtering by default
|
|
877
|
+
// If group is in format $smart/{group}, filter servers to that group
|
|
878
|
+
if (group?.startsWith('$smart/')) {
|
|
879
|
+
const targetGroup = group.substring(7);
|
|
880
|
+
const serversInGroup = getServersInGroup(targetGroup);
|
|
881
|
+
if (serversInGroup !== undefined && serversInGroup !== null) {
|
|
882
|
+
servers = serversInGroup;
|
|
883
|
+
if (servers.length > 0) {
|
|
884
|
+
console.log(`Filtering search to servers in group "${targetGroup}": ${servers.join(', ')}`);
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
console.log(`Group "${targetGroup}" has no servers, search will return no results`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
758
891
|
const searchResults = await searchToolsByVector(query, limitNum, thresholdNum, servers);
|
|
759
892
|
console.log(`Search results: ${JSON.stringify(searchResults)}`);
|
|
760
893
|
// Find actual tool information from serverInfos by serverName and toolName
|
|
@@ -911,9 +1044,7 @@ export const handleCallToolRequest = async (request, extra) => {
|
|
|
911
1044
|
console.log(`Invoking tool '${toolName}' on server '${targetServerInfo.name}' with arguments: ${JSON.stringify(finalArgs)}`);
|
|
912
1045
|
const separator = getNameSeparator();
|
|
913
1046
|
const prefix = `${targetServerInfo.name}${separator}`;
|
|
914
|
-
toolName = toolName.startsWith(prefix)
|
|
915
|
-
? toolName.substring(prefix.length)
|
|
916
|
-
: toolName;
|
|
1047
|
+
toolName = toolName.startsWith(prefix) ? toolName.substring(prefix.length) : toolName;
|
|
917
1048
|
const result = await callToolWithReconnect(targetServerInfo, {
|
|
918
1049
|
name: toolName,
|
|
919
1050
|
arguments: finalArgs,
|
|
@@ -1018,9 +1149,7 @@ export const handleGetPromptRequest = async (request, extra) => {
|
|
|
1018
1149
|
// Remove server prefix from prompt name if present
|
|
1019
1150
|
const separator = getNameSeparator();
|
|
1020
1151
|
const prefix = `${server.name}${separator}`;
|
|
1021
|
-
const cleanPromptName = name.startsWith(prefix)
|
|
1022
|
-
? name.substring(prefix.length)
|
|
1023
|
-
: name;
|
|
1152
|
+
const cleanPromptName = name.startsWith(prefix) ? name.substring(prefix.length) : name;
|
|
1024
1153
|
const promptParams = {
|
|
1025
1154
|
name: cleanPromptName || '',
|
|
1026
1155
|
arguments: promptArgs,
|