@tiledesk/tiledesk-server 2.23.1 → 2.24.2

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/CHANGELOG.md CHANGED
@@ -5,6 +5,15 @@
5
5
  🚀 IN PRODUCTION 🚀
6
6
  (https://www.npmjs.com/package/@tiledesk/tiledesk-server/v/2.3.77)
7
7
 
8
+ # 2.24.2
9
+ - Fixed bug recalling webhooks
10
+
11
+ # 2.24.1
12
+ - Added native MCP servers catalog and endpoints to list, connect and retrieve tools
13
+ - Added endpoint to save native and custom MCP servers in the project integration
14
+ - Improved MCP authentication with custom headers and additional auth types
15
+ - Improved MCP connection handshake with protocol version fallback and session initialization
16
+
8
17
  # 2.23.1
9
18
  - Restored cross-project access for global subscriptions (`id_project: '*'` or `global: true`); bots and project-bound subscriptions remain scoped to their own project
10
19
 
package/Dockerfile CHANGED
@@ -1,7 +1,4 @@
1
- FROM node:18-bullseye
2
-
3
- RUN sed -i 's/stable\/updates/stable-security\/updates/' /etc/apt/sources.list
4
-
1
+ FROM node:18-bookworm
5
2
 
6
3
  RUN apt-get update
7
4
 
package/Dockerfile-en CHANGED
@@ -1,6 +1,4 @@
1
- FROM node:18-bullseye
2
-
3
- RUN sed -i 's/stable\/updates/stable-security\/updates/' /etc/apt/sources.list
1
+ FROM node:18-bookworm
4
2
 
5
3
  RUN apt-get update
6
4
 
package/Dockerfile-jobs CHANGED
@@ -1,6 +1,4 @@
1
- FROM node:18-bullseye
2
-
3
- RUN sed -i 's/stable\/updates/stable-security\/updates/' /etc/apt/sources.list
1
+ FROM node:18-bookworm
4
2
 
5
3
  RUN apt-get update
6
4
 
@@ -1,7 +1,4 @@
1
- FROM node:18-bullseye
2
-
3
- RUN sed -i 's/stable\/updates/stable-security\/updates/' /etc/apt/sources.list
4
-
1
+ FROM node:18-bookworm
5
2
 
6
3
  RUN apt-get update
7
4
 
package/app.js CHANGED
@@ -93,6 +93,9 @@ let tdCache = new TdCache({
93
93
 
94
94
  tdCache.connect();
95
95
 
96
+ const mcpService = require('./services/mcpService');
97
+ mcpService.setTdCache(tdCache);
98
+
96
99
  // ROUTES DECLARATION
97
100
  var troubleshooting = require('./routes/troubleshooting');
98
101
  var auth = require('./routes/auth');
@@ -0,0 +1,18 @@
1
+ [
2
+ {
3
+ "id": "tiledesk-communicator",
4
+ "name": "Tiledesk Communicator",
5
+ "url": "https://stage.eks.tiledesk.com/mcp-tools/communicator/mcp",
6
+ "native": true,
7
+ "transport": "streamable_http",
8
+ "description": "Tiledesk Communicator is a MCP server that allows you to communicate with the Tiledesk platform."
9
+ },
10
+ {
11
+ "id": "tiledesk-data-table",
12
+ "name": "Tiledesk Data Table",
13
+ "url": "https://stage.eks.tiledesk.com/mcp-tools/data-table/mcp",
14
+ "native": true,
15
+ "transport": "streamable_http",
16
+ "description": "Tiledesk Data Table is a MCP server that allows you to manage data tables in the Tiledesk platform."
17
+ }
18
+ ]
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiledesk/tiledesk-server",
3
3
  "description": "The Tiledesk server module",
4
- "version": "2.23.1",
4
+ "version": "2.24.2",
5
5
  "scripts": {
6
6
  "start": "node ./bin/www",
7
7
  "pretest": "mongodb-runner start",
@@ -4,13 +4,12 @@ var winston = require('../../config/winston');
4
4
  var configGlobal = require('../../config/global');
5
5
 
6
6
  var port = process.env.PORT || '3000';
7
+ let TILEBOT_ENDPOINT = process.env.API_ENDPOINT ? process.env.API_ENDPOINT + "/modules/tilebot/ext/" : ("http://localhost:" + port + "/modules/tilebot/ext/");
7
8
 
8
- let TILEBOT_ENDPOINT = "http://localhost:" + port + "/modules/tilebot/ext/";;
9
9
  if (process.env.TILEBOT_ENDPOINT) {
10
- TILEBOT_ENDPOINT = process.env.TILEBOT_ENDPOINT + "/ext/"
10
+ TILEBOT_ENDPOINT = process.env.TILEBOT_ENDPOINT + "/ext/";
11
11
  }
12
-
13
- winston.debug("TILEBOT_ENDPOINT: " + TILEBOT_ENDPOINT);
12
+ winston.info("[tilebot/listener] TILEBOT_ENDPOINT: " + TILEBOT_ENDPOINT);
14
13
 
15
14
  /**
16
15
  * process.env.API_ENDPOINT --> Internal url of the server inside the cluster
@@ -34,11 +34,12 @@ const uuidv4 = require('uuid/v4');
34
34
  var jwt = require('jsonwebtoken');
35
35
 
36
36
  const port = process.env.PORT || '3000';
37
- let TILEBOT_ENDPOINT = "http://localhost:" + port + "/modules/tilebot/ext/";;
37
+ let TILEBOT_ENDPOINT = process.env.API_ENDPOINT ? process.env.API_ENDPOINT + "/modules/tilebot/ext/" : ("http://localhost:" + port + "/modules/tilebot/ext/");
38
+
38
39
  if (process.env.TILEBOT_ENDPOINT) {
39
- TILEBOT_ENDPOINT = process.env.TILEBOT_ENDPOINT + "/ext/"
40
+ TILEBOT_ENDPOINT = process.env.TILEBOT_ENDPOINT + "/ext/";
40
41
  }
41
- winston.debug("TILEBOT_ENDPOINT: " + TILEBOT_ENDPOINT);
42
+ winston.info("[rulesTrigger] TILEBOT_ENDPOINT: " + TILEBOT_ENDPOINT);
42
43
 
43
44
  class RulesTrigger {
44
45
 
package/routes/faq_kb.js CHANGED
@@ -284,11 +284,12 @@ router.put('/:faq_kbid/publish', roleChecker.hasRole('admin'), async (req, res)
284
284
  botEvent.emit('faqbot.update', updatedForkedChabot);
285
285
 
286
286
  const port = process.env.PORT || '3000';
287
- let TILEBOT_ENDPOINT = "http://localhost:" + port + "/modules/tilebot/ext/";;
287
+ let TILEBOT_ENDPOINT = process.env.API_ENDPOINT ? process.env.API_ENDPOINT + "/modules/tilebot/ext/" : ("http://localhost:" + port + "/modules/tilebot/ext/");
288
+
288
289
  if (process.env.TILEBOT_ENDPOINT) {
289
- TILEBOT_ENDPOINT = process.env.TILEBOT_ENDPOINT + "/ext/"
290
+ TILEBOT_ENDPOINT = process.env.TILEBOT_ENDPOINT + "/ext/";
290
291
  }
291
- winston.debug("TILEBOT_ENDPOINT: " + TILEBOT_ENDPOINT);
292
+ winston.info("[RequestService] TILEBOT_ENDPOINT: " + TILEBOT_ENDPOINT);
292
293
 
293
294
  let updatedOriginalChabot = await Faq_kb.findByIdAndUpdate(id_faq_kb, { url: TILEBOT_ENDPOINT + forkedChatBotId }, { new: true, upsert: true }).exec();
294
295
  winston.debug("updatedOriginalChabot: ", updatedOriginalChabot);
package/routes/mcp.js CHANGED
@@ -2,36 +2,125 @@ let express = require('express');
2
2
  let router = express.Router();
3
3
  let winston = require('../config/winston');
4
4
  const mcpService = require('../services/mcpService');
5
- const Project = require('../models/project');
5
+
6
+
7
+ /**
8
+ * GET /mcp/native
9
+ * Returns the list of available native MCP servers (without URLs)
10
+ */
11
+ router.get('/native', async (req, res) => {
12
+ try {
13
+ const servers = await mcpService.getNativeServersPublic();
14
+ res.status(200).send(servers);
15
+ } catch (error) {
16
+ winston.error('Error getting native MCP servers:', error);
17
+ res.status(500).send({ success: false, error: error.message || 'Error getting native MCP servers' });
18
+ }
19
+ });
20
+
21
+ /**
22
+ * POST /mcp/native/:nativeId/connect
23
+ * Initializes a connection to a native MCP server
24
+ */
25
+ router.post('/native/:nativeId/connect', async (req, res) => {
26
+ try {
27
+ const id_project = req.projectid;
28
+ const { nativeId } = req.params;
29
+
30
+ const serverConfig = await mcpService.resolveNativeServerConfig(nativeId, id_project);
31
+ const result = await mcpService.connectServer(serverConfig);
32
+
33
+ res.status(200).send({
34
+ success: true,
35
+ message: 'Native MCP server connected successfully',
36
+ nativeId,
37
+ capabilities: result.capabilities,
38
+ tools: result.tools
39
+ });
40
+ } catch (error) {
41
+ winston.error('Error connecting to native MCP server:', error);
42
+ res.status(error.statusCode || 500).send({
43
+ success: false,
44
+ error: error.message || 'Error connecting to native MCP server'
45
+ });
46
+ }
47
+ });
48
+
49
+ /**
50
+ * POST /mcp/native/:nativeId/tools
51
+ * Gets the list of tools from a native MCP server
52
+ */
53
+ router.post('/native/:nativeId/tools', async (req, res) => {
54
+ try {
55
+ const id_project = req.projectid;
56
+ const { nativeId } = req.params;
57
+
58
+ const serverConfig = await mcpService.resolveNativeServerConfig(nativeId, id_project);
59
+ const tools = await mcpService.listTools(serverConfig);
60
+
61
+ res.status(200).send(tools);
62
+ } catch (error) {
63
+ winston.error('Error getting tools from native MCP server:', error);
64
+ res.status(error.statusCode || 500).send({
65
+ success: false,
66
+ error: error.message || 'Error getting tools from native MCP server'
67
+ });
68
+ }
69
+ });
70
+
71
+ /**
72
+ * POST /mcp/servers
73
+ * Adds or updates an MCP server in the project mcp integration
74
+ * Body (native): { type: 'native', nativeId, tools?, selectedTools? }
75
+ * Body (custom): { type: 'custom', name, url, transport?, customHeaders?, tools?, selectedTools?, auth? }
76
+ */
77
+ router.post('/servers', async (req, res) => {
78
+ try {
79
+ const id_project = req.projectid;
80
+ const result = await mcpService.upsertMcpServerInIntegration(id_project, req.body);
81
+
82
+ res.status(200).send({
83
+ success: true,
84
+ server: result.server,
85
+ integration: result.integration
86
+ });
87
+ } catch (error) {
88
+ winston.error('Error saving MCP server in integration:', error);
89
+ res.status(error.statusCode || 500).send({
90
+ success: false,
91
+ error: error.message || 'Error saving MCP server in integration'
92
+ });
93
+ }
94
+ });
6
95
 
7
96
  /**
8
97
  * POST /mcp/connect
9
98
  * Initializes a connection to an MCP server
10
- * Body: { url: string, auth?: { type: 'bearer'|'api_key'|'basic', token?: string, key?: string, username?: string, password?: string } }
99
+ * Body: {
100
+ * url: string,
101
+ * customHeaders?: [{ key: string, value: string }],
102
+ * oauth?: object (not supported yet),
103
+ * auth?: object (legacy, optional)
104
+ * }
11
105
  */
12
106
  router.post('/connect', async (req, res) => {
13
107
  try {
14
108
  const id_project = req.projectid;
15
- const { url, auth } = req.body;
109
+ let serverConfig;
16
110
 
17
- if (!url) {
18
- return res.status(400).send({ success: false, error: "Missing required parameter 'url'" });
111
+ try {
112
+ serverConfig = mcpService.buildServerConfig(id_project, req.body);
113
+ } catch (error) {
114
+ return res.status(400).send({ success: false, error: error.message });
19
115
  }
20
116
 
21
- // Build server configuration
22
- const serverConfig = {
23
- url: url,
24
- projectId: id_project,
25
- auth: auth || undefined
26
- };
27
-
28
- // Initialize the connection
29
- const result = await mcpService.initializeServer(serverConfig);
117
+ const result = await mcpService.connectServer(serverConfig);
30
118
 
31
- res.status(200).send({
32
- success: true,
119
+ res.status(200).send({
120
+ success: true,
33
121
  message: 'MCP server connected successfully',
34
- capabilities: result?.capabilities || {}
122
+ capabilities: result.capabilities,
123
+ tools: result.tools
35
124
  });
36
125
  } catch (error) {
37
126
  winston.error(`Error connecting to MCP server:`, error);
@@ -42,25 +131,27 @@ router.post('/connect', async (req, res) => {
42
131
  /**
43
132
  * POST /mcp/tools
44
133
  * Gets the list of tools from an MCP server
45
- * Body: { url: string, auth?: object }
134
+ * Body: {
135
+ * url: string,
136
+ * customHeaders?: [{ key: string, value: string }],
137
+ * oauth?: object (not supported yet),
138
+ * auth?: object (legacy, optional)
139
+ * }
46
140
  */
47
141
  router.post('/tools', async (req, res) => {
48
142
  try {
49
143
  const id_project = req.projectid;
50
- const { url, auth } = req.body;
144
+ const { url } = req.body;
51
145
 
52
- if (!url) {
53
- return res.status(400).send({ success: false, error: "Missing required parameter 'url' in body" });
54
- }
146
+ winston.info(`MCP /tools called for project ${id_project}, url: ${url}`);
55
147
 
56
- // Build server configuration
57
- const serverConfig = {
58
- url: url,
59
- projectId: id_project,
60
- auth: auth || undefined
61
- };
148
+ let serverConfig;
149
+ try {
150
+ serverConfig = mcpService.buildServerConfig(id_project, req.body);
151
+ } catch (error) {
152
+ return res.status(400).send({ success: false, error: error.message });
153
+ }
62
154
 
63
- // listTools automatically initializes if necessary
64
155
  const tools = await mcpService.listTools(serverConfig);
65
156
 
66
157
  res.status(200).send(tools);
@@ -71,4 +162,3 @@ router.post('/tools', async (req, res) => {
71
162
  });
72
163
 
73
164
  module.exports = router;
74
-
package/routes/webhook.js CHANGED
@@ -11,14 +11,6 @@ const errorCodes = require('../errorCodes');
11
11
  const aiManager = require('../services/aiManager');
12
12
  var ObjectId = require('mongoose').Types.ObjectId;
13
13
  const default_embedding = require('../config/kb/embedding');
14
-
15
- const port = process.env.PORT || '3000';
16
- let TILEBOT_ENDPOINT = "http://localhost:" + port + "/modules/tilebot/";
17
- if (process.env.TILEBOT_ENDPOINT) {
18
- TILEBOT_ENDPOINT = process.env.TILEBOT_ENDPOINT + "/"
19
- }
20
- winston.debug("TILEBOT_ENDPOINT: " + TILEBOT_ENDPOINT);
21
-
22
14
  const KB_WEBHOOK_TOKEN = process.env.KB_WEBHOOK_TOKEN || 'kbcustomtoken';
23
15
  const AMQP_MANAGER_URL = process.env.AMQP_MANAGER_URL;
24
16
  const JOB_TOPIC_EXCHANGE = process.env.JOB_TOPIC_EXCHANGE_TRAIN || 'tiledesk-trainer';
@@ -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
-
@@ -22,10 +22,12 @@ const projectService = require('./projectService');
22
22
  const axios = require("axios").default;
23
23
 
24
24
  let port = process.env.PORT || '3000';
25
- let TILEBOT_ENDPOINT = "http://localhost:" + port + "/modules/tilebot/ext/";;
25
+ let TILEBOT_ENDPOINT = process.env.API_ENDPOINT ? process.env.API_ENDPOINT + "/modules/tilebot/ext/" : ("http://localhost:" + port + "/modules/tilebot/ext/");
26
+
26
27
  if (process.env.TILEBOT_ENDPOINT) {
27
- TILEBOT_ENDPOINT = process.env.TILEBOT_ENDPOINT + "/ext/"
28
+ TILEBOT_ENDPOINT = process.env.TILEBOT_ENDPOINT + "/ext/";
28
29
  }
30
+ winston.info("[RequestService] TILEBOT_ENDPOINT: " + TILEBOT_ENDPOINT);
29
31
 
30
32
  let tdCache = new TdCache({
31
33
  host: process.env.CACHE_REDIS_HOST,
@@ -7,11 +7,12 @@ const errorCodes = require("../errorCodes");
7
7
  var ObjectId = require('mongoose').Types.ObjectId;
8
8
 
9
9
  const port = process.env.PORT || '3000';
10
- let TILEBOT_ENDPOINT = "http://localhost:" + port + "/modules/tilebot/";;
10
+ let TILEBOT_ENDPOINT = process.env.API_ENDPOINT ? process.env.API_ENDPOINT + "/modules/tilebot/" : ("http://localhost:" + port + "/modules/tilebot/");
11
+
11
12
  if (process.env.TILEBOT_ENDPOINT) {
12
- TILEBOT_ENDPOINT = process.env.TILEBOT_ENDPOINT + "/"
13
+ TILEBOT_ENDPOINT = process.env.TILEBOT_ENDPOINT + "/";
13
14
  }
14
- winston.debug("TILEBOT_ENDPOINT: " + TILEBOT_ENDPOINT);
15
+ winston.info("[RequestService] TILEBOT_ENDPOINT: " + TILEBOT_ENDPOINT);
15
16
 
16
17
  class WebhookService {
17
18
 
@@ -0,0 +1,167 @@
1
+ //During the test the env variable is set to test
2
+ process.env.NODE_ENV = 'test';
3
+
4
+ const projectService = require('../services/projectService');
5
+ const userService = require('../services/userService');
6
+
7
+ //Require the dev-dependencies
8
+ let chai = require('chai');
9
+ let chaiHttp = require('chai-http');
10
+ let server = require('../app');
11
+ let should = chai.should();
12
+ const expect = chai.expect;
13
+
14
+ let log = false;
15
+
16
+ chai.use(chaiHttp);
17
+
18
+ function createProjectUser(done, callback) {
19
+ const email = "test-signup-" + Date.now() + "@email.com";
20
+ const pwd = "pwd";
21
+
22
+ userService.signup(email, pwd, "Test Firstname", "Test Lastname").then((savedUser) => {
23
+ projectService.create("test-native-mcp", savedUser._id).then((savedProject) => {
24
+ callback(savedProject, email, pwd, done);
25
+ });
26
+ });
27
+ }
28
+
29
+ describe('NativeMCP', () => {
30
+
31
+ it('get-native-mcp-servers-list', (done) => {
32
+ createProjectUser(done, (savedProject, email, pwd, done) => {
33
+ chai.request(server)
34
+ .get('/' + savedProject._id + '/mcp/native')
35
+ .auth(email, pwd)
36
+ .end((err, res) => {
37
+
38
+ if (err) { console.error("err: ", err); }
39
+ if (log) { console.log("res.body: ", res.body); }
40
+
41
+ res.should.have.status(200);
42
+ res.body.should.be.an('array');
43
+ res.body.should.have.length(2);
44
+
45
+ expect(res.body[0].id).to.equal("tiledesk-communicator");
46
+ expect(res.body[0].name).to.equal("Tiledesk Communicator");
47
+ expect(res.body[0].native).to.equal(true);
48
+ expect(res.body[0].transport).to.equal("streamable_http");
49
+ expect(res.body[0].url).to.not.exist;
50
+ expect(res.body[1].id).to.equal("tiledesk-data-table");
51
+ expect(res.body[1].name).to.equal("Tiledesk Data Table");
52
+ expect(res.body[1].native).to.equal(true);
53
+ expect(res.body[1].transport).to.equal("streamable_http");
54
+ expect(res.body[1].url).to.not.exist;
55
+
56
+ done();
57
+ });
58
+ });
59
+ });
60
+
61
+ it('connect-native-mcp-server-not-found', (done) => {
62
+ createProjectUser(done, (savedProject, email, pwd, done) => {
63
+ chai.request(server)
64
+ .post('/' + savedProject._id + '/mcp/native/unknown-server/connect')
65
+ .auth(email, pwd)
66
+ .end((err, res) => {
67
+
68
+ if (err) { console.error("err: ", err); }
69
+ if (log) { console.log("connect res.body: ", res.body); }
70
+
71
+ res.should.have.status(404);
72
+ res.body.should.have.property('success', false);
73
+ done();
74
+ });
75
+ });
76
+ });
77
+
78
+ it('connect-native-mcp-server', (done) => {
79
+ createProjectUser(done, (savedProject, email, pwd, done) => {
80
+ const serverId = 'tiledesk-communicator';
81
+
82
+ chai.request(server)
83
+ .post('/' + savedProject._id + '/mcp/native/' + serverId + '/connect')
84
+ .auth(email, pwd)
85
+ .end((err, connectRes) => {
86
+
87
+ if (err) { console.error("err: ", err); }
88
+ if (log) { console.log("connect res.body: ", connectRes.body); }
89
+
90
+ connectRes.should.have.status(200);
91
+ expect(connectRes.body).to.be.an('object');
92
+ expect(connectRes.body.success).to.equal(true);
93
+ expect(connectRes.body.message).to.equal('Native MCP server connected successfully');
94
+ expect(connectRes.body.nativeId).to.equal(serverId);
95
+ connectRes.body.tools.should.be.an('array');
96
+ connectRes.body.tools.length.should.be.greaterThan(0);
97
+ connectRes.body.tools[0].should.have.property('name');
98
+ connectRes.body.tools.map((tool) => tool.name).should.include('reply_to_user');
99
+
100
+ done();
101
+ });
102
+ });
103
+ });
104
+
105
+ it('save-native-mcp-server-to-integration', (done) => {
106
+ createProjectUser(done, (savedProject, email, pwd, done) => {
107
+
108
+ let serverId = 'tiledesk-communicator';
109
+ chai.request(server)
110
+ .post('/' + savedProject._id + '/mcp/native/' + serverId + '/tools')
111
+ .auth(email, pwd)
112
+ .end((err, toolsRes) => {
113
+
114
+ if (err) { console.error("err: ", err); }
115
+ if (log) { console.log("tools res.body: ", toolsRes.body); }
116
+
117
+ toolsRes.should.have.status(200);
118
+ expect(toolsRes.body).to.be.an('array');
119
+ const totalTools = toolsRes.body.length;
120
+ const tools = toolsRes.body.slice(0, totalTools - 2);
121
+
122
+ chai.request(server)
123
+ .post('/' + savedProject._id + '/mcp/servers')
124
+ .auth(email, pwd)
125
+ .send({
126
+ type: 'native',
127
+ nativeId: 'tiledesk-communicator',
128
+ tools,
129
+ selectedTools: [{ name: 'reply_to_user' }]
130
+ })
131
+ .end((err, res) => {
132
+
133
+ if (err) { console.error("err: ", err); }
134
+ if (log) { console.log("save res.body: ", res.body); }
135
+
136
+ res.should.have.status(200);
137
+ res.body.should.have.property('success', true);
138
+ res.body.server.should.have.property('type', 'native');
139
+ res.body.server.should.have.property('nativeId', 'tiledesk-communicator');
140
+ res.body.server.should.have.property('name', 'Tiledesk Communicator');
141
+ res.body.server.should.not.have.property('url');
142
+ res.body.server.tools.should.have.length(totalTools - 2);
143
+ res.body.server.selectedTools.should.deep.equal([{ name: 'reply_to_user' }]);
144
+
145
+ chai.request(server)
146
+ .get('/' + savedProject._id + '/integration/name/mcp')
147
+ .auth(email, pwd)
148
+ .end((err, integrationRes) => {
149
+
150
+ if (err) { console.error("err: ", err); }
151
+ if (log) { console.log("integration res.body: ", integrationRes.body); }
152
+
153
+ integrationRes.should.have.status(200);
154
+ integrationRes.body.should.have.property('name', 'mcp');
155
+ integrationRes.body.value.servers.should.be.an('array');
156
+ integrationRes.body.value.servers.should.have.length(1);
157
+ integrationRes.body.value.servers[0].should.have.property('nativeId', 'tiledesk-communicator');
158
+ integrationRes.body.value.servers[0].should.not.have.property('url');
159
+
160
+ done();
161
+ });
162
+ });
163
+ });
164
+ });
165
+ });
166
+
167
+ });