@app-connect/core 1.7.18 → 1.7.20

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 (60) hide show
  1. package/connector/proxy/index.js +2 -1
  2. package/handlers/auth.js +30 -55
  3. package/handlers/log.js +182 -10
  4. package/handlers/plugin.js +27 -0
  5. package/handlers/user.js +31 -2
  6. package/index.js +115 -22
  7. package/lib/authSession.js +21 -12
  8. package/lib/callLogComposer.js +1 -1
  9. package/lib/debugTracer.js +20 -2
  10. package/lib/util.js +21 -4
  11. package/mcp/README.md +395 -0
  12. package/mcp/mcpHandler.js +318 -82
  13. package/mcp/tools/checkAuthStatus.js +28 -35
  14. package/mcp/tools/createCallLog.js +13 -9
  15. package/mcp/tools/createContact.js +2 -6
  16. package/mcp/tools/doAuth.js +27 -157
  17. package/mcp/tools/findContactByName.js +6 -9
  18. package/mcp/tools/findContactByPhone.js +2 -6
  19. package/mcp/tools/getGoogleFilePicker.js +5 -9
  20. package/mcp/tools/getHelp.js +2 -3
  21. package/mcp/tools/getPublicConnectors.js +55 -24
  22. package/mcp/tools/index.js +11 -36
  23. package/mcp/tools/logout.js +32 -13
  24. package/mcp/tools/rcGetCallLogs.js +3 -20
  25. package/mcp/ui/App/App.tsx +358 -0
  26. package/mcp/ui/App/components/AuthInfoForm.tsx +113 -0
  27. package/mcp/ui/App/components/AuthSuccess.tsx +22 -0
  28. package/mcp/ui/App/components/ConnectorList.tsx +82 -0
  29. package/mcp/ui/App/components/DebugPanel.tsx +43 -0
  30. package/mcp/ui/App/components/OAuthConnect.tsx +270 -0
  31. package/mcp/ui/App/lib/callTool.ts +130 -0
  32. package/mcp/ui/App/lib/debugLog.ts +41 -0
  33. package/mcp/ui/App/lib/developerPortal.ts +111 -0
  34. package/mcp/ui/App/main.css +6 -0
  35. package/mcp/ui/App/root.tsx +13 -0
  36. package/mcp/ui/dist/index.html +53 -0
  37. package/mcp/ui/index.html +13 -0
  38. package/mcp/ui/package-lock.json +6356 -0
  39. package/mcp/ui/package.json +25 -0
  40. package/mcp/ui/tsconfig.json +26 -0
  41. package/mcp/ui/vite.config.ts +16 -0
  42. package/models/llmSessionModel.js +14 -0
  43. package/models/userModel.js +3 -0
  44. package/package.json +2 -2
  45. package/releaseNotes.json +24 -0
  46. package/test/handlers/auth.test.js +31 -0
  47. package/test/handlers/plugin.test.js +287 -0
  48. package/test/lib/util.test.js +379 -1
  49. package/test/mcp/tools/createCallLog.test.js +3 -3
  50. package/test/mcp/tools/doAuth.test.js +40 -303
  51. package/test/mcp/tools/findContactByName.test.js +3 -3
  52. package/test/mcp/tools/findContactByPhone.test.js +3 -3
  53. package/test/mcp/tools/getGoogleFilePicker.test.js +7 -7
  54. package/test/mcp/tools/getPublicConnectors.test.js +49 -70
  55. package/test/mcp/tools/logout.test.js +17 -11
  56. package/mcp/SupportedPlatforms.md +0 -12
  57. package/mcp/tools/collectAuthInfo.js +0 -91
  58. package/mcp/tools/setConnector.js +0 -69
  59. package/test/mcp/tools/collectAuthInfo.test.js +0 -234
  60. package/test/mcp/tools/setConnector.test.js +0 -177
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@app-connect/mcp-ui",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "dependencies": {
11
+ "@openai/apps-sdk-ui": "^0.2.0",
12
+ "react": "^18.3.1",
13
+ "react-dom": "^18.3.1"
14
+ },
15
+ "devDependencies": {
16
+ "@tailwindcss/vite": "^4.0.0",
17
+ "@types/react": "^18.3.12",
18
+ "@types/react-dom": "^18.3.1",
19
+ "@vitejs/plugin-react": "^4.3.4",
20
+ "tailwindcss": "^4.0.0",
21
+ "typescript": "^5.6.3",
22
+ "vite": "^6.0.1",
23
+ "vite-plugin-singlefile": "^2.3.0"
24
+ }
25
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "noEmit": true,
15
+ "jsx": "react-jsx",
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "types": []
23
+ },
24
+ "include": ["App"]
25
+ }
26
+
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import tailwindcss from '@tailwindcss/vite'
4
+ import { viteSingleFile } from 'vite-plugin-singlefile'
5
+
6
+ export default defineConfig({
7
+ plugins: [
8
+ react(),
9
+ tailwindcss(),
10
+ viteSingleFile(),
11
+ ],
12
+ build: {
13
+ outDir: 'dist',
14
+ }
15
+ })
16
+
@@ -0,0 +1,14 @@
1
+ const Sequelize = require('sequelize');
2
+ const { sequelize } = require('./sequelize');
3
+
4
+ // Model for Admin data
5
+ exports.LlmSessionModel = sequelize.define('llmSessions', {
6
+ // LLM session ID
7
+ id: {
8
+ type: Sequelize.STRING,
9
+ primaryKey: true,
10
+ },
11
+ jwtToken: {
12
+ type: Sequelize.STRING,
13
+ }
14
+ });
@@ -36,6 +36,9 @@ exports.UserModel = sequelize.define('users', {
36
36
  platformAdditionalInfo: {
37
37
  type: Sequelize.JSON
38
38
  },
39
+ hashedRcExtensionId: {
40
+ type: Sequelize.STRING,
41
+ },
39
42
  userSettings: {
40
43
  type: Sequelize.JSON
41
44
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@app-connect/core",
3
- "version": "1.7.18",
3
+ "version": "1.7.20",
4
4
  "description": "RingCentral App Connect Core",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -25,7 +25,7 @@
25
25
  "@aws-sdk/client-dynamodb": "^3.751.0",
26
26
  "@aws-sdk/client-s3": "^3.947.0",
27
27
  "@aws-sdk/s3-request-presigner": "^3.947.0",
28
- "@modelcontextprotocol/sdk": "^1.21.1",
28
+ "@modelcontextprotocol/sdk": "^1.26.0",
29
29
  "awesome-phonenumber": "^5.6.0",
30
30
  "body-parser": "^1.20.4",
31
31
  "body-parser-xml": "^2.0.5",
package/releaseNotes.json CHANGED
@@ -1,4 +1,28 @@
1
1
  {
2
+ "1.7.20": {
3
+ "global": [
4
+ {
5
+ "type": "Fix",
6
+ "description": "Click-to-dial not detecting numbers in input fields"
7
+ },
8
+ {
9
+ "type": "Fix",
10
+ "description": "Warm transfer call logging issue"
11
+ }
12
+ ]
13
+ },
14
+ "1.7.19": {
15
+ "global": [
16
+ {
17
+ "type": "Beta",
18
+ "description": "Plugin system infrastructure. No plugin available yet, but add soon. Take a quick look at its [overview](https://appconnect.labs.ringcentral.com/users/plugins)"
19
+ },
20
+ {
21
+ "type": "Better",
22
+ "description": "Click-to-dial with wider number match"
23
+ }
24
+ ]
25
+ },
2
26
  "1.7.18": {
3
27
  "global": [
4
28
  {
@@ -501,6 +501,7 @@ describe('Auth Handler', () => {
501
501
  describe('getLicenseStatus', () => {
502
502
  test('should return license status from platform module', async () => {
503
503
  // Arrange
504
+ const mockUser = global.testUtils.createMockUser({ id: 'user-123' });
504
505
  const mockLicenseStatus = {
505
506
  isValid: true,
506
507
  expiresAt: '2025-12-31',
@@ -513,6 +514,9 @@ describe('Auth Handler', () => {
513
514
 
514
515
  connectorRegistry.getConnector.mockReturnValue(mockConnector);
515
516
 
517
+ const { UserModel } = require('../../models/userModel');
518
+ jest.spyOn(UserModel, 'findByPk').mockResolvedValue(mockUser);
519
+
516
520
  // Act
517
521
  const result = await authHandler.getLicenseStatus({
518
522
  userId: 'user-123',
@@ -523,8 +527,35 @@ describe('Auth Handler', () => {
523
527
  expect(result).toEqual(mockLicenseStatus);
524
528
  expect(mockConnector.getLicenseStatus).toHaveBeenCalledWith({
525
529
  userId: 'user-123',
530
+ platform: 'testCRM',
531
+ user: mockUser
532
+ });
533
+ });
534
+
535
+ test('should return invalid license status when user not found', async () => {
536
+ // Arrange
537
+ const mockConnector = global.testUtils.createMockConnector({
538
+ getLicenseStatus: jest.fn()
539
+ });
540
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
541
+
542
+ const { UserModel } = require('../../models/userModel');
543
+ jest.spyOn(UserModel, 'findByPk').mockResolvedValue(null);
544
+
545
+ // Act
546
+ const result = await authHandler.getLicenseStatus({
547
+ userId: 'missing-user',
526
548
  platform: 'testCRM'
527
549
  });
550
+
551
+ // Assert
552
+ expect(result).toEqual({
553
+ isLicenseValid: false,
554
+ licenseStatus: 'Invalid (User not found)',
555
+ licenseStatusDescription: ''
556
+ });
557
+ expect(connectorRegistry.getConnector).not.toHaveBeenCalled();
558
+ expect(mockConnector.getLicenseStatus).not.toHaveBeenCalled();
528
559
  });
529
560
  });
530
561
 
@@ -0,0 +1,287 @@
1
+ // Use in-memory SQLite for isolated model tests
2
+ jest.mock('../../models/sequelize', () => {
3
+ const { Sequelize } = require('sequelize');
4
+ return {
5
+ sequelize: new Sequelize({
6
+ dialect: 'sqlite',
7
+ storage: ':memory:',
8
+ logging: false,
9
+ }),
10
+ };
11
+ });
12
+
13
+ const pluginHandler = require('../../handlers/plugin');
14
+ const { CacheModel } = require('../../models/cacheModel');
15
+ const { sequelize } = require('../../models/sequelize');
16
+
17
+ describe('Plugin Handler', () => {
18
+ beforeAll(async () => {
19
+ await CacheModel.sync({ force: true });
20
+ });
21
+
22
+ afterEach(async () => {
23
+ await CacheModel.destroy({ where: {} });
24
+ jest.clearAllMocks();
25
+ });
26
+
27
+ afterAll(async () => {
28
+ await sequelize.close();
29
+ });
30
+
31
+ describe('getPluginAsyncTasks', () => {
32
+ test('should retrieve async task status by IDs from CacheModel', async () => {
33
+ // Arrange
34
+ await CacheModel.create({
35
+ id: 'user-123-task-1',
36
+ status: 'processing',
37
+ userId: 'user-123',
38
+ cacheKey: 'pluginTask-googleDrive'
39
+ });
40
+ await CacheModel.create({
41
+ id: 'user-123-task-2',
42
+ status: 'completed',
43
+ userId: 'user-123',
44
+ cacheKey: 'pluginTask-piiRedaction'
45
+ });
46
+
47
+ // Act
48
+ const result = await pluginHandler.getPluginAsyncTasks({
49
+ asyncTaskIds: ['user-123-task-1', 'user-123-task-2']
50
+ });
51
+
52
+ // Assert
53
+ expect(result).toHaveLength(2);
54
+ expect(result).toContainEqual({ cacheKey: 'pluginTask-googleDrive', status: 'processing' });
55
+ expect(result).toContainEqual({ cacheKey: 'pluginTask-piiRedaction', status: 'completed' });
56
+ });
57
+
58
+ test('should return empty array when no matching tasks found', async () => {
59
+ // Arrange - no tasks created
60
+
61
+ // Act
62
+ const result = await pluginHandler.getPluginAsyncTasks({
63
+ asyncTaskIds: ['non-existent-task-1', 'non-existent-task-2']
64
+ });
65
+
66
+ // Assert
67
+ expect(result).toEqual([]);
68
+ });
69
+
70
+ test('should filter and return only tasks with matching IDs', async () => {
71
+ // Arrange
72
+ await CacheModel.create({
73
+ id: 'user-123-task-1',
74
+ status: 'processing',
75
+ userId: 'user-123',
76
+ cacheKey: 'pluginTask-googleDrive'
77
+ });
78
+ await CacheModel.create({
79
+ id: 'user-456-task-2',
80
+ status: 'completed',
81
+ userId: 'user-456',
82
+ cacheKey: 'pluginTask-piiRedaction'
83
+ });
84
+ await CacheModel.create({
85
+ id: 'user-789-task-3',
86
+ status: 'failed',
87
+ userId: 'user-789',
88
+ cacheKey: 'pluginTask-other'
89
+ });
90
+
91
+ // Act - only request tasks for user-123 and user-789
92
+ const result = await pluginHandler.getPluginAsyncTasks({
93
+ asyncTaskIds: ['user-123-task-1', 'user-789-task-3']
94
+ });
95
+
96
+ // Assert
97
+ expect(result).toHaveLength(2);
98
+ expect(result).toContainEqual({ cacheKey: 'pluginTask-googleDrive', status: 'processing' });
99
+ expect(result).toContainEqual({ cacheKey: 'pluginTask-other', status: 'failed' });
100
+ expect(result).not.toContainEqual(expect.objectContaining({ cacheKey: 'pluginTask-piiRedaction' }));
101
+ });
102
+
103
+ test('should automatically remove completed tasks from cache after retrieval', async () => {
104
+ // Arrange
105
+ await CacheModel.create({
106
+ id: 'user-123-completed-task',
107
+ status: 'completed',
108
+ userId: 'user-123',
109
+ cacheKey: 'pluginTask-googleDrive'
110
+ });
111
+
112
+ // Act
113
+ const result = await pluginHandler.getPluginAsyncTasks({
114
+ asyncTaskIds: ['user-123-completed-task']
115
+ });
116
+
117
+ // Assert - result should contain the task
118
+ expect(result).toHaveLength(1);
119
+ expect(result[0]).toEqual({ cacheKey: 'pluginTask-googleDrive', status: 'completed' });
120
+
121
+ // Verify task was removed from cache
122
+ const remainingTask = await CacheModel.findByPk('user-123-completed-task');
123
+ expect(remainingTask).toBeNull();
124
+ });
125
+
126
+ test('should automatically remove failed tasks from cache after retrieval', async () => {
127
+ // Arrange
128
+ await CacheModel.create({
129
+ id: 'user-123-failed-task',
130
+ status: 'failed',
131
+ userId: 'user-123',
132
+ cacheKey: 'pluginTask-piiRedaction'
133
+ });
134
+
135
+ // Act
136
+ const result = await pluginHandler.getPluginAsyncTasks({
137
+ asyncTaskIds: ['user-123-failed-task']
138
+ });
139
+
140
+ // Assert - result should contain the task
141
+ expect(result).toHaveLength(1);
142
+ expect(result[0]).toEqual({ cacheKey: 'pluginTask-piiRedaction', status: 'failed' });
143
+
144
+ // Verify task was removed from cache
145
+ const remainingTask = await CacheModel.findByPk('user-123-failed-task');
146
+ expect(remainingTask).toBeNull();
147
+ });
148
+
149
+ test('should preserve pending tasks in cache after retrieval', async () => {
150
+ // Arrange
151
+ await CacheModel.create({
152
+ id: 'user-123-pending-task',
153
+ status: 'pending',
154
+ userId: 'user-123',
155
+ cacheKey: 'pluginTask-googleDrive'
156
+ });
157
+
158
+ // Act
159
+ const result = await pluginHandler.getPluginAsyncTasks({
160
+ asyncTaskIds: ['user-123-pending-task']
161
+ });
162
+
163
+ // Assert - result should contain the task
164
+ expect(result).toHaveLength(1);
165
+ expect(result[0]).toEqual({ cacheKey: 'pluginTask-googleDrive', status: 'pending' });
166
+
167
+ // Verify task was NOT removed from cache
168
+ const remainingTask = await CacheModel.findByPk('user-123-pending-task');
169
+ expect(remainingTask).not.toBeNull();
170
+ expect(remainingTask.status).toBe('pending');
171
+ });
172
+
173
+ test('should preserve processing tasks in cache after retrieval', async () => {
174
+ // Arrange
175
+ await CacheModel.create({
176
+ id: 'user-123-processing-task',
177
+ status: 'processing',
178
+ userId: 'user-123',
179
+ cacheKey: 'pluginTask-piiRedaction'
180
+ });
181
+
182
+ // Act
183
+ const result = await pluginHandler.getPluginAsyncTasks({
184
+ asyncTaskIds: ['user-123-processing-task']
185
+ });
186
+
187
+ // Assert - result should contain the task
188
+ expect(result).toHaveLength(1);
189
+ expect(result[0]).toEqual({ cacheKey: 'pluginTask-piiRedaction', status: 'processing' });
190
+
191
+ // Verify task was NOT removed from cache
192
+ const remainingTask = await CacheModel.findByPk('user-123-processing-task');
193
+ expect(remainingTask).not.toBeNull();
194
+ expect(remainingTask.status).toBe('processing');
195
+ });
196
+
197
+ test('should handle mixed task statuses - remove completed/failed but preserve pending/processing', async () => {
198
+ // Arrange
199
+ await CacheModel.create({
200
+ id: 'task-completed',
201
+ status: 'completed',
202
+ userId: 'user-123',
203
+ cacheKey: 'pluginTask-1'
204
+ });
205
+ await CacheModel.create({
206
+ id: 'task-failed',
207
+ status: 'failed',
208
+ userId: 'user-123',
209
+ cacheKey: 'pluginTask-2'
210
+ });
211
+ await CacheModel.create({
212
+ id: 'task-pending',
213
+ status: 'pending',
214
+ userId: 'user-123',
215
+ cacheKey: 'pluginTask-3'
216
+ });
217
+ await CacheModel.create({
218
+ id: 'task-processing',
219
+ status: 'processing',
220
+ userId: 'user-123',
221
+ cacheKey: 'pluginTask-4'
222
+ });
223
+
224
+ // Act
225
+ const result = await pluginHandler.getPluginAsyncTasks({
226
+ asyncTaskIds: ['task-completed', 'task-failed', 'task-pending', 'task-processing']
227
+ });
228
+
229
+ // Assert - all tasks should be in result
230
+ expect(result).toHaveLength(4);
231
+
232
+ // Verify completed and failed tasks were removed
233
+ expect(await CacheModel.findByPk('task-completed')).toBeNull();
234
+ expect(await CacheModel.findByPk('task-failed')).toBeNull();
235
+
236
+ // Verify pending and processing tasks were preserved
237
+ expect(await CacheModel.findByPk('task-pending')).not.toBeNull();
238
+ expect(await CacheModel.findByPk('task-processing')).not.toBeNull();
239
+ });
240
+
241
+ test('should handle empty asyncTaskIds array', async () => {
242
+ // Arrange
243
+ await CacheModel.create({
244
+ id: 'some-task',
245
+ status: 'completed',
246
+ userId: 'user-123',
247
+ cacheKey: 'pluginTask-test'
248
+ });
249
+
250
+ // Act
251
+ const result = await pluginHandler.getPluginAsyncTasks({
252
+ asyncTaskIds: []
253
+ });
254
+
255
+ // Assert
256
+ expect(result).toEqual([]);
257
+
258
+ // Verify existing task was not touched
259
+ const existingTask = await CacheModel.findByPk('some-task');
260
+ expect(existingTask).not.toBeNull();
261
+ });
262
+
263
+ test('should preserve initialized status tasks in cache', async () => {
264
+ // Arrange
265
+ await CacheModel.create({
266
+ id: 'user-123-initialized-task',
267
+ status: 'initialized',
268
+ userId: 'user-123',
269
+ cacheKey: 'pluginTask-googleDrive'
270
+ });
271
+
272
+ // Act
273
+ const result = await pluginHandler.getPluginAsyncTasks({
274
+ asyncTaskIds: ['user-123-initialized-task']
275
+ });
276
+
277
+ // Assert
278
+ expect(result).toHaveLength(1);
279
+ expect(result[0]).toEqual({ cacheKey: 'pluginTask-googleDrive', status: 'initialized' });
280
+
281
+ // Verify task was NOT removed from cache
282
+ const remainingTask = await CacheModel.findByPk('user-123-initialized-task');
283
+ expect(remainingTask).not.toBeNull();
284
+ });
285
+ });
286
+ });
287
+