@app-connect/core 1.7.17 → 1.7.19
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/connector/proxy/index.js +2 -1
- package/handlers/log.js +181 -10
- package/handlers/plugin.js +27 -0
- package/handlers/user.js +31 -2
- package/index.js +99 -22
- package/lib/authSession.js +21 -12
- package/lib/callLogComposer.js +1 -1
- package/lib/debugTracer.js +20 -2
- package/lib/util.js +21 -4
- package/mcp/README.md +392 -0
- package/mcp/mcpHandler.js +293 -82
- package/mcp/tools/checkAuthStatus.js +27 -34
- package/mcp/tools/createCallLog.js +13 -9
- package/mcp/tools/createContact.js +2 -6
- package/mcp/tools/doAuth.js +27 -157
- package/mcp/tools/findContactByName.js +6 -9
- package/mcp/tools/findContactByPhone.js +2 -6
- package/mcp/tools/getGoogleFilePicker.js +5 -9
- package/mcp/tools/getHelp.js +2 -3
- package/mcp/tools/getPublicConnectors.js +41 -28
- package/mcp/tools/index.js +11 -36
- package/mcp/tools/logout.js +5 -10
- package/mcp/tools/rcGetCallLogs.js +3 -20
- package/mcp/ui/App/App.tsx +361 -0
- package/mcp/ui/App/components/AuthInfoForm.tsx +113 -0
- package/mcp/ui/App/components/AuthSuccess.tsx +22 -0
- package/mcp/ui/App/components/ConnectorList.tsx +82 -0
- package/mcp/ui/App/components/DebugPanel.tsx +43 -0
- package/mcp/ui/App/components/OAuthConnect.tsx +270 -0
- package/mcp/ui/App/lib/callTool.ts +130 -0
- package/mcp/ui/App/lib/debugLog.ts +41 -0
- package/mcp/ui/App/lib/developerPortal.ts +111 -0
- package/mcp/ui/App/main.css +6 -0
- package/mcp/ui/App/root.tsx +13 -0
- package/mcp/ui/dist/index.html +53 -0
- package/mcp/ui/index.html +13 -0
- package/mcp/ui/package-lock.json +6356 -0
- package/mcp/ui/package.json +25 -0
- package/mcp/ui/tsconfig.json +26 -0
- package/mcp/ui/vite.config.ts +16 -0
- package/models/llmSessionModel.js +14 -0
- package/package.json +2 -2
- package/releaseNotes.json +13 -1
- package/test/handlers/plugin.test.js +287 -0
- package/test/lib/util.test.js +379 -1
- package/test/mcp/tools/createCallLog.test.js +3 -3
- package/test/mcp/tools/doAuth.test.js +40 -303
- package/test/mcp/tools/findContactByName.test.js +3 -3
- package/test/mcp/tools/findContactByPhone.test.js +3 -3
- package/test/mcp/tools/getGoogleFilePicker.test.js +7 -7
- package/test/mcp/tools/getPublicConnectors.test.js +49 -70
- package/test/mcp/tools/logout.test.js +2 -2
- package/mcp/SupportedPlatforms.md +0 -12
- package/mcp/tools/collectAuthInfo.js +0 -91
- package/mcp/tools/setConnector.js +0 -69
- package/test/mcp/tools/collectAuthInfo.test.js +0 -234
- 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
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@app-connect/core",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.19",
|
|
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.
|
|
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,5 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
-
"1.7.
|
|
2
|
+
"1.7.19": {
|
|
3
|
+
"global": [
|
|
4
|
+
{
|
|
5
|
+
"type": "Beta",
|
|
6
|
+
"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)"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"type": "Better",
|
|
10
|
+
"description": "Click-to-dial with wider number match"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"1.7.18": {
|
|
3
15
|
"global": [
|
|
4
16
|
{
|
|
5
17
|
"type": "New",
|
|
@@ -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
|
+
|