@app-connect/core 1.7.18 → 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 +72 -72
- package/releaseNotes.json +12 -0
- 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
|
@@ -1,376 +1,113 @@
|
|
|
1
1
|
const doAuth = require('../../../mcp/tools/doAuth');
|
|
2
|
-
const
|
|
3
|
-
const jwt = require('../../../lib/jwt');
|
|
2
|
+
const { createAuthSession } = require('../../../lib/authSession');
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
jest.mock('../../../handlers/auth');
|
|
7
|
-
jest.mock('../../../lib/jwt');
|
|
4
|
+
jest.mock('../../../lib/authSession');
|
|
8
5
|
|
|
9
6
|
describe('MCP Tool: doAuth', () => {
|
|
10
7
|
beforeEach(() => {
|
|
11
8
|
jest.clearAllMocks();
|
|
12
|
-
process.env.APP_SERVER_SECRET_KEY = 'test-secret-key';
|
|
13
9
|
});
|
|
14
10
|
|
|
15
11
|
describe('tool definition', () => {
|
|
16
12
|
test('should have correct tool definition', () => {
|
|
17
13
|
expect(doAuth.definition).toBeDefined();
|
|
18
14
|
expect(doAuth.definition.name).toBe('doAuth');
|
|
19
|
-
expect(doAuth.definition.description).toContain('
|
|
15
|
+
expect(doAuth.definition.description).toContain('OAuth session');
|
|
20
16
|
expect(doAuth.definition.inputSchema).toBeDefined();
|
|
21
17
|
});
|
|
22
18
|
|
|
23
|
-
test('should have optional
|
|
24
|
-
expect(doAuth.definition.inputSchema.
|
|
19
|
+
test('should require connectorName and have optional hostname', () => {
|
|
20
|
+
expect(doAuth.definition.inputSchema.required).toContain('connectorName');
|
|
25
21
|
expect(doAuth.definition.inputSchema.properties).toHaveProperty('connectorName');
|
|
26
22
|
expect(doAuth.definition.inputSchema.properties).toHaveProperty('hostname');
|
|
27
|
-
expect(doAuth.definition.inputSchema.properties).toHaveProperty('apiKey');
|
|
28
|
-
expect(doAuth.definition.inputSchema.properties).toHaveProperty('additionalInfo');
|
|
29
|
-
expect(doAuth.definition.inputSchema.properties).toHaveProperty('callbackUri');
|
|
30
23
|
});
|
|
31
24
|
});
|
|
32
25
|
|
|
33
|
-
describe('execute
|
|
34
|
-
test('should
|
|
26
|
+
describe('execute', () => {
|
|
27
|
+
test('should create auth session successfully', async () => {
|
|
35
28
|
// Arrange
|
|
36
|
-
|
|
37
|
-
platforms: {
|
|
38
|
-
testCRM: {
|
|
39
|
-
name: 'testCRM',
|
|
40
|
-
auth: { type: 'apiKey', apiKey: { name: 'apiKey' } },
|
|
41
|
-
environment: { type: 'fixed' }
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const mockUserInfo = {
|
|
47
|
-
id: 'test-user-123',
|
|
48
|
-
name: 'Test User'
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
authCore.onApiKeyLogin.mockResolvedValue({
|
|
52
|
-
userInfo: mockUserInfo
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
jwt.generateJwt.mockReturnValue('mock-jwt-token');
|
|
29
|
+
createAuthSession.mockResolvedValue(undefined);
|
|
56
30
|
|
|
57
31
|
// Act
|
|
58
32
|
const result = await doAuth.execute({
|
|
59
|
-
|
|
60
|
-
connectorName: '
|
|
61
|
-
hostname: '
|
|
62
|
-
apiKey: 'test-api-key'
|
|
33
|
+
sessionId: 'session-abc',
|
|
34
|
+
connectorName: 'pipedrive',
|
|
35
|
+
hostname: 'mycompany.pipedrive.com'
|
|
63
36
|
});
|
|
64
37
|
|
|
65
38
|
// Assert
|
|
66
|
-
expect(result).toEqual({
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
message: expect.stringContaining('IMPORTANT')
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
expect(authCore.onApiKeyLogin).toHaveBeenCalledWith({
|
|
74
|
-
platform: 'testCRM',
|
|
75
|
-
hostname: 'test.crm.com',
|
|
76
|
-
apiKey: 'test-api-key',
|
|
77
|
-
additionalInfo: undefined
|
|
78
|
-
});
|
|
79
|
-
expect(jwt.generateJwt).toHaveBeenCalledWith({
|
|
80
|
-
id: 'test-user-123',
|
|
81
|
-
platform: 'testCRM'
|
|
39
|
+
expect(result).toEqual({ success: true });
|
|
40
|
+
expect(createAuthSession).toHaveBeenCalledWith('session-abc', {
|
|
41
|
+
platform: 'pipedrive',
|
|
42
|
+
hostname: 'mycompany.pipedrive.com'
|
|
82
43
|
});
|
|
83
44
|
});
|
|
84
45
|
|
|
85
|
-
test('should
|
|
46
|
+
test('should create auth session with empty hostname when not provided', async () => {
|
|
86
47
|
// Arrange
|
|
87
|
-
|
|
88
|
-
platforms: {
|
|
89
|
-
testCRM: {
|
|
90
|
-
name: 'testCRM',
|
|
91
|
-
auth: { type: 'apiKey', apiKey: { name: 'apiKey' } }
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const additionalInfo = {
|
|
97
|
-
username: 'testuser',
|
|
98
|
-
password: 'testpass',
|
|
99
|
-
apiUrl: 'https://api.test.com'
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const mockUserInfo = {
|
|
103
|
-
id: 'test-user-456',
|
|
104
|
-
name: 'Test User'
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
authCore.onApiKeyLogin.mockResolvedValue({
|
|
108
|
-
userInfo: mockUserInfo
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
jwt.generateJwt.mockReturnValue('mock-jwt-token-2');
|
|
48
|
+
createAuthSession.mockResolvedValue(undefined);
|
|
112
49
|
|
|
113
50
|
// Act
|
|
114
51
|
const result = await doAuth.execute({
|
|
115
|
-
|
|
116
|
-
connectorName: '
|
|
117
|
-
hostname: 'test.crm.com',
|
|
118
|
-
apiKey: 'test-api-key',
|
|
119
|
-
additionalInfo
|
|
52
|
+
sessionId: 'session-xyz',
|
|
53
|
+
connectorName: 'clio'
|
|
120
54
|
});
|
|
121
55
|
|
|
122
56
|
// Assert
|
|
123
|
-
expect(result
|
|
124
|
-
expect(
|
|
125
|
-
platform: '
|
|
126
|
-
hostname: '
|
|
127
|
-
apiKey: 'test-api-key',
|
|
128
|
-
additionalInfo
|
|
57
|
+
expect(result).toEqual({ success: true });
|
|
58
|
+
expect(createAuthSession).toHaveBeenCalledWith('session-xyz', {
|
|
59
|
+
platform: 'clio',
|
|
60
|
+
hostname: ''
|
|
129
61
|
});
|
|
130
62
|
});
|
|
131
63
|
|
|
132
|
-
test('should return error when
|
|
133
|
-
// Arrange
|
|
134
|
-
const mockManifest = {
|
|
135
|
-
platforms: {
|
|
136
|
-
testCRM: {
|
|
137
|
-
name: 'testCRM',
|
|
138
|
-
auth: { type: 'apiKey', apiKey: { name: 'apiKey' } }
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
authCore.onApiKeyLogin.mockResolvedValue({
|
|
144
|
-
userInfo: null
|
|
145
|
-
});
|
|
146
|
-
|
|
64
|
+
test('should return error when sessionId is missing', async () => {
|
|
147
65
|
// Act
|
|
148
|
-
const result = await doAuth.execute({
|
|
149
|
-
connectorManifest: mockManifest,
|
|
150
|
-
connectorName: 'testCRM',
|
|
151
|
-
hostname: 'test.crm.com',
|
|
152
|
-
apiKey: 'invalid-api-key'
|
|
153
|
-
});
|
|
66
|
+
const result = await doAuth.execute({ connectorName: 'pipedrive' });
|
|
154
67
|
|
|
155
68
|
// Assert
|
|
156
69
|
expect(result).toEqual({
|
|
157
70
|
success: false,
|
|
158
|
-
error: '
|
|
159
|
-
errorDetails: 'User info not found'
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
describe('execute - OAuth authentication', () => {
|
|
165
|
-
test('should return auth URI when callback not provided', async () => {
|
|
166
|
-
// Arrange
|
|
167
|
-
const mockManifest = {
|
|
168
|
-
platforms: {
|
|
169
|
-
salesforce: {
|
|
170
|
-
name: 'salesforce',
|
|
171
|
-
auth: {
|
|
172
|
-
type: 'oauth',
|
|
173
|
-
oauth: {
|
|
174
|
-
authUrl: 'https://login.salesforce.com/services/oauth2/authorize',
|
|
175
|
-
clientId: 'test-client-id',
|
|
176
|
-
scope: 'api refresh_token',
|
|
177
|
-
customState: ''
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
// Act
|
|
185
|
-
const result = await doAuth.execute({
|
|
186
|
-
connectorManifest: mockManifest,
|
|
187
|
-
connectorName: 'salesforce'
|
|
71
|
+
error: 'Missing required fields: sessionId, connectorName'
|
|
188
72
|
});
|
|
189
|
-
|
|
190
|
-
// Assert
|
|
191
|
-
expect(result.success).toBe(true);
|
|
192
|
-
expect(result.data.authUri).toContain('https://login.salesforce.com');
|
|
193
|
-
expect(result.data.authUri).toContain('client_id=test-client-id');
|
|
194
|
-
expect(result.data.authUri).toContain('response_type=code');
|
|
195
|
-
expect(result.data.message).toContain('IMPORTANT');
|
|
73
|
+
expect(createAuthSession).not.toHaveBeenCalled();
|
|
196
74
|
});
|
|
197
75
|
|
|
198
|
-
test('should
|
|
199
|
-
// Arrange
|
|
200
|
-
const mockManifest = {
|
|
201
|
-
platforms: {
|
|
202
|
-
salesforce: {
|
|
203
|
-
name: 'salesforce',
|
|
204
|
-
auth: {
|
|
205
|
-
type: 'oauth',
|
|
206
|
-
oauth: {
|
|
207
|
-
authUrl: 'https://login.salesforce.com/services/oauth2/authorize',
|
|
208
|
-
clientId: 'test-client-id'
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
const mockUserInfo = {
|
|
216
|
-
id: 'sf-user-123',
|
|
217
|
-
name: 'SF User'
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
authCore.onOAuthCallback.mockResolvedValue({
|
|
221
|
-
userInfo: mockUserInfo
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
jwt.generateJwt.mockReturnValue('mock-jwt-token-oauth');
|
|
225
|
-
|
|
76
|
+
test('should return error when connectorName is missing', async () => {
|
|
226
77
|
// Act
|
|
227
|
-
const result = await doAuth.execute({
|
|
228
|
-
connectorManifest: mockManifest,
|
|
229
|
-
connectorName: 'salesforce',
|
|
230
|
-
hostname: 'login.salesforce.com',
|
|
231
|
-
callbackUri: 'https://redirect.com?code=test-code&state=test-state'
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
// Assert
|
|
235
|
-
expect(result).toEqual({
|
|
236
|
-
success: true,
|
|
237
|
-
data: {
|
|
238
|
-
jwtToken: 'mock-jwt-token-oauth',
|
|
239
|
-
message: expect.stringContaining('IMPORTANT')
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
expect(authCore.onOAuthCallback).toHaveBeenCalledWith({
|
|
243
|
-
platform: 'salesforce',
|
|
244
|
-
hostname: 'login.salesforce.com',
|
|
245
|
-
callbackUri: 'https://redirect.com?code=test-code&state=test-state',
|
|
246
|
-
query: expect.objectContaining({
|
|
247
|
-
hostname: 'login.salesforce.com'
|
|
248
|
-
})
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
test('should return error when OAuth callback fails', async () => {
|
|
253
|
-
// Arrange
|
|
254
|
-
const mockManifest = {
|
|
255
|
-
platforms: {
|
|
256
|
-
salesforce: {
|
|
257
|
-
name: 'salesforce',
|
|
258
|
-
auth: {
|
|
259
|
-
type: 'oauth',
|
|
260
|
-
oauth: {
|
|
261
|
-
authUrl: 'https://login.salesforce.com/services/oauth2/authorize',
|
|
262
|
-
clientId: 'test-client-id'
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
authCore.onOAuthCallback.mockResolvedValue({
|
|
270
|
-
userInfo: null
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// Act - callbackUri needs code= and state= to be treated as OAuth callback
|
|
274
|
-
const result = await doAuth.execute({
|
|
275
|
-
connectorManifest: mockManifest,
|
|
276
|
-
connectorName: 'salesforce',
|
|
277
|
-
hostname: 'login.salesforce.com',
|
|
278
|
-
callbackUri: 'https://redirect.com?code=invalid-code&state=test-state'
|
|
279
|
-
});
|
|
78
|
+
const result = await doAuth.execute({ sessionId: 'session-abc' });
|
|
280
79
|
|
|
281
80
|
// Assert
|
|
282
81
|
expect(result).toEqual({
|
|
283
82
|
success: false,
|
|
284
|
-
error: '
|
|
285
|
-
errorDetails: 'User info not found'
|
|
83
|
+
error: 'Missing required fields: sessionId, connectorName'
|
|
286
84
|
});
|
|
85
|
+
expect(createAuthSession).not.toHaveBeenCalled();
|
|
287
86
|
});
|
|
288
87
|
|
|
289
|
-
test('should
|
|
290
|
-
// Arrange
|
|
291
|
-
const mockManifest = {
|
|
292
|
-
platforms: {
|
|
293
|
-
customCRM: {
|
|
294
|
-
name: 'customCRM',
|
|
295
|
-
auth: {
|
|
296
|
-
type: 'oauth',
|
|
297
|
-
oauth: {
|
|
298
|
-
authUrl: 'https://custom.com/oauth',
|
|
299
|
-
clientId: 'custom-client-id',
|
|
300
|
-
scope: '',
|
|
301
|
-
customState: 'custom=state&other=value'
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
// Act
|
|
309
|
-
const result = await doAuth.execute({
|
|
310
|
-
connectorManifest: mockManifest,
|
|
311
|
-
connectorName: 'customCRM'
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
// Assert - state is now URL-encoded and includes sessionId, platform, hostname, plus customState
|
|
315
|
-
expect(result.success).toBe(true);
|
|
316
|
-
// The state parameter now contains session info and custom state appended
|
|
317
|
-
// Decode and verify custom state is included
|
|
318
|
-
const stateMatch = result.data.authUri.match(/state=([^&]+)/);
|
|
319
|
-
expect(stateMatch).toBeTruthy();
|
|
320
|
-
const decodedState = decodeURIComponent(stateMatch[1]);
|
|
321
|
-
expect(decodedState).toContain('custom=state&other=value');
|
|
322
|
-
expect(decodedState).toContain('sessionId=');
|
|
323
|
-
expect(decodedState).toContain('platform=customCRM');
|
|
324
|
-
});
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
describe('error handling', () => {
|
|
328
|
-
test('should handle authentication errors gracefully', async () => {
|
|
329
|
-
// Arrange
|
|
330
|
-
const mockManifest = {
|
|
331
|
-
platforms: {
|
|
332
|
-
testCRM: {
|
|
333
|
-
name: 'testCRM',
|
|
334
|
-
auth: { type: 'apiKey', apiKey: { name: 'apiKey' } }
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
authCore.onApiKeyLogin.mockRejectedValue(
|
|
340
|
-
new Error('Invalid credentials')
|
|
341
|
-
);
|
|
342
|
-
|
|
88
|
+
test('should return error when both sessionId and connectorName are missing', async () => {
|
|
343
89
|
// Act
|
|
344
|
-
const result = await doAuth.execute({
|
|
345
|
-
connectorManifest: mockManifest,
|
|
346
|
-
connectorName: 'testCRM',
|
|
347
|
-
hostname: 'test.crm.com',
|
|
348
|
-
apiKey: 'bad-key'
|
|
349
|
-
});
|
|
90
|
+
const result = await doAuth.execute({});
|
|
350
91
|
|
|
351
92
|
// Assert
|
|
352
93
|
expect(result.success).toBe(false);
|
|
353
|
-
expect(result.error).
|
|
354
|
-
expect(result.errorDetails).toBeDefined();
|
|
94
|
+
expect(result.error).toContain('Missing required fields');
|
|
355
95
|
});
|
|
356
96
|
|
|
357
|
-
test('should handle
|
|
97
|
+
test('should handle unexpected errors gracefully', async () => {
|
|
358
98
|
// Arrange
|
|
359
|
-
|
|
360
|
-
platforms: {}
|
|
361
|
-
};
|
|
99
|
+
createAuthSession.mockRejectedValue(new Error('DB write failed'));
|
|
362
100
|
|
|
363
101
|
// Act
|
|
364
102
|
const result = await doAuth.execute({
|
|
365
|
-
|
|
366
|
-
connectorName: '
|
|
367
|
-
apiKey: 'test-key'
|
|
103
|
+
sessionId: 'session-abc',
|
|
104
|
+
connectorName: 'pipedrive'
|
|
368
105
|
});
|
|
369
106
|
|
|
370
107
|
// Assert
|
|
371
108
|
expect(result.success).toBe(false);
|
|
372
|
-
expect(result.error).
|
|
109
|
+
expect(result.error).toBe('DB write failed');
|
|
110
|
+
expect(result.errorDetails).toBeDefined();
|
|
373
111
|
});
|
|
374
112
|
});
|
|
375
113
|
});
|
|
376
|
-
|
|
@@ -17,12 +17,12 @@ describe('MCP Tool: findContactByName', () => {
|
|
|
17
17
|
test('should have correct tool definition', () => {
|
|
18
18
|
expect(findContactByName.definition).toBeDefined();
|
|
19
19
|
expect(findContactByName.definition.name).toBe('findContactByName');
|
|
20
|
-
expect(findContactByName.definition.description).toContain('REQUIRES
|
|
20
|
+
expect(findContactByName.definition.description).toContain('REQUIRES CRM CONNECTION');
|
|
21
21
|
expect(findContactByName.definition.inputSchema).toBeDefined();
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
test('should require jwtToken
|
|
25
|
-
expect(findContactByName.definition.inputSchema.required).toContain('jwtToken');
|
|
24
|
+
test('should require name parameter (jwtToken is server-injected)', () => {
|
|
25
|
+
expect(findContactByName.definition.inputSchema.required).not.toContain('jwtToken');
|
|
26
26
|
expect(findContactByName.definition.inputSchema.required).toContain('name');
|
|
27
27
|
});
|
|
28
28
|
});
|
|
@@ -17,12 +17,12 @@ describe('MCP Tool: findContactByPhone', () => {
|
|
|
17
17
|
test('should have correct tool definition', () => {
|
|
18
18
|
expect(findContactByPhone.definition).toBeDefined();
|
|
19
19
|
expect(findContactByPhone.definition.name).toBe('findContactByPhone');
|
|
20
|
-
expect(findContactByPhone.definition.description).toContain('REQUIRES
|
|
20
|
+
expect(findContactByPhone.definition.description).toContain('REQUIRES CRM CONNECTION');
|
|
21
21
|
expect(findContactByPhone.definition.inputSchema).toBeDefined();
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
test('should require jwtToken
|
|
25
|
-
expect(findContactByPhone.definition.inputSchema.required).toContain('jwtToken');
|
|
24
|
+
test('should require phoneNumber parameter (jwtToken is server-injected)', () => {
|
|
25
|
+
expect(findContactByPhone.definition.inputSchema.required).not.toContain('jwtToken');
|
|
26
26
|
expect(findContactByPhone.definition.inputSchema.required).toContain('phoneNumber');
|
|
27
27
|
});
|
|
28
28
|
|
|
@@ -18,13 +18,13 @@ describe('MCP Tool: getGoogleFilePicker', () => {
|
|
|
18
18
|
test('should have correct tool definition', () => {
|
|
19
19
|
expect(getGoogleFilePicker.definition).toBeDefined();
|
|
20
20
|
expect(getGoogleFilePicker.definition.name).toBe('getGoogleFilePicker');
|
|
21
|
-
expect(getGoogleFilePicker.definition.description).toContain('REQUIRES
|
|
21
|
+
expect(getGoogleFilePicker.definition.description).toContain('REQUIRES CRM CONNECTION');
|
|
22
22
|
expect(getGoogleFilePicker.definition.description).toContain('Google Sheets file picker');
|
|
23
23
|
expect(getGoogleFilePicker.definition.inputSchema).toBeDefined();
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
test('should require jwtToken
|
|
27
|
-
expect(getGoogleFilePicker.definition.inputSchema.required).toContain('jwtToken');
|
|
26
|
+
test('should not require jwtToken in schema (it is server-injected)', () => {
|
|
27
|
+
expect(getGoogleFilePicker.definition.inputSchema.required).not.toContain('jwtToken');
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
test('should have optional sheetName parameter', () => {
|
|
@@ -62,7 +62,7 @@ describe('MCP Tool: getGoogleFilePicker', () => {
|
|
|
62
62
|
expect(result).toEqual({
|
|
63
63
|
success: true,
|
|
64
64
|
data: {
|
|
65
|
-
filePickerUrl: 'https://test-app-server.com/googleSheets/filePicker?token=mock-jwt-token
|
|
65
|
+
filePickerUrl: 'https://test-app-server.com/googleSheets/filePicker?token=mock-jwt-token',
|
|
66
66
|
message: expect.stringContaining('Please open this URL')
|
|
67
67
|
}
|
|
68
68
|
});
|
|
@@ -149,7 +149,7 @@ describe('MCP Tool: getGoogleFilePicker', () => {
|
|
|
149
149
|
// Assert
|
|
150
150
|
expect(result).toEqual({
|
|
151
151
|
success: false,
|
|
152
|
-
error: 'JWT token is required. Please
|
|
152
|
+
error: 'JWT token is required. Please connect to the CRM first using getPublicConnectors.'
|
|
153
153
|
});
|
|
154
154
|
});
|
|
155
155
|
|
|
@@ -162,7 +162,7 @@ describe('MCP Tool: getGoogleFilePicker', () => {
|
|
|
162
162
|
// Assert
|
|
163
163
|
expect(result).toEqual({
|
|
164
164
|
success: false,
|
|
165
|
-
error: 'JWT token is required. Please
|
|
165
|
+
error: 'JWT token is required. Please connect to the CRM first using getPublicConnectors.'
|
|
166
166
|
});
|
|
167
167
|
});
|
|
168
168
|
|
|
@@ -217,7 +217,7 @@ describe('MCP Tool: getGoogleFilePicker', () => {
|
|
|
217
217
|
// Assert
|
|
218
218
|
expect(result).toEqual({
|
|
219
219
|
success: false,
|
|
220
|
-
error: 'User not found. Please
|
|
220
|
+
error: 'User not found. Please connect to the CRM first using getPublicConnectors.'
|
|
221
221
|
});
|
|
222
222
|
expect(UserModel.findByPk).toHaveBeenCalledWith('nonexistent-user');
|
|
223
223
|
});
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
const getPublicConnectors = require('../../../mcp/tools/getPublicConnectors');
|
|
2
|
-
const
|
|
2
|
+
const axios = require('axios');
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
jest.mock('../../../connector/developerPortal');
|
|
4
|
+
jest.mock('axios');
|
|
6
5
|
|
|
7
6
|
describe('MCP Tool: getPublicConnectors', () => {
|
|
8
7
|
beforeEach(() => {
|
|
9
8
|
jest.clearAllMocks();
|
|
10
|
-
|
|
9
|
+
process.env.APP_SERVER = 'https://test-server.com';
|
|
11
10
|
});
|
|
12
11
|
|
|
13
12
|
describe('tool definition', () => {
|
|
14
13
|
test('should have correct tool definition', () => {
|
|
15
14
|
expect(getPublicConnectors.definition).toBeDefined();
|
|
16
15
|
expect(getPublicConnectors.definition.name).toBe('getPublicConnectors');
|
|
17
|
-
expect(getPublicConnectors.definition.description).toContain('
|
|
16
|
+
expect(getPublicConnectors.definition.description).toContain('connectors');
|
|
18
17
|
expect(getPublicConnectors.definition.inputSchema).toBeDefined();
|
|
19
18
|
expect(getPublicConnectors.definition.inputSchema.type).toBe('object');
|
|
20
19
|
});
|
|
@@ -25,104 +24,84 @@ describe('MCP Tool: getPublicConnectors', () => {
|
|
|
25
24
|
});
|
|
26
25
|
|
|
27
26
|
describe('execute', () => {
|
|
28
|
-
test('should return
|
|
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
|
-
|
|
27
|
+
test('should return structuredContent with server URL when no rcAccessToken', async () => {
|
|
39
28
|
// Act
|
|
40
|
-
const result = await getPublicConnectors.execute();
|
|
29
|
+
const result = await getPublicConnectors.execute({});
|
|
41
30
|
|
|
42
31
|
// Assert
|
|
43
32
|
expect(result).toEqual({
|
|
44
|
-
|
|
45
|
-
|
|
33
|
+
structuredContent: {
|
|
34
|
+
serverUrl: 'https://test-server.com',
|
|
35
|
+
rcExtensionId: null,
|
|
36
|
+
rcAccountId: null,
|
|
37
|
+
openaiSessionId: null,
|
|
38
|
+
}
|
|
46
39
|
});
|
|
47
|
-
expect(
|
|
40
|
+
expect(axios.get).not.toHaveBeenCalled();
|
|
48
41
|
});
|
|
49
42
|
|
|
50
|
-
test('should
|
|
51
|
-
// Arrange
|
|
52
|
-
|
|
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
|
|
43
|
+
test('should resolve RC account and extension IDs when rcAccessToken provided', async () => {
|
|
44
|
+
// Arrange
|
|
45
|
+
axios.get.mockResolvedValue({
|
|
46
|
+
data: { id: 'ext-456', account: { id: 'acc-789' } }
|
|
66
47
|
});
|
|
67
48
|
|
|
68
49
|
// Act
|
|
69
|
-
const result = await getPublicConnectors.execute(
|
|
50
|
+
const result = await getPublicConnectors.execute({
|
|
51
|
+
rcAccessToken: 'valid-rc-token',
|
|
52
|
+
openaiSessionId: 'session-abc'
|
|
53
|
+
});
|
|
70
54
|
|
|
71
55
|
// Assert
|
|
72
56
|
expect(result).toEqual({
|
|
73
|
-
|
|
74
|
-
|
|
57
|
+
structuredContent: {
|
|
58
|
+
serverUrl: 'https://test-server.com',
|
|
59
|
+
rcExtensionId: 'ext-456',
|
|
60
|
+
rcAccountId: 'acc-789',
|
|
61
|
+
openaiSessionId: 'session-abc',
|
|
62
|
+
}
|
|
75
63
|
});
|
|
76
|
-
expect(
|
|
77
|
-
|
|
64
|
+
expect(axios.get).toHaveBeenCalledWith(
|
|
65
|
+
'https://platform.ringcentral.com/restapi/v1.0/account/~/extension/~',
|
|
66
|
+
{ headers: { Authorization: 'Bearer valid-rc-token' } }
|
|
67
|
+
);
|
|
78
68
|
});
|
|
79
69
|
|
|
80
|
-
test('should return
|
|
81
|
-
// Arrange
|
|
82
|
-
|
|
83
|
-
connectors: []
|
|
84
|
-
});
|
|
70
|
+
test('should return null RC IDs and continue when RC API call fails', async () => {
|
|
71
|
+
// Arrange — RC API failure is non-fatal: widget only shows public connectors
|
|
72
|
+
axios.get.mockRejectedValue(new Error('RC API unavailable'));
|
|
85
73
|
|
|
86
74
|
// Act
|
|
87
|
-
const result = await getPublicConnectors.execute();
|
|
75
|
+
const result = await getPublicConnectors.execute({ rcAccessToken: 'bad-token' });
|
|
88
76
|
|
|
89
|
-
// Assert
|
|
77
|
+
// Assert — still returns structuredContent, just without RC IDs
|
|
90
78
|
expect(result).toEqual({
|
|
91
|
-
|
|
92
|
-
|
|
79
|
+
structuredContent: {
|
|
80
|
+
serverUrl: 'https://test-server.com',
|
|
81
|
+
rcExtensionId: null,
|
|
82
|
+
rcAccountId: null,
|
|
83
|
+
openaiSessionId: null,
|
|
84
|
+
}
|
|
93
85
|
});
|
|
94
86
|
});
|
|
95
87
|
|
|
96
|
-
test('should
|
|
97
|
-
// Arrange
|
|
98
|
-
const errorMessage = 'Failed to fetch connectors';
|
|
99
|
-
developerPortal.getPublicConnectorList.mockRejectedValue(
|
|
100
|
-
new Error(errorMessage)
|
|
101
|
-
);
|
|
102
|
-
|
|
88
|
+
test('should include openaiSessionId when provided', async () => {
|
|
103
89
|
// Act
|
|
104
|
-
const result = await getPublicConnectors.execute();
|
|
90
|
+
const result = await getPublicConnectors.execute({ openaiSessionId: 'my-session' });
|
|
105
91
|
|
|
106
92
|
// Assert
|
|
107
|
-
expect(result.
|
|
108
|
-
expect(result.error).toBe(errorMessage);
|
|
109
|
-
expect(result.errorDetails).toBeDefined();
|
|
93
|
+
expect(result.structuredContent.openaiSessionId).toBe('my-session');
|
|
110
94
|
});
|
|
111
95
|
|
|
112
|
-
test('should
|
|
96
|
+
test('should use default server URL when APP_SERVER is not set', async () => {
|
|
113
97
|
// Arrange
|
|
114
|
-
|
|
115
|
-
networkError.code = 'ECONNREFUSED';
|
|
116
|
-
developerPortal.getPublicConnectorList.mockRejectedValue(networkError);
|
|
98
|
+
delete process.env.APP_SERVER;
|
|
117
99
|
|
|
118
100
|
// Act
|
|
119
|
-
const result = await getPublicConnectors.execute();
|
|
101
|
+
const result = await getPublicConnectors.execute({});
|
|
120
102
|
|
|
121
103
|
// Assert
|
|
122
|
-
expect(result.
|
|
123
|
-
expect(result.error).toBe('Network request failed');
|
|
124
|
-
expect(result.errorDetails).toBeDefined();
|
|
104
|
+
expect(result.structuredContent.serverUrl).toBe('https://localhost:6066');
|
|
125
105
|
});
|
|
126
106
|
});
|
|
127
107
|
});
|
|
128
|
-
|