@app-connect/core 1.7.19 → 1.7.21
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/handlers/auth.js +30 -55
- package/handlers/contact.js +7 -2
- package/handlers/log.js +3 -2
- package/index.js +19 -8
- package/mcp/README.md +5 -2
- package/mcp/mcpHandler.js +27 -2
- package/mcp/tools/checkAuthStatus.js +1 -1
- package/mcp/tools/getPublicConnectors.js +25 -7
- package/mcp/tools/logout.js +28 -4
- package/mcp/ui/App/App.tsx +2 -5
- package/mcp/ui/dist/index.html +10 -10
- package/models/userModel.js +3 -0
- package/package.json +1 -1
- package/releaseNotes.json +24 -0
- package/test/handlers/auth.test.js +31 -0
- package/test/handlers/contact.test.js +162 -0
- package/test/mcp/tools/logout.test.js +15 -9
package/handlers/auth.js
CHANGED
|
@@ -7,7 +7,7 @@ const adminCore = require('./admin');
|
|
|
7
7
|
const { Connector } = require('../models/dynamo/connectorSchema');
|
|
8
8
|
const { handleDatabaseError } = require('../lib/errorHandler');
|
|
9
9
|
|
|
10
|
-
async function onOAuthCallback({ platform, hostname, tokenUrl, query, isFromMCP = false }) {
|
|
10
|
+
async function onOAuthCallback({ platform, hostname, tokenUrl, query, hashedRcExtensionId, isFromMCP = false }) {
|
|
11
11
|
const callbackUri = query.callbackUri;
|
|
12
12
|
const apiUrl = query.apiUrl;
|
|
13
13
|
const username = query.username;
|
|
@@ -54,6 +54,7 @@ async function onOAuthCallback({ platform, hostname, tokenUrl, query, isFromMCP
|
|
|
54
54
|
refreshToken,
|
|
55
55
|
tokenExpiry: isNaN(expires) ? null : expires,
|
|
56
56
|
rcAccountId: query?.rcAccountId,
|
|
57
|
+
hashedRcExtensionId,
|
|
57
58
|
proxyId
|
|
58
59
|
});
|
|
59
60
|
}
|
|
@@ -76,7 +77,7 @@ async function onOAuthCallback({ platform, hostname, tokenUrl, query, isFromMCP
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
async function onApiKeyLogin({ platform, hostname, apiKey, proxyId, additionalInfo }) {
|
|
80
|
+
async function onApiKeyLogin({ platform, hostname, apiKey, proxyId, rcAccountId, hashedRcExtensionId, additionalInfo }) {
|
|
80
81
|
const platformModule = connectorRegistry.getConnector(platform);
|
|
81
82
|
const basicAuth = platformModule.getBasicAuth({ apiKey });
|
|
82
83
|
const { successful, platformUserInfo, returnMessage } = await platformModule.getUserInfo({ authHeader: `Basic ${basicAuth}`, hostname, platform, additionalInfo, apiKey, proxyId });
|
|
@@ -88,6 +89,8 @@ async function onApiKeyLogin({ platform, hostname, apiKey, proxyId, additionalIn
|
|
|
88
89
|
platform,
|
|
89
90
|
hostname,
|
|
90
91
|
proxyId,
|
|
92
|
+
hashedRcExtensionId,
|
|
93
|
+
rcAccountId,
|
|
91
94
|
accessToken: platformUserInfo.overridingApiKey ?? apiKey
|
|
92
95
|
});
|
|
93
96
|
}
|
|
@@ -110,7 +113,7 @@ async function onApiKeyLogin({ platform, hostname, apiKey, proxyId, additionalIn
|
|
|
110
113
|
}
|
|
111
114
|
}
|
|
112
115
|
|
|
113
|
-
async function saveUserInfo({ platformUserInfo, platform, hostname, accessToken, refreshToken, tokenExpiry, rcAccountId, proxyId }) {
|
|
116
|
+
async function saveUserInfo({ platformUserInfo, platform, hostname, accessToken, refreshToken, tokenExpiry, rcAccountId, hashedRcExtensionId, proxyId }) {
|
|
114
117
|
const id = platformUserInfo.id;
|
|
115
118
|
const name = platformUserInfo.name;
|
|
116
119
|
const existingUser = await UserModel.findByPk(id);
|
|
@@ -130,6 +133,7 @@ async function saveUserInfo({ platformUserInfo, platform, hostname, accessToken,
|
|
|
130
133
|
refreshToken,
|
|
131
134
|
tokenExpiry,
|
|
132
135
|
rcAccountId,
|
|
136
|
+
hashedRcExtensionId,
|
|
133
137
|
platformAdditionalInfo: {
|
|
134
138
|
...existingUser.platformAdditionalInfo, // keep existing platformAdditionalInfo
|
|
135
139
|
...platformAdditionalInfo,
|
|
@@ -143,57 +147,20 @@ async function saveUserInfo({ platformUserInfo, platform, hostname, accessToken,
|
|
|
143
147
|
}
|
|
144
148
|
else {
|
|
145
149
|
try {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
rcAccountId,
|
|
161
|
-
platformAdditionalInfo,
|
|
162
|
-
userSettings: userWithOldID.userSettings
|
|
163
|
-
});
|
|
164
|
-
await userWithOldID.destroy();
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
await UserModel.create({
|
|
168
|
-
id,
|
|
169
|
-
hostname,
|
|
170
|
-
timezoneName,
|
|
171
|
-
timezoneOffset,
|
|
172
|
-
platform,
|
|
173
|
-
accessToken,
|
|
174
|
-
refreshToken,
|
|
175
|
-
tokenExpiry,
|
|
176
|
-
rcAccountId,
|
|
177
|
-
platformAdditionalInfo,
|
|
178
|
-
userSettings: {}
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
await UserModel.create({
|
|
184
|
-
id,
|
|
185
|
-
hostname,
|
|
186
|
-
timezoneName,
|
|
187
|
-
timezoneOffset,
|
|
188
|
-
platform,
|
|
189
|
-
accessToken,
|
|
190
|
-
refreshToken,
|
|
191
|
-
tokenExpiry,
|
|
192
|
-
rcAccountId,
|
|
193
|
-
platformAdditionalInfo,
|
|
194
|
-
userSettings: {}
|
|
195
|
-
});
|
|
196
|
-
}
|
|
150
|
+
await UserModel.create({
|
|
151
|
+
id,
|
|
152
|
+
hostname,
|
|
153
|
+
timezoneName,
|
|
154
|
+
timezoneOffset,
|
|
155
|
+
platform,
|
|
156
|
+
accessToken,
|
|
157
|
+
refreshToken,
|
|
158
|
+
tokenExpiry,
|
|
159
|
+
rcAccountId,
|
|
160
|
+
hashedRcExtensionId,
|
|
161
|
+
platformAdditionalInfo,
|
|
162
|
+
userSettings: {}
|
|
163
|
+
});
|
|
197
164
|
}
|
|
198
165
|
catch (error) {
|
|
199
166
|
return handleDatabaseError(error, 'Error saving user info');
|
|
@@ -206,8 +173,16 @@ async function saveUserInfo({ platformUserInfo, platform, hostname, accessToken,
|
|
|
206
173
|
}
|
|
207
174
|
|
|
208
175
|
async function getLicenseStatus({ userId, platform }) {
|
|
176
|
+
const user = await UserModel.findByPk(userId);
|
|
177
|
+
if (!user) {
|
|
178
|
+
return {
|
|
179
|
+
isLicenseValid: false,
|
|
180
|
+
licenseStatus: 'Invalid (User not found)',
|
|
181
|
+
licenseStatusDescription: ''
|
|
182
|
+
}
|
|
183
|
+
}
|
|
209
184
|
const platformModule = connectorRegistry.getConnector(platform);
|
|
210
|
-
const licenseStatus = await platformModule.getLicenseStatus({ userId, platform });
|
|
185
|
+
const licenseStatus = await platformModule.getLicenseStatus({ userId, platform, user });
|
|
211
186
|
return licenseStatus;
|
|
212
187
|
}
|
|
213
188
|
|
package/handlers/contact.js
CHANGED
|
@@ -80,7 +80,8 @@ async function findContact({ platform, userId, phoneNumber, overridingFormat, is
|
|
|
80
80
|
const { successful, matchedContactInfo, returnMessage, extraDataTracking } = await platformModule.findContact({ user, authHeader, phoneNumber, overridingFormat, isExtension, proxyConfig, tracer, isForceRefreshAccountData });
|
|
81
81
|
tracer?.trace('handler.findContact:platformFindResult', { successful, matchedContactInfo });
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
const matchedNonNewContacts = matchedContactInfo?.filter(c => !c.isNewContact) ?? [];
|
|
84
|
+
if (matchedContactInfo != null && matchedNonNewContacts.length > 0) {
|
|
84
85
|
tracer?.trace('handler.findContact:contactsFound', { count: matchedContactInfo.length });
|
|
85
86
|
// save in org data
|
|
86
87
|
// Danger: it does NOT support one RC account mapping to multiple CRM platforms, because contacts will be shared
|
|
@@ -104,6 +105,10 @@ async function findContact({ platform, userId, phoneNumber, overridingFormat, is
|
|
|
104
105
|
}
|
|
105
106
|
else {
|
|
106
107
|
tracer?.trace('handler.findContact:noContactsMatched', { matchedContactInfo });
|
|
108
|
+
if (isForceRefreshAccountData && existingMatchedContactInfo) {
|
|
109
|
+
await existingMatchedContactInfo.destroy();
|
|
110
|
+
tracer?.trace('handler.findContact:staleCacheRemoved', { phoneNumber });
|
|
111
|
+
}
|
|
107
112
|
if (returnMessage) {
|
|
108
113
|
return {
|
|
109
114
|
successful,
|
|
@@ -272,4 +277,4 @@ async function findContactWithName({ platform, userId, name }) {
|
|
|
272
277
|
|
|
273
278
|
exports.findContact = findContact;
|
|
274
279
|
exports.createContact = createContact;
|
|
275
|
-
exports.findContactWithName = findContactWithName;
|
|
280
|
+
exports.findContactWithName = findContactWithName;
|
package/handlers/log.js
CHANGED
|
@@ -129,11 +129,11 @@ async function createCallLog({ jwtToken, platform, userId, incomingData, hashedA
|
|
|
129
129
|
let pluginDataResponse = null;
|
|
130
130
|
switch (pluginSetting.value.access) {
|
|
131
131
|
case 'public':
|
|
132
|
-
pluginDataResponse = await axios.get(
|
|
132
|
+
pluginDataResponse = await axios.get(`https://appconnect.labs.ringcentral.com/public-api/connectors/${pluginId}/manifest?type=plugin`);
|
|
133
133
|
break;
|
|
134
134
|
case 'private':
|
|
135
135
|
case 'shared':
|
|
136
|
-
pluginDataResponse = await axios.get(
|
|
136
|
+
pluginDataResponse = await axios.get(`https://appconnect.labs.ringcentral.com/public-api/connectors/${pluginId}/manifest?access=internal&type=connector&accountId=${user.rcAccountId}`);
|
|
137
137
|
break;
|
|
138
138
|
default:
|
|
139
139
|
throw new Error('Invalid plugin access');
|
|
@@ -174,6 +174,7 @@ async function createCallLog({ jwtToken, platform, userId, incomingData, hashedA
|
|
|
174
174
|
});
|
|
175
175
|
// eslint-disable-next-line no-param-reassign
|
|
176
176
|
incomingData = processedResultResponse.data;
|
|
177
|
+
note = incomingData.note;
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
180
|
|
package/index.js
CHANGED
|
@@ -3,6 +3,7 @@ const cors = require('cors')
|
|
|
3
3
|
const bodyParser = require('body-parser');
|
|
4
4
|
require('body-parser-xml')(bodyParser);
|
|
5
5
|
const dynamoose = require('dynamoose');
|
|
6
|
+
const Sequelize = require('sequelize');
|
|
6
7
|
const { DynamoDB } = require('@aws-sdk/client-dynamodb');
|
|
7
8
|
const axios = require('axios');
|
|
8
9
|
const { UserModel } = require('./models/userModel');
|
|
@@ -40,7 +41,7 @@ try {
|
|
|
40
41
|
packageJson = require('./package.json');
|
|
41
42
|
}
|
|
42
43
|
catch (e) {
|
|
43
|
-
logger.error('Error loading package.json', { stack: e.stack });
|
|
44
|
+
logger.error('Error loading package.json', { stack: e.stack });
|
|
44
45
|
packageJson = require('../package.json');
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -73,6 +74,20 @@ async function initDB() {
|
|
|
73
74
|
await CacheModel.sync();
|
|
74
75
|
await CallDownListModel.sync();
|
|
75
76
|
await AccountDataModel.sync();
|
|
77
|
+
|
|
78
|
+
// if UserModel doesn't have hashedRcExtensionId column, add it
|
|
79
|
+
const queryInterface = UserModel.sequelize.getQueryInterface();
|
|
80
|
+
const userTableName = UserModel.getTableName();
|
|
81
|
+
const userTableSchema = await queryInterface.describeTable(userTableName);
|
|
82
|
+
if (!userTableSchema.hashedRcExtensionId) {
|
|
83
|
+
logger.info('adding hashedRcExtensionId column to users table...');
|
|
84
|
+
await queryInterface.addColumn(userTableName, 'hashedRcExtensionId', {
|
|
85
|
+
type: Sequelize.STRING,
|
|
86
|
+
allowNull: true,
|
|
87
|
+
});
|
|
88
|
+
await UserModel.sync();
|
|
89
|
+
logger.info('hashedRcExtensionId column added to users table');
|
|
90
|
+
}
|
|
76
91
|
}
|
|
77
92
|
}
|
|
78
93
|
|
|
@@ -870,6 +885,7 @@ function createCoreRouter() {
|
|
|
870
885
|
tokenUrl,
|
|
871
886
|
query: req.query,
|
|
872
887
|
proxyId: req.query.proxyId,
|
|
888
|
+
hashedRcExtensionId: hashedExtensionId,
|
|
873
889
|
isFromMCP
|
|
874
890
|
});
|
|
875
891
|
if (userInfo) {
|
|
@@ -951,12 +967,7 @@ function createCoreRouter() {
|
|
|
951
967
|
res.status(400).send(tracer ? tracer.wrapResponse('Missing platform name') : 'Missing platform name');
|
|
952
968
|
return;
|
|
953
969
|
}
|
|
954
|
-
|
|
955
|
-
tracer?.trace('apiKeyLogin:missingApiKey', {});
|
|
956
|
-
res.status(400).send(tracer ? tracer.wrapResponse('Missing api key') : 'Missing api key');
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
|
-
const { userInfo, returnMessage } = await authCore.onApiKeyLogin({ platform, hostname, apiKey, proxyId, additionalInfo });
|
|
970
|
+
const { userInfo, returnMessage } = await authCore.onApiKeyLogin({ platform, hostname, apiKey, proxyId, rcAccountId: req.body.rcAccountId, hashedRcExtensionId: hashedExtensionId, additionalInfo });
|
|
960
971
|
if (userInfo) {
|
|
961
972
|
const jwtToken = jwt.generateJwt({
|
|
962
973
|
id: userInfo.id.toString(),
|
|
@@ -1341,7 +1352,7 @@ function createCoreRouter() {
|
|
|
1341
1352
|
if (extraDataTracking) {
|
|
1342
1353
|
extraData = extraDataTracking;
|
|
1343
1354
|
}
|
|
1344
|
-
|
|
1355
|
+
res.status(200).send(tracer ? tracer.wrapResponse({ successful, logId, returnMessage, pluginAsyncTaskIds }) : { successful, logId, returnMessage, pluginAsyncTaskIds });
|
|
1345
1356
|
success = true;
|
|
1346
1357
|
}
|
|
1347
1358
|
}
|
package/mcp/README.md
CHANGED
|
@@ -60,7 +60,7 @@ A stateless, hand-rolled JSON-RPC handler — no `@modelcontextprotocol/sdk`, no
|
|
|
60
60
|
- Defines `inputSchema` (JSON Schema) for every tool that takes parameters — required so ChatGPT forwards arguments
|
|
61
61
|
- Injects `rcAccessToken`, `openaiSessionId`, and `rcExtensionId` into every `tools/call` request
|
|
62
62
|
- Verifies the RC access token against the RC API and caches `rcExtensionId` in `CacheModel` keyed by `openaiSessionId` (24h TTL) — subsequent requests hit the cache instead of the RC API
|
|
63
|
-
- Automatically looks up and injects `jwtToken` from `LlmSessionModel` using `rcExtensionId` (or `openaiSessionId` as a fallback)
|
|
63
|
+
- Automatically looks up and injects `jwtToken` from `LlmSessionModel` using `rcExtensionId` (or `openaiSessionId` as a fallback), only when the linked `User` row still has a CRM `accessToken`
|
|
64
64
|
- Stamps `WIDGET_URI` into `getPublicConnectors`'s `_meta['openai/outputTemplate']` at response time
|
|
65
65
|
- Serves the widget HTML via `resources/read`
|
|
66
66
|
- Exposes `handleWidgetToolCall` which searches both `tools.tools` and `tools.widgetTools`
|
|
@@ -116,7 +116,9 @@ Tools are split into two registries in `tools/index.js`:
|
|
|
116
116
|
| `rcExtensionId` | RC API (`/extension/~`), verified once and cached in `sessionContext` | Cryptographically verified RC identity; used as `LlmSessionModel` key |
|
|
117
117
|
| `jwtToken` | `LlmSessionModel.findByPk(rcExtensionId)` (fallback: `findByPk(openaiSessionId)`) | CRM auth token (after OAuth) |
|
|
118
118
|
|
|
119
|
-
Tools do **not** need ChatGPT to pass `jwtToken` explicitly — it is resolved from the session automatically. The `rcExtensionId` is verified via the RC API on the **first tool call** of each session and cached for all subsequent calls within the same conversation (0 additional API calls after that).
|
|
119
|
+
Tools do **not** need ChatGPT to pass `jwtToken` explicitly — it is resolved from the session automatically. The `rcExtensionId` is verified via the RC API on the **first tool call** of each session and cached for all subsequent calls within the same conversation (0 additional API calls after that). If the user has logged out or the CRM token was cleared, `jwtToken` is not injected even when a stale row exists in `LlmSessionModel`.
|
|
120
|
+
|
|
121
|
+
New or refreshed LLM session JWTs are written only when the user record has an `accessToken`, so disconnected users are not issued a new tool JWT.
|
|
120
122
|
|
|
121
123
|
Note: `widgetTools` are called via `POST /mcp/widget-tool-call` which bypasses the MCP session layer entirely. No server-side injection occurs for widget tool calls — all required values must be passed explicitly by the widget in the request body.
|
|
122
124
|
|
|
@@ -149,6 +151,7 @@ Logs out user from the CRM platform.
|
|
|
149
151
|
| Destructive | Yes |
|
|
150
152
|
| Parameters | `jwtToken` (optional — injected from session if not passed) |
|
|
151
153
|
| Action | Clears user credentials |
|
|
154
|
+
| Note | Local session cleanup always runs. Failures from the connector `unAuthorize` call are logged; the tool still returns success so the client can clear context. |
|
|
152
155
|
|
|
153
156
|
#### Contact & Call Log Tools
|
|
154
157
|
|
package/mcp/mcpHandler.js
CHANGED
|
@@ -10,6 +10,9 @@ const axios = require('axios');
|
|
|
10
10
|
const tools = require('./tools');
|
|
11
11
|
const { LlmSessionModel } = require('../models/llmSessionModel');
|
|
12
12
|
const { CacheModel } = require('../models/cacheModel');
|
|
13
|
+
const { UserModel } = require('../models/userModel');
|
|
14
|
+
const { getHashValue } = require('../lib/util');
|
|
15
|
+
const jwt = require('../lib/jwt');
|
|
13
16
|
const logger = require('../lib/logger');
|
|
14
17
|
const fs = require('fs');
|
|
15
18
|
const path = require('path');
|
|
@@ -18,7 +21,7 @@ const path = require('path');
|
|
|
18
21
|
* Increment this to bust ChatGPT's widget resource cache after every UI build.
|
|
19
22
|
* This is the single source of truth — injected into getPublicConnectors _meta at response time.
|
|
20
23
|
*/
|
|
21
|
-
const WIDGET_VERSION =
|
|
24
|
+
const WIDGET_VERSION = 10;
|
|
22
25
|
const WIDGET_URI = `ui://widget/ConnectorList-v${WIDGET_VERSION}.html`;
|
|
23
26
|
|
|
24
27
|
const JSON_RPC_INTERNAL_ERROR = -32603;
|
|
@@ -206,8 +209,30 @@ async function handleMcpRequest(req, res) {
|
|
|
206
209
|
await LlmSessionModel.upsert({ id: rcExtensionId, jwtToken: fallback.jwtToken });
|
|
207
210
|
llmSession = fallback;
|
|
208
211
|
}
|
|
212
|
+
else {
|
|
213
|
+
const hashedRcExtensionId = getHashValue(rcExtensionId, process.env.HASH_KEY);
|
|
214
|
+
const user = await UserModel.findOne({ where: { hashedRcExtensionId } });
|
|
215
|
+
if (user?.accessToken) {
|
|
216
|
+
await LlmSessionModel.upsert({
|
|
217
|
+
id: rcExtensionId,
|
|
218
|
+
jwtToken: jwt.generateJwt({
|
|
219
|
+
id: user.id.toString(),
|
|
220
|
+
platform: user.platform
|
|
221
|
+
}),
|
|
222
|
+
});
|
|
223
|
+
llmSession = await LlmSessionModel.findByPk(rcExtensionId);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (llmSession?.jwtToken) {
|
|
228
|
+
const { id: userId } = jwt.decodeJwt(llmSession.jwtToken);
|
|
229
|
+
if (userId) {
|
|
230
|
+
const user = await UserModel.findByPk(userId);
|
|
231
|
+
if (user?.accessToken) {
|
|
232
|
+
toolArgs.jwtToken = llmSession.jwtToken;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
209
235
|
}
|
|
210
|
-
if (llmSession?.jwtToken) toolArgs.jwtToken = llmSession.jwtToken;
|
|
211
236
|
}
|
|
212
237
|
}
|
|
213
238
|
|
|
@@ -9,7 +9,7 @@ const { LlmSessionModel } = require('../../models/llmSessionModel');
|
|
|
9
9
|
|
|
10
10
|
const toolDefinition = {
|
|
11
11
|
name: 'checkAuthStatus',
|
|
12
|
-
description: '
|
|
12
|
+
description: 'Check the status of an ongoing OAuth authentication session. Poll this after user clicks the auth link.',
|
|
13
13
|
inputSchema: {
|
|
14
14
|
type: 'object',
|
|
15
15
|
properties: {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const axios = require('axios');
|
|
2
|
+
const { UserModel } = require('../../models/userModel');
|
|
3
|
+
const { getHashValue } = require('../../lib/util');
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* MCP Tool: Get Public Connectors
|
|
@@ -52,14 +54,30 @@ async function execute({ rcAccessToken, openaiSessionId } = {}) {
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
// Check if user session already exists from Chrome extension
|
|
58
|
+
const hashedRcExtensionId = getHashValue(rcExtensionId, process.env.HASH_KEY);
|
|
59
|
+
const user = await UserModel.findOne({ where: { hashedRcExtensionId } });
|
|
60
|
+
// Case: user exists, return user info in plain message
|
|
61
|
+
if (user?.accessToken) {
|
|
62
|
+
return {
|
|
63
|
+
structuredContent: {
|
|
64
|
+
error: true,
|
|
65
|
+
errorMessage: `You are already connected to ${user.platform}. It's controlled from App Connect Chrome extension.`
|
|
66
|
+
}
|
|
61
67
|
}
|
|
62
|
-
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// Case: user doesn't exist, return structured content for widget
|
|
71
|
+
return {
|
|
72
|
+
structuredContent: {
|
|
73
|
+
serverUrl: process.env.APP_SERVER || 'https://localhost:6066',
|
|
74
|
+
rcExtensionId,
|
|
75
|
+
rcAccountId,
|
|
76
|
+
openaiSessionId: openaiSessionId ?? null,
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
63
81
|
}
|
|
64
82
|
|
|
65
83
|
exports.definition = toolDefinition;
|
package/mcp/tools/logout.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const jwt = require('../../lib/jwt');
|
|
2
2
|
const { UserModel } = require('../../models/userModel');
|
|
3
|
+
const { LlmSessionModel } = require('../../models/llmSessionModel');
|
|
3
4
|
const connectorRegistry = require('../../connector/registry');
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -22,6 +23,13 @@ const toolDefinition = {
|
|
|
22
23
|
}
|
|
23
24
|
};
|
|
24
25
|
|
|
26
|
+
function isMissingSessionTableError(error) {
|
|
27
|
+
const message = error?.message || '';
|
|
28
|
+
return message.includes('no such table: llmSessions')
|
|
29
|
+
|| message.includes('relation "llmSessions" does not exist')
|
|
30
|
+
|| message.includes("relation 'llmSessions' does not exist");
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
/**
|
|
26
34
|
* Execute the logout tool
|
|
27
35
|
* @param {Object} args - The tool arguments
|
|
@@ -31,8 +39,19 @@ const toolDefinition = {
|
|
|
31
39
|
async function execute(args) {
|
|
32
40
|
try {
|
|
33
41
|
const { jwtToken } = args;
|
|
34
|
-
const
|
|
35
|
-
|
|
42
|
+
const session = jwt.decodeJwt(jwtToken);
|
|
43
|
+
if (!session?.platform || !session?.id) {
|
|
44
|
+
throw new Error('Invalid JWT token');
|
|
45
|
+
}
|
|
46
|
+
const { platform, id } = session;
|
|
47
|
+
try {
|
|
48
|
+
await LlmSessionModel.destroy({ where: { id } });
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (!isMissingSessionTableError(error)) {
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
36
55
|
const userToLogout = await UserModel.findByPk(id);
|
|
37
56
|
if (!userToLogout) {
|
|
38
57
|
return {
|
|
@@ -42,7 +61,12 @@ async function execute(args) {
|
|
|
42
61
|
};
|
|
43
62
|
}
|
|
44
63
|
const platformModule = connectorRegistry.getConnector(platform);
|
|
45
|
-
|
|
64
|
+
try {
|
|
65
|
+
await platformModule.unAuthorize({ user: userToLogout });
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.log(error);
|
|
69
|
+
}
|
|
46
70
|
return {
|
|
47
71
|
success: true,
|
|
48
72
|
data: {
|
|
@@ -60,4 +84,4 @@ async function execute(args) {
|
|
|
60
84
|
}
|
|
61
85
|
|
|
62
86
|
exports.definition = toolDefinition;
|
|
63
|
-
exports.execute = execute;
|
|
87
|
+
exports.execute = execute;
|
package/mcp/ui/App/App.tsx
CHANGED
|
@@ -6,7 +6,6 @@ import { AuthSuccess } from './components/AuthSuccess'
|
|
|
6
6
|
import { setServerUrl } from './lib/callTool'
|
|
7
7
|
import { fetchConnectors, fetchManifest } from './lib/developerPortal'
|
|
8
8
|
import { dbg } from './lib/debugLog'
|
|
9
|
-
import { DebugPanel } from './components/DebugPanel'
|
|
10
9
|
|
|
11
10
|
// Initial structuredContent from getPublicConnectors — serverUrl, rcAccountId, rcExtensionId, openaiSessionId
|
|
12
11
|
interface ToolOutput {
|
|
@@ -267,10 +266,9 @@ export function App() {
|
|
|
267
266
|
if (data.error) {
|
|
268
267
|
return (
|
|
269
268
|
<div className="p-4">
|
|
270
|
-
<div className="rounded-lg border border-
|
|
271
|
-
<p className="text-
|
|
269
|
+
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-4">
|
|
270
|
+
<p className="text-yellow-800">{data.errorMessage || 'An error occurred'}</p>
|
|
272
271
|
</div>
|
|
273
|
-
<DebugPanel />
|
|
274
272
|
</div>
|
|
275
273
|
)
|
|
276
274
|
}
|
|
@@ -353,7 +351,6 @@ export function App() {
|
|
|
353
351
|
>
|
|
354
352
|
← Back to connector list
|
|
355
353
|
</button>
|
|
356
|
-
<DebugPanel />
|
|
357
354
|
</div>
|
|
358
355
|
)}
|
|
359
356
|
</div>
|