@app-connect/core 1.7.10 → 1.7.12

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 (59) hide show
  1. package/connector/developerPortal.js +43 -0
  2. package/connector/proxy/index.js +10 -3
  3. package/connector/registry.js +8 -6
  4. package/handlers/admin.js +135 -22
  5. package/handlers/auth.js +89 -67
  6. package/handlers/calldown.js +10 -4
  7. package/handlers/contact.js +4 -104
  8. package/handlers/disposition.js +7 -145
  9. package/handlers/log.js +174 -258
  10. package/handlers/user.js +19 -6
  11. package/index.js +280 -47
  12. package/lib/analytics.js +3 -1
  13. package/lib/authSession.js +68 -0
  14. package/lib/callLogComposer.js +498 -420
  15. package/lib/errorHandler.js +206 -0
  16. package/lib/jwt.js +2 -0
  17. package/lib/logger.js +190 -0
  18. package/lib/oauth.js +21 -10
  19. package/lib/ringcentral.js +2 -10
  20. package/lib/sharedSMSComposer.js +471 -0
  21. package/mcp/SupportedPlatforms.md +12 -0
  22. package/mcp/lib/validator.js +91 -0
  23. package/mcp/mcpHandler.js +166 -0
  24. package/mcp/tools/checkAuthStatus.js +110 -0
  25. package/mcp/tools/collectAuthInfo.js +91 -0
  26. package/mcp/tools/createCallLog.js +308 -0
  27. package/mcp/tools/createContact.js +117 -0
  28. package/mcp/tools/createMessageLog.js +283 -0
  29. package/mcp/tools/doAuth.js +190 -0
  30. package/mcp/tools/findContactByName.js +92 -0
  31. package/mcp/tools/findContactByPhone.js +101 -0
  32. package/mcp/tools/getCallLog.js +98 -0
  33. package/mcp/tools/getGoogleFilePicker.js +103 -0
  34. package/mcp/tools/getHelp.js +44 -0
  35. package/mcp/tools/getPublicConnectors.js +53 -0
  36. package/mcp/tools/index.js +64 -0
  37. package/mcp/tools/logout.js +68 -0
  38. package/mcp/tools/rcGetCallLogs.js +78 -0
  39. package/mcp/tools/setConnector.js +69 -0
  40. package/mcp/tools/updateCallLog.js +122 -0
  41. package/models/cacheModel.js +3 -0
  42. package/package.json +71 -70
  43. package/releaseNotes.json +24 -0
  44. package/test/handlers/log.test.js +11 -4
  45. package/test/lib/logger.test.js +206 -0
  46. package/test/lib/ringcentral.test.js +0 -6
  47. package/test/lib/sharedSMSComposer.test.js +1084 -0
  48. package/test/mcp/tools/collectAuthInfo.test.js +234 -0
  49. package/test/mcp/tools/createCallLog.test.js +425 -0
  50. package/test/mcp/tools/createMessageLog.test.js +580 -0
  51. package/test/mcp/tools/doAuth.test.js +376 -0
  52. package/test/mcp/tools/findContactByName.test.js +263 -0
  53. package/test/mcp/tools/findContactByPhone.test.js +284 -0
  54. package/test/mcp/tools/getCallLog.test.js +286 -0
  55. package/test/mcp/tools/getGoogleFilePicker.test.js +281 -0
  56. package/test/mcp/tools/getPublicConnectors.test.js +128 -0
  57. package/test/mcp/tools/logout.test.js +169 -0
  58. package/test/mcp/tools/setConnector.test.js +177 -0
  59. package/test/mcp/tools/updateCallLog.test.js +346 -0
