@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.
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 +328 -199
  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-BLUhSkL4.js +0 -217
  36. package/frontend/dist/assets/index-BLUhSkL4.js.map +0 -1
  37. 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
- 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,
@@ -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
- if (isHttp40xError && attempt < maxRetries && serverInfo.transport && isStreamableHttp) {
208
- console.warn(`HTTP 40x error detected for StreamableHTTP server ${serverInfo.name}, attempting reconnection (attempt ${attempt + 1}/${maxRetries + 1})`);
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
- serverInfos = [];
283
- for (const conf of allServers) {
284
- const { name } = conf;
285
- // Skip disabled servers
286
- if (conf.enabled === false) {
287
- console.log(`Skipping disabled server: ${name}`);
288
- serverInfos.push({
289
- name,
290
- owner: conf.owner,
291
- status: 'disconnected',
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: conf.owner,
316
+ owner: expandedConf.owner,
319
317
  status: 'disconnected',
320
- error: 'Missing OpenAPI specification URL or schema',
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: conf.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
- enabled: conf.enabled === undefined ? true : conf.enabled,
337
- config: conf, // Store reference to original config for OpenAPI passthrough headers
433
+ config: expandedConf, // Store reference to expanded config
338
434
  };
339
- serverInfos.push(serverInfo);
340
- try {
341
- // Create OpenAPI client instance
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
- // Set up keep-alive ping for SSE connections
456
- setupKeepAlive(serverInfo, conf);
457
- }
458
- else {
459
- serverInfo.status = 'disconnected';
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
- .catch((error) => {
464
- console.error(`Failed to connect client for server ${name} by error: ${error} with stack: ${error.stack}`);
465
- serverInfo.status = 'disconnected';
466
- serverInfo.error = `Failed to connect: ${error.stack} `;
467
- });
468
- 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;
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
- 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';
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
- 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
+ }
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,