@samanhappy/mcphub 0.9.16 → 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.
Files changed (37) hide show
  1. package/README.md +67 -0
  2. package/README.zh.md +65 -0
  3. package/dist/config/index.js +17 -4
  4. package/dist/config/index.js.map +1 -1
  5. package/dist/controllers/authController.js +16 -0
  6. package/dist/controllers/authController.js.map +1 -1
  7. package/dist/controllers/oauthCallbackController.js +276 -0
  8. package/dist/controllers/oauthCallbackController.js.map +1 -0
  9. package/dist/controllers/serverController.js +1 -1
  10. package/dist/controllers/serverController.js.map +1 -1
  11. package/dist/controllers/userController.js +23 -1
  12. package/dist/controllers/userController.js.map +1 -1
  13. package/dist/routes/index.js +3 -0
  14. package/dist/routes/index.js.map +1 -1
  15. package/dist/server.js +10 -0
  16. package/dist/server.js.map +1 -1
  17. package/dist/services/mcpOAuthProvider.js +472 -0
  18. package/dist/services/mcpOAuthProvider.js.map +1 -0
  19. package/dist/services/mcpService.js +321 -191
  20. package/dist/services/mcpService.js.map +1 -1
  21. package/dist/services/oauthClientRegistration.js +444 -0
  22. package/dist/services/oauthClientRegistration.js.map +1 -0
  23. package/dist/services/oauthService.js +216 -0
  24. package/dist/services/oauthService.js.map +1 -0
  25. package/dist/services/oauthSettingsStore.js +106 -0
  26. package/dist/services/oauthSettingsStore.js.map +1 -0
  27. package/dist/utils/passwordValidation.js +38 -0
  28. package/dist/utils/passwordValidation.js.map +1 -0
  29. package/dist/utils/path.js.map +1 -1
  30. package/frontend/dist/assets/index-BP5IZhlg.js +251 -0
  31. package/frontend/dist/assets/index-BP5IZhlg.js.map +1 -0
  32. package/frontend/dist/assets/index-C_58ZhSt.css +1 -0
  33. package/frontend/dist/index.html +2 -2
  34. package/package.json +3 -2
  35. package/frontend/dist/assets/index-DDUK8zl_.js +0 -217
  36. package/frontend/dist/assets/index-DDUK8zl_.js.map +0 -1
  37. package/frontend/dist/assets/index-DcVhHcn9.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