@@ -0,0 +1,281 @@
1
+ const getGoogleFilePicker = require('../../../mcp/tools/getGoogleFilePicker');
2
+ const jwt = require('../../../lib/jwt');
3
+ const { UserModel } = require('../../../models/userModel');
4
+ const axios = require('axios');
5
+
6
+ // Mock dependencies
7
+ jest.mock('../../../lib/jwt');
8
+ jest.mock('../../../models/userModel');
9
+ jest.mock('axios');
10
+
11
+ describe('MCP Tool: getGoogleFilePicker', () => {
12
+ beforeEach(() => {
13
+ jest.clearAllMocks();
14
+ process.env.APP_SERVER = 'https://test-app-server.com';
15
+ });
16
+
17
+ describe('tool definition', () => {
18
+ test('should have correct tool definition', () => {
19
+ expect(getGoogleFilePicker.definition).toBeDefined();
20
+ expect(getGoogleFilePicker.definition.name).toBe('getGoogleFilePicker');
21
+ expect(getGoogleFilePicker.definition.description).toContain('REQUIRES AUTHENTICATION');
22
+ expect(getGoogleFilePicker.definition.description).toContain('Google Sheets file picker');
23
+ expect(getGoogleFilePicker.definition.inputSchema).toBeDefined();
24
+ });
25
+
26
+ test('should require jwtToken parameter', () => {
27
+ expect(getGoogleFilePicker.definition.inputSchema.required).toContain('jwtToken');
28
+ });
29
+
30
+ test('should have optional sheetName parameter', () => {
31
+ expect(getGoogleFilePicker.definition.inputSchema.properties).toHaveProperty('sheetName');
32
+ expect(getGoogleFilePicker.definition.inputSchema.required).not.toContain('sheetName');
33
+ });
34
+
35
+ test('should have correct annotations', () => {
36
+ expect(getGoogleFilePicker.definition.annotations).toEqual({
37
+ readOnlyHint: false,
38
+ openWorldHint: true,
39
+ destructiveHint: false
40
+ });
41
+ });
42
+ });
43
+
44
+ describe('execute - file picker URL', () => {
45
+ test('should return file picker URL successfully', async () => {
46
+ // Arrange
47
+ jwt.decodeJwt.mockReturnValue({
48
+ id: 'user-123'
49
+ });
50
+
51
+ UserModel.findByPk.mockResolvedValue({
52
+ id: 'user-123',
53
+ name: 'Test User'
54
+ });
55
+
56
+ // Act
57
+ const result = await getGoogleFilePicker.execute({
58
+ jwtToken: 'mock-jwt-token'
59
+ });
60
+
61
+ // Assert
62
+ expect(result).toEqual({
63
+ success: true,
64
+ data: {
65
+ filePickerUrl: 'https://test-app-server.com/googleSheets/filePicker?token=mock-jwt-token}',
66
+ message: expect.stringContaining('Please open this URL')
67
+ }
68
+ });
69
+ expect(jwt.decodeJwt).toHaveBeenCalledWith('mock-jwt-token');
70
+ expect(UserModel.findByPk).toHaveBeenCalledWith('user-123');
71
+ });
72
+ });
73
+
74
+ describe('execute - create new sheet', () => {
75
+ test('should create new sheet when sheetName is provided', async () => {
76
+ // Arrange
77
+ jwt.decodeJwt.mockReturnValue({
78
+ id: 'user-123'
79
+ });
80
+
81
+ UserModel.findByPk.mockResolvedValue({
82
+ id: 'user-123',
83
+ name: 'Test User'
84
+ });
85
+
86
+ const mockSheetResponse = {
87
+ data: {
88
+ success: true,
89
+ data: {
90
+ sheetId: 'sheet-123',
91
+ sheetName: 'My Call Logs'
92
+ }
93
+ }
94
+ };
95
+
96
+ axios.post.mockResolvedValue(mockSheetResponse);
97
+
98
+ // Act
99
+ const result = await getGoogleFilePicker.execute({
100
+ jwtToken: 'mock-jwt-token',
101
+ sheetName: 'My Call Logs'
102
+ });
103
+
104
+ // Assert
105
+ expect(result).toEqual({
106
+ success: true,
107
+ data: {
108
+ sheetId: 'sheet-123',
109
+ sheetName: 'My Call Logs'
110
+ }
111
+ });
112
+ expect(axios.post).toHaveBeenCalledWith(
113
+ 'https://test-app-server.com/googleSheets/sheet?jwtToken=mock-jwt-token',
114
+ { name: 'My Call Logs' }
115
+ );
116
+ });
117
+
118
+ test('should return error when sheet creation fails', async () => {
119
+ // Arrange
120
+ jwt.decodeJwt.mockReturnValue({
121
+ id: 'user-123'
122
+ });
123
+
124
+ UserModel.findByPk.mockResolvedValue({
125
+ id: 'user-123',
126
+ name: 'Test User'
127
+ });
128
+
129
+ axios.post.mockRejectedValue(new Error('Failed to create sheet'));
130
+
131
+ // Act
132
+ const result = await getGoogleFilePicker.execute({
133
+ jwtToken: 'mock-jwt-token',
134
+ sheetName: 'My Call Logs'
135
+ });
136
+
137
+ // Assert
138
+ expect(result.success).toBe(false);
139
+ expect(result.error).toBe('Failed to create sheet');
140
+ expect(result.errorDetails).toBeDefined();
141
+ });
142
+ });
143
+
144
+ describe('execute - error handling', () => {
145
+ test('should return error when jwtToken is missing', async () => {
146
+ // Act
147
+ const result = await getGoogleFilePicker.execute({});
148
+
149
+ // Assert
150
+ expect(result).toEqual({
151
+ success: false,
152
+ error: 'JWT token is required. Please authenticate with googleSheets platform first using the doAuth tool.'
153
+ });
154
+ });
155
+
156
+ test('should return error when jwtToken is null', async () => {
157
+ // Act
158
+ const result = await getGoogleFilePicker.execute({
159
+ jwtToken: null
160
+ });
161
+
162
+ // Assert
163
+ expect(result).toEqual({
164
+ success: false,
165
+ error: 'JWT token is required. Please authenticate with googleSheets platform first using the doAuth tool.'
166
+ });
167
+ });
168
+
169
+ test('should return error when JWT is invalid (no userId)', async () => {
170
+ // Arrange
171
+ jwt.decodeJwt.mockReturnValue({
172
+ // id is missing
173
+ platform: 'googleSheets'
174
+ });
175
+
176
+ // Act
177
+ const result = await getGoogleFilePicker.execute({
178
+ jwtToken: 'invalid-jwt-token'
179
+ });
180
+
181
+ // Assert
182
+ expect(result).toEqual({
183
+ success: false,
184
+ error: 'Invalid JWT token: userId not found'
185
+ });
186
+ });
187
+
188
+ test('should return error when JWT decode returns null', async () => {
189
+ // Arrange
190
+ jwt.decodeJwt.mockReturnValue(null);
191
+
192
+ // Act
193
+ const result = await getGoogleFilePicker.execute({
194
+ jwtToken: 'malformed-jwt-token'
195
+ });
196
+
197
+ // Assert
198
+ expect(result).toEqual({
199
+ success: false,
200
+ error: 'Invalid JWT token: userId not found'
201
+ });
202
+ });
203
+
204
+ test('should return error when user not found', async () => {
205
+ // Arrange
206
+ jwt.decodeJwt.mockReturnValue({
207
+ id: 'nonexistent-user'
208
+ });
209
+
210
+ UserModel.findByPk.mockResolvedValue(null);
211
+
212
+ // Act
213
+ const result = await getGoogleFilePicker.execute({
214
+ jwtToken: 'mock-jwt-token'
215
+ });
216
+
217
+ // Assert
218
+ expect(result).toEqual({
219
+ success: false,
220
+ error: 'User not found. Please authenticate with googleSheets platform first.'
221
+ });
222
+ expect(UserModel.findByPk).toHaveBeenCalledWith('nonexistent-user');
223
+ });
224
+
225
+ test('should handle database errors gracefully', async () => {
226
+ // Arrange
227
+ jwt.decodeJwt.mockReturnValue({
228
+ id: 'user-123'
229
+ });
230
+
231
+ UserModel.findByPk.mockRejectedValue(new Error('Database connection failed'));
232
+
233
+ // Act
234
+ const result = await getGoogleFilePicker.execute({
235
+ jwtToken: 'mock-jwt-token'
236
+ });
237
+
238
+ // Assert
239
+ expect(result.success).toBe(false);
240
+ expect(result.error).toBe('Database connection failed');
241
+ expect(result.errorDetails).toBeDefined();
242
+ });
243
+
244
+ test('should handle JWT decode errors gracefully', async () => {
245
+ // Arrange
246
+ jwt.decodeJwt.mockImplementation(() => {
247
+ throw new Error('JWT decode error');
248
+ });
249
+
250
+ // Act
251
+ const result = await getGoogleFilePicker.execute({
252
+ jwtToken: 'corrupted-jwt-token'
253
+ });
254
+
255
+ // Assert
256
+ expect(result.success).toBe(false);
257
+ expect(result.error).toBe('JWT decode error');
258
+ expect(result.errorDetails).toBeDefined();
259
+ });
260
+
261
+ test('should handle errors without message property', async () => {
262
+ // Arrange
263
+ jwt.decodeJwt.mockReturnValue({
264
+ id: 'user-123'
265
+ });
266
+
267
+ const errorWithoutMessage = { code: 'UNKNOWN_ERROR' };
268
+ UserModel.findByPk.mockRejectedValue(errorWithoutMessage);
269
+
270
+ // Act
271
+ const result = await getGoogleFilePicker.execute({
272
+ jwtToken: 'mock-jwt-token'
273
+ });
274
+
275
+ // Assert
276
+ expect(result.success).toBe(false);
277
+ expect(result.error).toBe('Unknown error occurred');
278
+ });
279
+ });
280
+ });
281
+
@@ -0,0 +1,128 @@
1
+ const getPublicConnectors = require('../../../mcp/tools/getPublicConnectors');
2
+ const developerPortal = require('../../../connector/developerPortal');
3
+
4
+ // Mock the developerPortal module
5
+ jest.mock('../../../connector/developerPortal');
6
+
7
+ describe('MCP Tool: getPublicConnectors', () => {
8
+ beforeEach(() => {
9
+ jest.clearAllMocks();
10
+ delete process.env.RC_ACCOUNT_ID;
11
+ });
12
+
13
+ describe('tool definition', () => {
14
+ test('should have correct tool definition', () => {
15
+ expect(getPublicConnectors.definition).toBeDefined();
16
+ expect(getPublicConnectors.definition.name).toBe('getPublicConnectors');
17
+ expect(getPublicConnectors.definition.description).toContain('Auth flow step.1');
18
+ expect(getPublicConnectors.definition.inputSchema).toBeDefined();
19
+ expect(getPublicConnectors.definition.inputSchema.type).toBe('object');
20
+ });
21
+
22
+ test('should have no required parameters', () => {
23
+ expect(getPublicConnectors.definition.inputSchema.required).toEqual([]);
24
+ });
25
+ });
26
+
27
+ describe('execute', () => {
28
+ test('should return public connectors successfully', async () => {
29
+ // Arrange - use supported platform names: 'googleSheets' and 'clio'
30
+ const mockConnectors = [
31
+ { id: '1', name: 'googleSheets', displayName: 'Google Sheets' },
32
+ { id: '2', name: 'clio', displayName: 'Clio' }
33
+ ];
34
+
35
+ developerPortal.getPublicConnectorList.mockResolvedValue({
36
+ connectors: mockConnectors
37
+ });
38
+
39
+ // Act
40
+ const result = await getPublicConnectors.execute();
41
+
42
+ // Assert
43
+ expect(result).toEqual({
44
+ success: true,
45
+ data: ['Google Sheets', 'Clio']
46
+ });
47
+ expect(developerPortal.getPublicConnectorList).toHaveBeenCalledTimes(1);
48
+ });
49
+
50
+ test('should include private connectors when RC_ACCOUNT_ID is set', async () => {
51
+ // Arrange - use supported platform names
52
+ process.env.RC_ACCOUNT_ID = 'test-account-id';
53
+
54
+ const mockPublicConnectors = [
55
+ { id: '1', name: 'googleSheets', displayName: 'Google Sheets' }
56
+ ];
57
+ const mockPrivateConnectors = [
58
+ { id: '3', name: 'clio', displayName: 'Clio' }
59
+ ];
60
+
61
+ developerPortal.getPublicConnectorList.mockResolvedValue({
62
+ connectors: mockPublicConnectors
63
+ });
64
+ developerPortal.getPrivateConnectorList.mockResolvedValue({
65
+ privateConnectors: mockPrivateConnectors
66
+ });
67
+
68
+ // Act
69
+ const result = await getPublicConnectors.execute();
70
+
71
+ // Assert
72
+ expect(result).toEqual({
73
+ success: true,
74
+ data: ['Google Sheets', 'Clio']
75
+ });
76
+ expect(developerPortal.getPublicConnectorList).toHaveBeenCalledTimes(1);
77
+ expect(developerPortal.getPrivateConnectorList).toHaveBeenCalledTimes(1);
78
+ });
79
+
80
+ test('should return empty array when no connectors available', async () => {
81
+ // Arrange
82
+ developerPortal.getPublicConnectorList.mockResolvedValue({
83
+ connectors: []
84
+ });
85
+
86
+ // Act
87
+ const result = await getPublicConnectors.execute();
88
+
89
+ // Assert
90
+ expect(result).toEqual({
91
+ success: true,
92
+ data: []
93
+ });
94
+ });
95
+
96
+ test('should handle errors gracefully', async () => {
97
+ // Arrange
98
+ const errorMessage = 'Failed to fetch connectors';
99
+ developerPortal.getPublicConnectorList.mockRejectedValue(
100
+ new Error(errorMessage)
101
+ );
102
+
103
+ // Act
104
+ const result = await getPublicConnectors.execute();
105
+
106
+ // Assert
107
+ expect(result.success).toBe(false);
108
+ expect(result.error).toBe(errorMessage);
109
+ expect(result.errorDetails).toBeDefined();
110
+ });
111
+
112
+ test('should handle network errors', async () => {
113
+ // Arrange
114
+ const networkError = new Error('Network request failed');
115
+ networkError.code = 'ECONNREFUSED';
116
+ developerPortal.getPublicConnectorList.mockRejectedValue(networkError);
117
+
118
+ // Act
119
+ const result = await getPublicConnectors.execute();
120
+
121
+ // Assert
122
+ expect(result.success).toBe(false);
123
+ expect(result.error).toBe('Network request failed');
124
+ expect(result.errorDetails).toBeDefined();
125
+ });
126
+ });
127
+ });
128
+
@@ -0,0 +1,169 @@
1
+ const logout = require('../../../mcp/tools/logout');
2
+ const jwt = require('../../../lib/jwt');
3
+ const { UserModel } = require('../../../models/userModel');
4
+ const connectorRegistry = require('../../../connector/registry');
5
+
6
+ // Mock dependencies
7
+ jest.mock('../../../lib/jwt');
8
+ jest.mock('../../../models/userModel');
9
+ jest.mock('../../../connector/registry');
10
+
11
+ describe('MCP Tool: logout', () => {
12
+ beforeEach(() => {
13
+ jest.clearAllMocks();
14
+ });
15
+
16
+ describe('tool definition', () => {
17
+ test('should have correct tool definition', () => {
18
+ expect(logout.definition).toBeDefined();
19
+ expect(logout.definition.name).toBe('logout');
20
+ expect(logout.definition.description).toContain('Logout');
21
+ expect(logout.definition.inputSchema).toBeDefined();
22
+ });
23
+
24
+ test('should have jwtToken property', () => {
25
+ expect(logout.definition.inputSchema.properties).toHaveProperty('jwtToken');
26
+ });
27
+ });
28
+
29
+ describe('execute', () => {
30
+ test('should logout user successfully', async () => {
31
+ // Arrange
32
+ const mockUser = {
33
+ id: 'test-user-id',
34
+ platform: 'testCRM',
35
+ accessToken: 'test-access-token'
36
+ };
37
+
38
+ const mockConnector = {
39
+ unAuthorize: jest.fn().mockResolvedValue({
40
+ returnMessage: {
41
+ messageType: 'success',
42
+ message: 'Logged out successfully'
43
+ }
44
+ })
45
+ };
46
+
47
+ jwt.decodeJwt.mockReturnValue({
48
+ id: 'test-user-id',
49
+ platform: 'testCRM'
50
+ });
51
+
52
+ UserModel.findByPk.mockResolvedValue(mockUser);
53
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
54
+
55
+ // Act
56
+ const result = await logout.execute({
57
+ jwtToken: 'mock-jwt-token'
58
+ });
59
+
60
+ // Assert
61
+ expect(result).toEqual({
62
+ success: true,
63
+ data: {
64
+ message: expect.stringContaining('IMPORTANT')
65
+ }
66
+ });
67
+ expect(jwt.decodeJwt).toHaveBeenCalledWith('mock-jwt-token');
68
+ expect(UserModel.findByPk).toHaveBeenCalledWith('test-user-id');
69
+ expect(connectorRegistry.getConnector).toHaveBeenCalledWith('testCRM');
70
+ expect(mockConnector.unAuthorize).toHaveBeenCalledWith({
71
+ user: mockUser
72
+ });
73
+ });
74
+
75
+ test('should return error when user not found', async () => {
76
+ // Arrange
77
+ jwt.decodeJwt.mockReturnValue({
78
+ id: 'non-existent-user',
79
+ platform: 'testCRM'
80
+ });
81
+
82
+ UserModel.findByPk.mockResolvedValue(null);
83
+
84
+ // Act
85
+ const result = await logout.execute({
86
+ jwtToken: 'mock-jwt-token'
87
+ });
88
+
89
+ // Assert
90
+ expect(result).toEqual({
91
+ success: false,
92
+ error: 'User not found',
93
+ errorDetails: 'User not found'
94
+ });
95
+ });
96
+
97
+ test('should handle logout errors gracefully', async () => {
98
+ // Arrange
99
+ const mockUser = {
100
+ id: 'test-user-id',
101
+ platform: 'testCRM'
102
+ };
103
+
104
+ const mockConnector = {
105
+ unAuthorize: jest.fn().mockRejectedValue(
106
+ new Error('Logout API failed')
107
+ )
108
+ };
109
+
110
+ jwt.decodeJwt.mockReturnValue({
111
+ id: 'test-user-id',
112
+ platform: 'testCRM'
113
+ });
114
+
115
+ UserModel.findByPk.mockResolvedValue(mockUser);
116
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
117
+
118
+ // Act
119
+ const result = await logout.execute({
120
+ jwtToken: 'mock-jwt-token'
121
+ });
122
+
123
+ // Assert
124
+ expect(result.success).toBe(false);
125
+ expect(result.error).toBe('Logout API failed');
126
+ expect(result.errorDetails).toBeDefined();
127
+ });
128
+
129
+ test('should handle invalid JWT token', async () => {
130
+ // Arrange
131
+ jwt.decodeJwt.mockReturnValue(null);
132
+
133
+ // Act
134
+ const result = await logout.execute({
135
+ jwtToken: 'invalid-token'
136
+ });
137
+
138
+ // Assert
139
+ expect(result.success).toBe(false);
140
+ expect(result.error).toBeDefined();
141
+ });
142
+
143
+ test('should handle missing platform connector', async () => {
144
+ // Arrange
145
+ const mockUser = {
146
+ id: 'test-user-id',
147
+ platform: 'unknownCRM'
148
+ };
149
+
150
+ jwt.decodeJwt.mockReturnValue({
151
+ id: 'test-user-id',
152
+ platform: 'unknownCRM'
153
+ });
154
+
155
+ UserModel.findByPk.mockResolvedValue(mockUser);
156
+ connectorRegistry.getConnector.mockReturnValue(null);
157
+
158
+ // Act
159
+ const result = await logout.execute({
160
+ jwtToken: 'mock-jwt-token'
161
+ });
162
+
163
+ // Assert
164
+ expect(result.success).toBe(false);
165
+ expect(result.error).toBeDefined();
166
+ });
167
+ });
168
+ });
169
+