@tiledesk/tiledesk-server 2.23.0 → 2.24.1

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.
@@ -1,7 +1,15 @@
1
1
  'use strict';
2
2
 
3
+ const fs = require('fs').promises;
4
+ const path = require('path');
3
5
  const axios = require('axios').default;
4
6
  const winston = require('../config/winston');
7
+ const cacheUtil = require('../utils/cacheUtil');
8
+ const Integration = require('../models/integrations');
9
+ const integrationEvent = require('../event/integrationEvent');
10
+
11
+ const NATIVE_MCP_CACHE_KEY = 'native_mcp:servers';
12
+ const NATIVE_MCP_CONFIG_PATH = path.join(__dirname, '../config/native_mcp/native_mcp_servers.json');
5
13
 
6
14
 
7
15
  /**
@@ -15,6 +23,167 @@ class MCPService {
15
23
  // Key: `${projectId}_${url}` or just `${url}` if projectId is not provided
16
24
  // Value: { config, sessionId?, initialized, capabilities }
17
25
  this.connections = new Map();
26
+ this.tdCache = undefined;
27
+ }
28
+
29
+ setTdCache(tdCache) {
30
+ this.tdCache = tdCache;
31
+ }
32
+
33
+ /**
34
+ * Normalizes auth.type to a canonical value
35
+ * Accepts: mcp_api_key, X-MCP-API-Key, x-mcp-api-key, etc.
36
+ * @param {String} type - Auth type from request body
37
+ * @returns {String} - Normalized auth type
38
+ */
39
+ normalizeAuthType(type) {
40
+ return (type || '').toLowerCase().replace(/_/g, '-');
41
+ }
42
+
43
+ /**
44
+ * Applies custom headers from the client payload: [{ key, value }] or [{ name, value }]
45
+ * @param {Object} headers - HTTP headers object
46
+ * @param {Array} customHeaders - Custom headers array
47
+ * @returns {Boolean} - Whether any header was applied
48
+ */
49
+ applyCustomHeaders(headers, customHeaders) {
50
+ if (!Array.isArray(customHeaders)) {
51
+ return false;
52
+ }
53
+
54
+ let applied = false;
55
+ for (const header of customHeaders) {
56
+ const name = header.key || header.name || header.headerName;
57
+ const value = header.value ?? header.headerValue;
58
+ if (name && value != null && value !== '') {
59
+ headers[name] = value;
60
+ applied = true;
61
+ }
62
+ }
63
+ return applied;
64
+ }
65
+
66
+ /**
67
+ * Applies customHeaders and/or legacy auth config to outgoing MCP requests
68
+ * @param {Object} headers - HTTP headers object
69
+ * @param {Object} serverConfig - { auth?, customHeaders? }
70
+ * @returns {Boolean} - Whether any header was applied
71
+ */
72
+ applyRequestHeaders(headers, serverConfig) {
73
+ if (!serverConfig) {
74
+ return false;
75
+ }
76
+
77
+ const fromCustom = this.applyCustomHeaders(headers, serverConfig.customHeaders);
78
+ const fromAuth = serverConfig.auth ? this.applyAuthHeaders(headers, serverConfig.auth) : false;
79
+ return fromCustom || fromAuth;
80
+ }
81
+
82
+ /**
83
+ * Builds serverConfig from the client request body
84
+ * @param {String} projectId - Tiledesk project ID
85
+ * @param {Object} body - Request body
86
+ * @returns {Object} - Normalized server configuration
87
+ */
88
+ buildServerConfig(projectId, body) {
89
+ const { url, auth, customHeaders, oauth } = body;
90
+
91
+ if (!url) {
92
+ throw new Error("Missing required parameter 'url'");
93
+ }
94
+
95
+ const hasCustomHeaders = Array.isArray(customHeaders) && customHeaders.length > 0;
96
+ const hasAuth = auth && typeof auth === 'object' && Object.keys(auth).length > 0;
97
+ const hasOAuth = oauth && typeof oauth === 'object' && (
98
+ oauth.clientId || oauth.clientSecret || oauth.redirectUrl || oauth.scope
99
+ );
100
+
101
+ if (hasOAuth && !hasCustomHeaders && !hasAuth) {
102
+ throw new Error('OAuth2 authentication is not supported yet. Use customHeaders instead.');
103
+ }
104
+
105
+ if (hasOAuth) {
106
+ winston.warn('MCP OAuth2 config received but not supported yet; ignoring oauth object');
107
+ }
108
+
109
+ return {
110
+ url,
111
+ projectId,
112
+ auth: hasAuth ? auth : undefined,
113
+ customHeaders: hasCustomHeaders ? customHeaders : undefined
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Applies authentication headers to an HTTP request config (legacy auth object).
119
+ * Supports predefined types and generic custom headers (headerName/headerValue).
120
+ * @param {Object} headers - HTTP headers object
121
+ * @param {Object} auth - Authentication configuration (optional)
122
+ * @returns {Boolean} - Whether auth headers were applied
123
+ */
124
+ applyAuthHeaders(headers, auth) {
125
+ if (!auth) {
126
+ return false;
127
+ }
128
+
129
+ // Generic custom header — matches UI "header name" / "header value" fields
130
+ if (auth.headerName && auth.headerValue != null && auth.headerValue !== '') {
131
+ headers[auth.headerName] = auth.headerValue;
132
+ return true;
133
+ }
134
+ if (auth.name && auth.value != null && auth.value !== '') {
135
+ headers[auth.name] = auth.value;
136
+ return true;
137
+ }
138
+
139
+ const hasCredentials = !!(auth.token || auth.key || auth.username || auth.password);
140
+ if (!auth.type && !hasCredentials) {
141
+ return false;
142
+ }
143
+
144
+ const authType = this.normalizeAuthType(auth.type);
145
+
146
+ if (authType === 'custom') {
147
+ if (Array.isArray(auth.headers)) {
148
+ let applied = false;
149
+ for (const header of auth.headers) {
150
+ const name = header.key || header.name || header.headerName;
151
+ const value = header.value ?? header.headerValue;
152
+ if (name && value != null && value !== '') {
153
+ headers[name] = value;
154
+ applied = true;
155
+ }
156
+ }
157
+ return applied;
158
+ }
159
+ return false;
160
+ }
161
+
162
+ if (authType === 'bearer') {
163
+ const token = auth.token || auth.key;
164
+ if (token) {
165
+ headers['Authorization'] = token.startsWith('Bearer ') ? token : `Bearer ${token}`;
166
+ return true;
167
+ }
168
+ }
169
+ if ((authType === 'mcp-api-key' || authType === 'x-mcp-api-key') && auth.key) {
170
+ headers['X-MCP-API-Key'] = auth.key;
171
+ return true;
172
+ }
173
+ if ((authType === 'api-key' || authType === 'x-api-key') && auth.key) {
174
+ headers['X-API-Key'] = auth.key;
175
+ return true;
176
+ }
177
+ if (authType === 'basic' && auth.username && auth.password) {
178
+ const credentials = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');
179
+ headers['Authorization'] = `Basic ${credentials}`;
180
+ return true;
181
+ }
182
+
183
+ if (auth.type || hasCredentials) {
184
+ winston.warn(`MCP auth type not recognized or incomplete: "${auth.type}"`);
185
+ }
186
+ return false;
18
187
  }
19
188
 
20
189
  /**
@@ -37,15 +206,122 @@ class MCPService {
37
206
  throw new Error('No data field found in SSE response');
38
207
  }
39
208
 
209
+ /**
210
+ * Parses an MCP HTTP response body (JSON, SSE, or empty)
211
+ * @param {*} data - Response body
212
+ * @returns {Object|null} - Parsed JSON object, or null for empty bodies
213
+ */
214
+ parseResponseBody(data) {
215
+ if (data === '' || data == null) {
216
+ return null;
217
+ }
218
+ if (typeof data === 'string') {
219
+ if (!data.trim()) {
220
+ return null;
221
+ }
222
+ return this.parseSSEResponse(data);
223
+ }
224
+ return data;
225
+ }
226
+
227
+ /**
228
+ * Sends an initialize request, retrying with an older protocol version if needed
229
+ * @param {String} url - MCP server URL
230
+ * @param {Object} auth - Authentication configuration (optional)
231
+ * @returns {Promise<Object>} - Initialize response
232
+ */
233
+ async sendInitializeRequest(serverConfig) {
234
+ const protocolVersions = ['2025-03-26', '2024-11-05'];
235
+ let lastError = null;
236
+
237
+ for (const protocolVersion of protocolVersions) {
238
+ try {
239
+ return await this.sendJSONRPCRequest(serverConfig.url, {
240
+ jsonrpc: '2.0',
241
+ id: Date.now(),
242
+ method: 'initialize',
243
+ params: {
244
+ protocolVersion,
245
+ capabilities: {
246
+ tools: {}
247
+ },
248
+ clientInfo: {
249
+ name: 'tiledesk-server',
250
+ version: '1.0.0'
251
+ }
252
+ }
253
+ }, serverConfig, null);
254
+ } catch (error) {
255
+ lastError = error;
256
+ const isProtocolError = error.message && (
257
+ error.message.includes('protocol version') ||
258
+ error.message.includes('Unsupported protocol') ||
259
+ error.message.includes('code: -32602')
260
+ );
261
+ if (!isProtocolError) {
262
+ throw error;
263
+ }
264
+ winston.debug(`MCP initialize failed with protocolVersion ${protocolVersion}, trying fallback`);
265
+ }
266
+ }
267
+
268
+ throw lastError;
269
+ }
270
+
271
+ /**
272
+ * Sends notifications/initialized when the server uses sessions.
273
+ * Best-effort: some servers require it (Streamable HTTP), others ignore or reject it.
274
+ * @returns {Promise<Boolean>} - true if notification was accepted
275
+ */
276
+ async sendInitializedNotification(serverConfig, sessionId) {
277
+ const config = {
278
+ method: 'POST',
279
+ url: serverConfig.url,
280
+ headers: {
281
+ 'Content-Type': 'application/json',
282
+ 'Accept': 'application/json, text/event-stream',
283
+ 'mcp-session-id': sessionId
284
+ },
285
+ data: {
286
+ jsonrpc: '2.0',
287
+ method: 'notifications/initialized'
288
+ },
289
+ timeout: 30000,
290
+ validateStatus: function (status) {
291
+ return status >= 200 && status < 500;
292
+ }
293
+ };
294
+
295
+ this.applyRequestHeaders(config.headers, serverConfig);
296
+
297
+ try {
298
+ const response = await axios(config);
299
+
300
+ if (response.status >= 400) {
301
+ return false;
302
+ }
303
+
304
+ const responseData = this.parseResponseBody(response.data);
305
+ if (responseData?.error) {
306
+ return false;
307
+ }
308
+
309
+ return true;
310
+ } catch (error) {
311
+ winston.debug(`MCP notifications/initialized failed: ${error.message}`);
312
+ return false;
313
+ }
314
+ }
315
+
40
316
  /**
41
317
  * Sends a JSON-RPC request to an MCP server
42
318
  * @param {String} url - MCP server URL
43
319
  * @param {Object} request - JSON-RPC request
44
- * @param {Object} auth - Authentication configuration (optional)
320
+ * @param {Object} serverConfig - MCP server configuration { url, projectId?, auth?, customHeaders? }
45
321
  * @param {String} sessionId - Session ID for the MCP server (optional)
46
322
  * @returns {Promise<Object>} - Server response (includes sessionId if present in headers)
47
323
  */
48
- async sendJSONRPCRequest(url, request, auth, sessionId) {
324
+ async sendJSONRPCRequest(url, request, serverConfig, sessionId) {
49
325
  const config = {
50
326
  method: 'POST',
51
327
  url: url,
@@ -65,33 +341,33 @@ class MCPService {
65
341
  config.headers['mcp-session-id'] = sessionId;
66
342
  }
67
343
 
68
- // Add authentication if present
69
- if (auth) {
70
- if (auth.type === 'bearer' && auth.token) {
71
- config.headers['Authorization'] = `Bearer ${auth.token}`;
72
- } else if (auth.type === 'api_key' && auth.key) {
73
- config.headers['X-API-Key'] = auth.key;
74
- } else if (auth.type === 'basic' && auth.username && auth.password) {
75
- const credentials = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');
76
- config.headers['Authorization'] = `Basic ${credentials}`;
77
- }
78
- }
344
+ this.applyRequestHeaders(config.headers, serverConfig);
79
345
 
80
346
  try {
81
347
  const response = await axios(config);
82
348
 
349
+ if (response.status >= 400) {
350
+ const details = response.data ? ` - ${JSON.stringify(response.data)}` : '';
351
+ throw new Error(`HTTP Error ${response.status}: ${response.statusText}${details}`);
352
+ }
353
+
83
354
  // Capture session ID from response headers (if present)
84
- const sessionIdFromHeader = response.headers['mcp-session-id'] ||
355
+ const sessionIdFromHeader = response.headers['mcp-session-id'] ||
85
356
  response.headers['x-mcp-session-id'];
86
357
 
358
+ // Notifications and empty responses (e.g. 202 Accepted)
359
+ if (response.status === 202 || response.data === '' || response.data == null) {
360
+ const result = { status: response.status };
361
+ if (sessionIdFromHeader) {
362
+ result.sessionId = sessionIdFromHeader;
363
+ }
364
+ return result;
365
+ }
366
+
87
367
  // Parse response: could be SSE (text/event-stream) or direct JSON
88
- let responseData;
89
- if (typeof response.data === 'string') {
90
- // SSE format: extracts JSON from "data:" line
91
- responseData = this.parseSSEResponse(response.data);
92
- } else {
93
- // JSON response already parsed
94
- responseData = response.data;
368
+ let responseData = this.parseResponseBody(response.data);
369
+ if (!responseData) {
370
+ responseData = { status: response.status };
95
371
  }
96
372
 
97
373
  // Add session ID from response if present in headers
@@ -134,9 +410,226 @@ class MCPService {
134
410
  return projectId ? `${projectId}_${url}` : url;
135
411
  }
136
412
 
413
+ async getNativeServersCatalog() {
414
+ if (this.tdCache) {
415
+ try {
416
+ const cached = await this.tdCache.getJSON(NATIVE_MCP_CACHE_KEY);
417
+ if (cached) {
418
+ return cached;
419
+ }
420
+ } catch (error) {
421
+ winston.warn('Error reading native MCP servers from cache:', error);
422
+ }
423
+ }
424
+
425
+ const servers = await this.loadNativeServersFromFile();
426
+
427
+ if (this.tdCache) {
428
+ try {
429
+ await this.tdCache.setJSON(NATIVE_MCP_CACHE_KEY, servers, { EX: cacheUtil.longTTL });
430
+ } catch (error) {
431
+ winston.warn('Error caching native MCP servers:', error);
432
+ }
433
+ }
434
+
435
+ return servers;
436
+ }
437
+
438
+ sanitizeNativeServerForPublic(server) {
439
+ const publicServer = {
440
+ id: server.id,
441
+ name: server.name,
442
+ native: server.native,
443
+ transport: server.transport
444
+ };
445
+ if (server.description) {
446
+ publicServer.description = server.description;
447
+ }
448
+ return publicServer;
449
+ }
450
+
451
+ async loadNativeServersFromFile() {
452
+ const content = await fs.readFile(NATIVE_MCP_CONFIG_PATH, 'utf8');
453
+ const servers = JSON.parse(content);
454
+
455
+ if (!Array.isArray(servers)) {
456
+ throw new Error('Native MCP servers configuration must be an array');
457
+ }
458
+
459
+ for (const server of servers) {
460
+ if (!server.id) {
461
+ throw new Error(`Native MCP server '${server.name || 'unknown'}' is missing required field 'id'`);
462
+ }
463
+ }
464
+
465
+ return servers;
466
+ }
467
+
468
+ async getNativeServersPublic() {
469
+ const servers = await this.getNativeServersCatalog();
470
+ return servers.map((server) => this.sanitizeNativeServerForPublic(server));
471
+ }
472
+
473
+ async getNativeServerById(id) {
474
+ const servers = await this.getNativeServersCatalog();
475
+ return servers.find((server) => server.id === id) || null;
476
+ }
477
+
478
+ async resolveNativeServerConfig(nativeId, projectId) {
479
+ const native = await this.getNativeServerById(nativeId);
480
+ if (!native) {
481
+ const error = new Error(`Native MCP server '${nativeId}' not found`);
482
+ error.statusCode = 404;
483
+ throw error;
484
+ }
485
+
486
+ return {
487
+ url: native.url,
488
+ projectId,
489
+ transport: native.transport,
490
+ auth: native.auth || undefined
491
+ };
492
+ }
493
+
494
+ formatToolsForIntegration(tools) {
495
+ return (tools || []).map((tool) => ({
496
+ name: tool.name,
497
+ description: tool.description || ''
498
+ }));
499
+ }
500
+
501
+ formatSelectedTools(selectedTools) {
502
+ return (selectedTools || []).map((tool) => ({
503
+ name: typeof tool === 'string' ? tool : tool.name
504
+ })).filter((tool) => tool.name);
505
+ }
506
+
507
+ findServerIndex(servers, entry) {
508
+ if (entry.type === 'native') {
509
+ return servers.findIndex((server) => server.type === 'native' && server.nativeId === entry.nativeId);
510
+ }
511
+
512
+ return servers.findIndex((server) => {
513
+ const isCustom = server.type === 'custom' || !server.type;
514
+ return isCustom && server.url === entry.url;
515
+ });
516
+ }
517
+
518
+ async normalizeMcpServerEntry(serverEntry, projectId) {
519
+ if (!serverEntry) {
520
+ throw new Error('MCP server entry is missing or invalid');
521
+ }
522
+
523
+ const type = serverEntry.type || (serverEntry.nativeId ? 'native' : 'custom');
524
+
525
+ if (type === 'native') {
526
+ if (!serverEntry.nativeId) {
527
+ throw new Error("Missing required field 'nativeId'");
528
+ }
529
+
530
+ const native = await this.getNativeServerById(serverEntry.nativeId);
531
+ if (!native) {
532
+ const error = new Error(`Native MCP server '${serverEntry.nativeId}' not found`);
533
+ error.statusCode = 404;
534
+ throw error;
535
+ }
536
+
537
+ let tools = serverEntry.tools;
538
+ if (!tools || !tools.length) {
539
+ const serverConfig = await this.resolveNativeServerConfig(serverEntry.nativeId, projectId);
540
+ tools = await this.listTools(serverConfig);
541
+ }
542
+
543
+ return {
544
+ type: 'native',
545
+ nativeId: native.id,
546
+ name: native.name,
547
+ transport: native.transport,
548
+ tools: this.formatToolsForIntegration(tools),
549
+ selectedTools: this.formatSelectedTools(serverEntry.selectedTools)
550
+ };
551
+ }
552
+
553
+ if (!serverEntry.url) {
554
+ throw new Error("Missing required field 'url'");
555
+ }
556
+
557
+ let tools = serverEntry.tools;
558
+ if (!tools || !tools.length) {
559
+ tools = await this.listTools({
560
+ url: serverEntry.url,
561
+ projectId,
562
+ auth: serverEntry.auth || undefined
563
+ });
564
+ }
565
+
566
+ return {
567
+ type: 'custom',
568
+ name: serverEntry.name || serverEntry.url,
569
+ url: serverEntry.url,
570
+ transport: serverEntry.transport || 'streamable_http',
571
+ customHeaders: serverEntry.customHeaders || [],
572
+ tools: this.formatToolsForIntegration(tools),
573
+ selectedTools: this.formatSelectedTools(serverEntry.selectedTools)
574
+ };
575
+ }
576
+
577
+ async upsertMcpServerInIntegration(projectId, serverEntry) {
578
+ const normalized = await this.normalizeMcpServerEntry(serverEntry, projectId);
579
+
580
+ let integration = await Integration.findOne({ id_project: projectId, name: 'mcp' });
581
+
582
+ if (!integration) {
583
+ integration = new Integration({
584
+ id_project: projectId,
585
+ name: 'mcp',
586
+ value: { servers: [normalized] }
587
+ });
588
+ } else {
589
+ const servers = Array.isArray(integration.value?.servers) ? [...integration.value.servers] : [];
590
+ const index = this.findServerIndex(servers, normalized);
591
+
592
+ if (index >= 0) {
593
+ servers[index] = normalized;
594
+ } else {
595
+ servers.push(normalized);
596
+ }
597
+
598
+ integration.value = {
599
+ ...integration.value,
600
+ servers
601
+ };
602
+ integration.markModified('value');
603
+ }
604
+
605
+ const savedIntegration = await integration.save();
606
+ const integrations = await Integration.find({ id_project: projectId });
607
+ integrationEvent.emit('integration.update', integrations, projectId);
608
+
609
+ return {
610
+ integration: savedIntegration,
611
+ server: normalized
612
+ };
613
+ }
614
+
137
615
  /**
138
- * Initializes a connection to an MCP server
616
+ * Connects to an MCP server and returns capabilities + tools
139
617
  * @param {Object} serverConfig - MCP server configuration { url, projectId?, auth? }
618
+ * @returns {Promise<Object>} - { capabilities, tools }
619
+ */
620
+ async connectServer(serverConfig) {
621
+ const result = await this.initializeServer(serverConfig);
622
+ const tools = await this.listTools(serverConfig);
623
+
624
+ return {
625
+ capabilities: result?.capabilities || {},
626
+ tools
627
+ };
628
+ }
629
+
630
+ /**
631
+ * Initializes a connection to an MCP server
632
+ * @param {Object} serverConfig - MCP server configuration { url, projectId?, auth?, customHeaders? }
140
633
  * @returns {Promise<Object>} - Initialization result
141
634
  */
142
635
  async initializeServer(serverConfig) {
@@ -150,38 +643,30 @@ class MCPService {
150
643
  // Initialize session (some MCP servers require session ID)
151
644
  let sessionId = null;
152
645
  let initializeResponse = null;
153
-
646
+
154
647
  try {
155
- // JSON-RPC call to initialize the server
156
- const response = await this.sendJSONRPCRequest(serverConfig.url, {
157
- jsonrpc: '2.0',
158
- id: Date.now(),
159
- method: 'initialize',
160
- params: {
161
- protocolVersion: '2024-11-05',
162
- capabilities: {
163
- tools: {}
164
- },
165
- clientInfo: {
166
- name: 'tiledesk-server',
167
- version: '1.0.0'
168
- }
169
- }
170
- }, serverConfig.auth, null);
648
+ const response = await this.sendInitializeRequest(serverConfig);
171
649
 
172
650
  // Session ID is returned in 'mcp-session-id' header from response
173
651
  sessionId = response.sessionId || null;
174
652
  initializeResponse = response;
175
653
 
176
654
  if (sessionId) {
177
- winston.debug(`MCP Server ${serverId} session initialized with ID: ${sessionId}`);
655
+ winston.info(`MCP Server ${serverId} session initialized with ID: ${sessionId}`);
656
+ const notificationAccepted = await this.sendInitializedNotification(
657
+ serverConfig,
658
+ sessionId
659
+ );
660
+ if (notificationAccepted) {
661
+ winston.info(`MCP Server ${serverId} sent notifications/initialized`);
662
+ } else {
663
+ winston.warn(`MCP Server ${serverId} notifications/initialized not accepted (continuing)`);
664
+ }
178
665
  } else {
179
- winston.debug(`MCP Server ${serverId} initialized (stateless, no session ID required)`);
666
+ winston.info(`MCP Server ${serverId} initialized (stateless, no session ID required)`);
180
667
  }
181
668
  } catch (initError) {
182
- // If initialization fails, try anyway without session ID
183
- // (some MCP servers don't require session ID)
184
- winston.debug(`MCP Server ${serverId} initialization failed: ${initError.message}`);
669
+ winston.error(`MCP Server ${serverId} initialization failed: ${initError.message}`);
185
670
  throw initError;
186
671
  }
187
672
 
@@ -193,7 +678,7 @@ class MCPService {
193
678
  capabilities: initializeResponse.result?.capabilities || {}
194
679
  });
195
680
 
196
- winston.info(`MCP Server initialized: ${serverId}${sessionId ? ` (session: ${sessionId})` : ' (stateless)'}`);
681
+ winston.info(`MCP Server ready: ${serverId}${sessionId ? ` (session: ${sessionId})` : ' (stateless)'}`);
197
682
  return initializeResponse.result;
198
683
  } catch (error) {
199
684
  winston.error(`Error initializing MCP server ${serverId}:`, error);
@@ -203,7 +688,7 @@ class MCPService {
203
688
 
204
689
  /**
205
690
  * Gets the list of available tools from an MCP server
206
- * @param {Object} serverConfig - MCP server configuration { url, projectId?, auth? }
691
+ * @param {Object} serverConfig - MCP server configuration { url, projectId?, auth?, customHeaders? }
207
692
  * @returns {Promise<Array>} - List of available tools
208
693
  */
209
694
  async listTools(serverConfig) {
@@ -214,6 +699,8 @@ class MCPService {
214
699
  const serverId = this.getCacheKey(serverConfig.url, serverConfig.projectId);
215
700
 
216
701
  try {
702
+ winston.info(`MCP listTools request for ${serverId}`);
703
+
217
704
  // Check if server is already initialized
218
705
  let connection = this.connections.get(serverId);
219
706
  if (!connection) {
@@ -232,9 +719,9 @@ class MCPService {
232
719
  id: Date.now(),
233
720
  method: 'tools/list',
234
721
  params: {}
235
- }, serverConfig.auth, sessionId);
722
+ }, serverConfig, sessionId);
236
723
  } catch (error) {
237
- // If it fails and we have a "No valid session ID" error, try without session ID
724
+ // Fallback for stateless servers that reject session IDs
238
725
  if (sessionId && error.message && error.message.includes('No valid session ID')) {
239
726
  winston.debug(`Retrying tools/list without session ID for server ${serverId}...`);
240
727
  response = await this.sendJSONRPCRequest(serverConfig.url, {
@@ -242,14 +729,14 @@ class MCPService {
242
729
  id: Date.now(),
243
730
  method: 'tools/list',
244
731
  params: {}
245
- }, serverConfig.auth, null);
732
+ }, serverConfig, null);
246
733
  } else {
247
734
  throw error;
248
735
  }
249
736
  }
250
737
 
251
738
  const tools = response.result?.tools || [];
252
- winston.debug(`MCP Server ${serverId} returned ${tools.length} tools`);
739
+ winston.info(`MCP Server ${serverId} returned ${tools.length} tools`);
253
740
  return tools;
254
741
  } catch (error) {
255
742
  winston.error(`Error listing tools from MCP server ${serverId}:`, error);
@@ -280,4 +767,3 @@ class MCPService {
280
767
  const mcpService = new MCPService();
281
768
 
282
769
  module.exports = mcpService;
283
-