- if (conf.headers && Object.keys(conf.headers).length > 0) {
140
+ const headers = conf.headers ? replaceEnvVars(conf.headers) : {};
141
+ if (Object.keys(headers).length > 0) {
136
142
  options.requestInit = {
137
- headers: conf.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
- if (conf.headers && Object.keys(conf.headers).length > 0) {
157
+ const headers = conf.headers ? replaceEnvVars(conf.headers) : {};
158
+ if (Object.keys(headers).length > 0) {
146
159
  options.eventSourceInit = {
147
- headers: conf.headers,
160
+ headers,
148
161
  };
149
162
  options.requestInit = {
150
- headers: conf.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,
@@ -222,7 +242,7 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
222
242
  throw new Error(`Server configuration not found for: ${serverInfo.name}`);
223
243
  }
224
244
  // Recreate transport using helper function
225
- const newTransport = createTransportFromConfig(serverInfo.name, server);
245
+ const newTransport = await createTransportFromConfig(serverInfo.name, server);
226
246
  // Create new client
227
247
  const client = new Client({
228
248
  name: `mcp-client-${serverInfo.name}`,
@@ -282,194 +302,230 @@ const callToolWithReconnect = async (serverInfo, toolParams, options, maxRetries
282
302
  export const initializeClientsFromSettings = async (isInit, serverName) => {
283
303
  const allServers = await serverDao.findAll();
284
304
  const existingServerInfos = serverInfos;
285
- serverInfos = [];
286
- for (const conf of allServers) {
287
- const { name } = conf;
288
- // Skip disabled servers
289
- if (conf.enabled === false) {
290
- console.log(`Skipping disabled server: ${name}`);
291
- serverInfos.push({
292
- name,
293
- owner: conf.owner,
294
- status: 'disconnected',
295
- error: null,
296
- tools: [],
297
- prompts: [],
298
- createTime: Date.now(),
299
- enabled: false,
300
- });
301
- continue;
302
- }
303
- // Check if server is already connected
304
- const existingServer = existingServerInfos.find((s) => s.name === name && s.status === 'connected');
305
- if (existingServer && (!serverName || serverName !== name)) {
306
- serverInfos.push({
307
- ...existingServer,
308
- enabled: conf.enabled === undefined ? true : conf.enabled,
309
- });
310
- console.log(`Server '${name}' is already connected.`);
311
- continue;
312
- }
313
- let transport;
314
- let openApiClient;
315
- if (conf.type === 'openapi') {
316
- // Handle OpenAPI type servers
317
- if (!conf.openapi?.url && !conf.openapi?.schema) {
318
- console.warn(`Skipping OpenAPI server '${name}': missing OpenAPI specification URL or schema`);
319
- 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({
320
315
  name,
321
- owner: conf.owner,
316
+ owner: expandedConf.owner,
322
317
  status: 'disconnected',
323
- error: 'Missing OpenAPI specification URL or schema',
318
+ error: null,
324
319
  tools: [],
325
320
  prompts: [],
326
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,
327
332
  });
333
+ console.log(`Server '${name}' is already connected.`);
328
334
  continue;
329
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
+ };
330
421
  // Create server info first and keep reference to it
331
422
  const serverInfo = {
332
423
  name,
333
- owner: conf.owner,
424
+ owner: expandedConf.owner,
334
425
  status: 'connecting',
335
426
  error: null,
336
427
  tools: [],
337
428
  prompts: [],
429
+ client,
430
+ transport,
431
+ options: requestOptions,
338
432
  createTime: Date.now(),
339
- enabled: conf.enabled === undefined ? true : conf.enabled,
340
- config: conf, // Store reference to original config for OpenAPI passthrough headers
433
+ config: expandedConf, // Store reference to expanded config
341
434
  };
342
- serverInfos.push(serverInfo);
343
- try {
344
- // Create OpenAPI client instance
345
- openApiClient = new OpenAPIClient(conf);
346
- console.log(`Initializing OpenAPI server: ${name}...`);
347
- // Perform async initialization
348
- await openApiClient.initialize();
349
- // Convert OpenAPI tools to MCP tool format
350
- const openApiTools = openApiClient.getTools();
351
- const mcpTools = openApiTools.map((tool) => ({
352
- name: `${name}${getNameSeparator()}${tool.name}`,
353
- description: tool.description,
354
- inputSchema: cleanInputSchema(tool.inputSchema),
355
- }));
356
- // Update server info with successful initialization
357
- serverInfo.status = 'connected';
358
- serverInfo.tools = mcpTools;
359
- serverInfo.openApiClient = openApiClient;
360
- console.log(`Successfully initialized OpenAPI server: ${name} with ${mcpTools.length} tools`);
361
- // Save tools as vector embeddings for search
362
- saveToolsAsVectorEmbeddings(name, mcpTools);
363
- continue;
364
- }
365
- catch (error) {
366
- console.error(`Failed to initialize OpenAPI server ${name}:`, error);
367
- // Update the already pushed server info with error status
368
- serverInfo.status = 'disconnected';
369
- serverInfo.error = `Failed to initialize OpenAPI server: ${error}`;
370
- continue;
371
- }
372
- }
373
- else {
374
- transport = createTransportFromConfig(name, conf);
375
- }
376
- const client = new Client({
377
- name: `mcp-client-${name}`,
378
- version: '1.0.0',
379
- }, {
380
- capabilities: {
381
- prompts: {},
382
- resources: {},
383
- tools: {},
384
- },
385
- });
386
- const initRequestOptions = isInit
387
- ? {
388
- timeout: Number(config.initTimeout) || 60000,
389
- }
390
- : undefined;
391
- // Get request options from server configuration, with fallbacks
392
- const serverRequestOptions = conf.options || {};
393
- const requestOptions = {
394
- timeout: serverRequestOptions.timeout || 60000,
395
- resetTimeoutOnProgress: serverRequestOptions.resetTimeoutOnProgress || false,
396
- maxTotalTimeout: serverRequestOptions.maxTotalTimeout,
397
- };
398
- // Create server info first and keep reference to it
399
- const serverInfo = {
400
- name,
401
- owner: conf.owner,
402
- status: 'connecting',
403
- error: null,
404
- tools: [],
405
- prompts: [],
406
- client,
407
- transport,
408
- options: requestOptions,
409
- createTime: Date.now(),
410
- config: conf, // Store reference to original config
411
- };
412
- serverInfos.push(serverInfo);
413
- client
414
- .connect(transport, initRequestOptions || requestOptions)
415
- .then(() => {
416
- console.log(`Successfully connected client for server: ${name}`);
417
- const capabilities = client.getServerCapabilities();
418
- console.log(`Server capabilities: ${JSON.stringify(capabilities)}`);
419
- let dataError = null;
420
- if (capabilities?.tools) {
421
- client
422
- .listTools({}, initRequestOptions || requestOptions)
423
- .then((tools) => {
424
- console.log(`Successfully listed ${tools.tools.length} tools for server: ${name}`);
425
- serverInfo.tools = tools.tools.map((tool) => ({
426
- name: `${name}${getNameSeparator()}${tool.name}`,
427
- description: tool.description || '',
428
- inputSchema: cleanInputSchema(tool.inputSchema || {}),
429
- }));
430
- // Save tools as vector embeddings for search
431
- saveToolsAsVectorEmbeddings(name, serverInfo.tools);
432
- })
433
- .catch((error) => {
434
- console.error(`Failed to list tools for server ${name} by error: ${error} with stack: ${error.stack}`);
435
- dataError = error;
436
- });
437
- }
438
- if (capabilities?.prompts) {
439
- client
440
- .listPrompts({}, initRequestOptions || requestOptions)
441
- .then((prompts) => {
442
- console.log(`Successfully listed ${prompts.prompts.length} prompts for server: ${name}`);
443
- serverInfo.prompts = prompts.prompts.map((prompt) => ({
444
- name: `${name}${getNameSeparator()}${prompt.name}`,
445
- title: prompt.title,
446
- description: prompt.description,
447
- arguments: prompt.arguments,
448
- }));
449
- })
450
- .catch((error) => {
451
- console.error(`Failed to list prompts for server ${name} by error: ${error} with stack: ${error.stack}`);
452
- dataError = error;
453
- });
454
- }
455
- if (!dataError) {
456
- serverInfo.status = 'connected';
435
+ const pendingAuth = expandedConf.oauth?.pendingAuthorization;
436
+ if (pendingAuth) {
437
+ serverInfo.status = 'oauth_required';
457
438
  serverInfo.error = null;
458
- // Set up keep-alive ping for SSE connections
459
- setupKeepAlive(serverInfo, conf);
460
- }
461
- else {
462
- serverInfo.status = 'disconnected';
463
- serverInfo.error = `Failed to list data: ${dataError} `;
439
+ serverInfo.oauth = {
440
+ authorizationUrl: pendingAuth.authorizationUrl,
441
+ state: pendingAuth.state,
442
+ codeVerifier: pendingAuth.codeVerifier,
443
+ };
464
444
  }
465
- })
466
- .catch((error) => {
467
- console.error(`Failed to connect client for server ${name} by error: ${error} with stack: ${error.stack}`);
468
- serverInfo.status = 'disconnected';
469
- serverInfo.error = `Failed to connect: ${error.stack} `;
470
- });
471
- console.log(`Initialized client for server: ${name}`);
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;
472
527
  }
528
+ serverInfos = nextServerInfos;
473
529
  return serverInfos;
474
530
  };
475
531
  // Register all MCP tools
@@ -483,7 +539,7 @@ export const getServersInfo = async () => {
483
539
  const filterServerInfos = dataService.filterData
484
540
  ? dataService.filterData(serverInfos)
485
541
  : serverInfos;
486
- const infos = filterServerInfos.map(({ name, status, tools, prompts, createTime, error }) => {
542
+ const infos = filterServerInfos.map(({ name, status, tools, prompts, createTime, error, oauth }) => {
487
543
  const serverConfig = allServers.find((server) => server.name === name);
488
544
  const enabled = serverConfig ? serverConfig.enabled !== false : true;
489
545
  // Add enabled status and custom description to each tool
@@ -511,6 +567,13 @@ export const getServersInfo = async () => {
511
567
  prompts: promptsWithEnabled,
512
568
  createTime,
513
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,
514
577
  };
515
578
  });
516
579
  infos.sort((a, b) => {
@@ -524,6 +587,45 @@ export const getServersInfo = async () => {
524
587
  export const getServerByName = (name) => {
525
588
  return serverInfos.find((serverInfo) => serverInfo.name === name);
526
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
+ };
527
629
  // Filter tools by server configuration
528
630
  const filterToolsByConfig = async (serverName, tools) => {
529
631
  const serverConfig = await serverDao.findById(serverName);
@@ -635,28 +737,39 @@ export const handleListToolsRequest = async (_, extra) => {
635
737
  const group = getGroup(sessionId);
636
738
  console.log(`Handling ListToolsRequest for group: ${group}`);
637
739
  // Special handling for $smart group to return special tools
638
- if (group === '$smart') {
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';
639
762
  return {
640
763
  tools: [
641
764
  {
642
765
  name: 'search_tools',
643
- description: (() => {
644
- // Get info about available servers
645
- const availableServers = serverInfos.filter((server) => server.status === 'connected' && server.enabled !== false);
646
- // Create simple server information with only server names
647
- const serversList = availableServers
648
- .map((server) => {
649
- return `${server.name}`;
650
- })
651
- .join(', ');
652
- 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.
653
767
 
654
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.
655
769
 
656
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.
657
771
 
658
- Available servers: ${serversList}`;
659
- })(),
772
+ Available servers: ${serversList}`,
660
773
  inputSchema: {
661
774
  type: 'object',
662
775
  properties: {
@@ -757,7 +870,24 @@ export const handleCallToolRequest = async (request, extra) => {
757
870
  thresholdNum = 0.4;
758
871
  }
759
872
  console.log(`Using similarity threshold: ${thresholdNum} for query: "${query}"`);
760
- const servers = undefined; // No server filtering
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
+ }
761
891
  const searchResults = await searchToolsByVector(query, limitNum, thresholdNum, servers);
762
892
  console.log(`Search results: ${JSON.stringify(searchResults)}`);
763
893
  // Find actual tool information from serverInfos by serverName and toolName