@app-connect/core 1.7.32 → 1.7.33
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/docs/connectors.md +1 -1
- package/docs/handlers.md +11 -8
- package/docs/libraries.md +0 -3
- package/docs/models.md +5 -5
- package/docs/routes.md +5 -1
- package/handlers/log.js +431 -167
- package/handlers/plugin.js +0 -24
- package/handlers/user.js +96 -9
- package/index.js +58 -46
- package/mcp/README.md +8 -0
- package/mcp/mcpHandler.js +126 -21
- package/package.json +1 -1
- package/releaseNotes.json +24 -0
- package/test/handlers/log.test.js +225 -0
- package/test/handlers/plugin.test.js +0 -256
- package/test/index.test.js +31 -0
- package/test/mcp/mcpHandler.test.js +73 -0
package/handlers/plugin.js
CHANGED
|
@@ -51,29 +51,6 @@ async function getPluginLicenseStatus({ rcAccountId, pluginId }) {
|
|
|
51
51
|
return licenseStatusResponse.data;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
async function getPluginAsyncTasks({ asyncTaskIds }) {
|
|
55
|
-
const caches = await CacheModel.findAll({
|
|
56
|
-
where: {
|
|
57
|
-
id: {
|
|
58
|
-
[Op.in]: asyncTaskIds
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
const result = caches.map(cache => ({
|
|
63
|
-
cacheKey: cache.cacheKey,
|
|
64
|
-
status: cache.status
|
|
65
|
-
}));
|
|
66
|
-
const toRemoveCaches = caches.filter(cache => cache.status === 'completed' || cache.status === 'failed');
|
|
67
|
-
await CacheModel.destroy({
|
|
68
|
-
where: {
|
|
69
|
-
id: {
|
|
70
|
-
[Op.in]: toRemoveCaches.map(cache => cache.id)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
return result;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
54
|
function getRefreshedJwtTokenFromHeaders({ headers }) {
|
|
78
55
|
if (!headers) {
|
|
79
56
|
return null;
|
|
@@ -201,7 +178,6 @@ async function unregisterPluginAccount({ pluginId, rcAccountId }) {
|
|
|
201
178
|
exports.getPluginsFromRcAccountId = getPluginsFromRcAccountId;
|
|
202
179
|
exports.getPluginConfigFromUserSettings = getPluginConfigFromUserSettings;
|
|
203
180
|
exports.getPluginLicenseStatus = getPluginLicenseStatus;
|
|
204
|
-
exports.getPluginAsyncTasks = getPluginAsyncTasks;
|
|
205
181
|
exports.getRefreshedJwtTokenFromHeaders = getRefreshedJwtTokenFromHeaders;
|
|
206
182
|
exports.resolvePluginManifest = resolvePluginManifest;
|
|
207
183
|
exports.persistPluginData = persistPluginData;
|
package/handlers/user.js
CHANGED
|
@@ -4,6 +4,83 @@ const { getHashValue } = require('../lib/util');
|
|
|
4
4
|
const connectorRegistry = require('../connector/registry');
|
|
5
5
|
const logger = require('../lib/logger');
|
|
6
6
|
const { handleDatabaseError } = require('../lib/errorHandler');
|
|
7
|
+
const { UserModel } = require('../models/userModel');
|
|
8
|
+
const { handleApiError } = require('../lib/errorHandler');
|
|
9
|
+
const oauth = require('../lib/oauth');
|
|
10
|
+
const { Connector } = require('../models/dynamo/connectorSchema');
|
|
11
|
+
|
|
12
|
+
async function refreshUserInfo({ platform, userId, tracer }) {
|
|
13
|
+
tracer?.trace('refreshUserInfo:start', { platform, userId });
|
|
14
|
+
try {
|
|
15
|
+
let user = await UserModel.findOne({
|
|
16
|
+
where: {
|
|
17
|
+
id: userId,
|
|
18
|
+
platform
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
tracer?.trace('refreshUserInfo:userFound', { user });
|
|
22
|
+
|
|
23
|
+
if (!user || !user.accessToken) {
|
|
24
|
+
tracer?.trace('refreshUserInfo:noUser', { userId });
|
|
25
|
+
return {
|
|
26
|
+
successful: false,
|
|
27
|
+
returnMessage: {
|
|
28
|
+
message: `User not found`,
|
|
29
|
+
messageType: 'warning',
|
|
30
|
+
ttl: 5000
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const proxyId = user.platformAdditionalInfo?.proxyId;
|
|
36
|
+
let proxyConfig = null;
|
|
37
|
+
if (proxyId) {
|
|
38
|
+
proxyConfig = await Connector.getProxyConfig(proxyId);
|
|
39
|
+
tracer?.trace('refreshUserInfo:proxyConfig', { proxyConfig });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const platformModule = connectorRegistry.getConnector(platform);
|
|
43
|
+
const authType = await platformModule.getAuthType({ proxyId, proxyConfig });
|
|
44
|
+
tracer?.trace('refreshUserInfo:authType', { authType });
|
|
45
|
+
|
|
46
|
+
let authHeader = '';
|
|
47
|
+
switch (authType) {
|
|
48
|
+
case 'oauth':
|
|
49
|
+
const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: user?.platformAdditionalInfo?.tokenUrl, hostname: user?.hostname, proxyId, proxyConfig })));
|
|
50
|
+
user = await oauth.checkAndRefreshAccessToken(oauthApp, user);
|
|
51
|
+
if (!user) {
|
|
52
|
+
return {
|
|
53
|
+
successful: false,
|
|
54
|
+
returnMessage: {
|
|
55
|
+
message: `User session expired. Please connect again.`,
|
|
56
|
+
messageType: 'warning',
|
|
57
|
+
ttl: 5000
|
|
58
|
+
},
|
|
59
|
+
isRevokeUserSession: true
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
authHeader = `Bearer ${user.accessToken}`;
|
|
63
|
+
tracer?.trace('refreshUserInfo:oauthAuth', { authHeader });
|
|
64
|
+
break;
|
|
65
|
+
case 'apiKey':
|
|
66
|
+
const basicAuth = platformModule.getBasicAuth({ apiKey: user.accessToken });
|
|
67
|
+
authHeader = `Basic ${basicAuth}`;
|
|
68
|
+
tracer?.trace('refreshUserInfo:apiKeyAuth', {});
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { successful, returnMessage } = await platformModule.refreshUserInfo({ user, authHeader, proxyConfig });
|
|
73
|
+
tracer?.trace('refreshUserInfo:platformRefreshResult', { successful, returnMessage });
|
|
74
|
+
return {
|
|
75
|
+
successful,
|
|
76
|
+
returnMessage
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
tracer?.traceError('refreshUserInfo:error', e, { platform, userId });
|
|
81
|
+
return handleApiError(e, platform, 'refreshUserInfo', { userId });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
7
84
|
|
|
8
85
|
async function getUserSettingsByAdmin({ rcAccessToken, rcAccountId }) {
|
|
9
86
|
let hashedRcAccountId = null;
|
|
@@ -63,22 +140,31 @@ async function getUserSettings({ user, rcAccessToken, rcAccountId }) {
|
|
|
63
140
|
// Special case: plugins
|
|
64
141
|
if (key.startsWith('plugin_')) {
|
|
65
142
|
const config = Object.keys(result[key].value.config)?.length === 0 ? null : result[key].value.config;
|
|
143
|
+
const configFromAdminSettings = userSettingsByAdmin.userSettings[key]?.value?.config ?? null;
|
|
66
144
|
if (config) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
145
|
+
if (configFromAdminSettings) {
|
|
146
|
+
for (const k in config) {
|
|
147
|
+
// use admin setting to replace, if not customizable
|
|
148
|
+
if (configFromAdminSettings[k] && !configFromAdminSettings[k].customizable || !config[k].value && configFromAdminSettings[k].value) {
|
|
149
|
+
config[k] = configFromAdminSettings[k];
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
config[k].customizable = configFromAdminSettings[k]?.customizable ?? true;
|
|
153
|
+
}
|
|
75
154
|
}
|
|
76
155
|
}
|
|
77
156
|
result[key].value.config = config;
|
|
78
157
|
}
|
|
79
158
|
//Case: no config at all, use admin setting directly
|
|
80
159
|
else {
|
|
81
|
-
|
|
160
|
+
if (configFromAdminSettings) {
|
|
161
|
+
result[key].value.config = {
|
|
162
|
+
...configFromAdminSettings
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
delete result[key];
|
|
167
|
+
}
|
|
82
168
|
}
|
|
83
169
|
}
|
|
84
170
|
}
|
|
@@ -138,6 +224,7 @@ async function updateUserSettings({ user, userSettings, settingKeysToRemove, pla
|
|
|
138
224
|
};
|
|
139
225
|
}
|
|
140
226
|
|
|
227
|
+
exports.refreshUserInfo = refreshUserInfo;
|
|
141
228
|
exports.getUserSettingsByAdmin = getUserSettingsByAdmin;
|
|
142
229
|
exports.getUserSettings = getUserSettings;
|
|
143
230
|
exports.updateUserSettings = updateUserSettings;
|
package/index.js
CHANGED
|
@@ -260,6 +260,7 @@ function createCoreRouter() {
|
|
|
260
260
|
result.getUserList = !!platformModule.getUserList;
|
|
261
261
|
result.getLicenseStatus = !!platformModule.getLicenseStatus;
|
|
262
262
|
result.getLogFormatType = !!platformModule.getLogFormatType;
|
|
263
|
+
result.refreshUserInfo = !!platformModule.refreshUserInfo;
|
|
263
264
|
result.cacheCallNote = !!process.env.USE_CACHE;
|
|
264
265
|
res.status(200).send(tracer ? tracer.wrapResponse(result) : result);
|
|
265
266
|
}
|
|
@@ -984,6 +985,49 @@ function createCoreRouter() {
|
|
|
984
985
|
}
|
|
985
986
|
}
|
|
986
987
|
);
|
|
988
|
+
router.post('/user/refreshInfo', async function (req, res) {
|
|
989
|
+
const requestStartTime = new Date().getTime();
|
|
990
|
+
const tracer = req.headers['is-debug'] === 'true' ? DebugTracer.fromRequest(req) : null;
|
|
991
|
+
tracer?.trace('refreshUserInfo:start', { body: req.body });
|
|
992
|
+
let platformName = null;
|
|
993
|
+
let success = false;
|
|
994
|
+
const { hashedExtensionId, hashedAccountId, userAgent, ip, author, eventAddedVia } = getAnalyticsVariablesInReqHeaders({ headers: req.headers })
|
|
995
|
+
try {
|
|
996
|
+
const jwtToken = req.jwtToken || req.query.jwtToken;
|
|
997
|
+
if (jwtToken) {
|
|
998
|
+
const unAuthData = jwt.decodeJwt(jwtToken);
|
|
999
|
+
platformName = unAuthData?.platform ?? 'Unknown';
|
|
1000
|
+
const userId = unAuthData?.id;
|
|
1001
|
+
const { successful, returnMessage } = await userCore.refreshUserInfo({ platform: platformName, userId, tracer });
|
|
1002
|
+
res.status(200).send(tracer ? tracer.wrapResponse({ successful, returnMessage }) : { successful, returnMessage });
|
|
1003
|
+
success = true;
|
|
1004
|
+
}
|
|
1005
|
+
else {
|
|
1006
|
+
tracer?.trace('refreshUserInfo:noToken', {});
|
|
1007
|
+
res.status(400).send(tracer ? tracer.wrapResponse('Please go to Settings and authorize CRM platform') : 'Please go to Settings and authorize CRM platform');
|
|
1008
|
+
success = false;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
catch (e) {
|
|
1012
|
+
logger.error('Refresh user info failed', { platform: platformName, stack: e.stack });
|
|
1013
|
+
tracer?.traceError('refreshUserInfo:error', e, { platform: platformName });
|
|
1014
|
+
res.status(400).send(tracer ? tracer.wrapResponse({ error: e.message || e }) : { error: e.message || e });
|
|
1015
|
+
}
|
|
1016
|
+
const requestEndTime = new Date().getTime();
|
|
1017
|
+
analytics.track({
|
|
1018
|
+
eventName: 'Refresh user info',
|
|
1019
|
+
interfaceName: 'refreshUserInfo',
|
|
1020
|
+
connectorName: platformName,
|
|
1021
|
+
accountId: hashedAccountId,
|
|
1022
|
+
extensionId: hashedExtensionId,
|
|
1023
|
+
success,
|
|
1024
|
+
requestDuration: (requestEndTime - requestStartTime) / 1000,
|
|
1025
|
+
userAgent,
|
|
1026
|
+
ip,
|
|
1027
|
+
author,
|
|
1028
|
+
eventAddedVia
|
|
1029
|
+
});
|
|
1030
|
+
})
|
|
987
1031
|
router.get('/user/settings', async function (req, res) {
|
|
988
1032
|
const requestStartTime = new Date().getTime();
|
|
989
1033
|
const tracer = req.headers['is-debug'] === 'true' ? DebugTracer.fromRequest(req) : null;
|
|
@@ -2053,7 +2097,7 @@ function createCoreRouter() {
|
|
|
2053
2097
|
}
|
|
2054
2098
|
const { id: userId, platform } = decodedToken;
|
|
2055
2099
|
platformName = platform;
|
|
2056
|
-
const { successful, logId, returnMessage, extraDataTracking,
|
|
2100
|
+
const { successful, logId, returnMessage, extraDataTracking, isRevokeUserSession } = await logCore.createCallLog({ jwtToken, platform, userId, incomingData: req.body, hashedAccountId: hashedAccountId ?? util.getHashValue(req.body.logInfo?.accountId, process.env.HASH_KEY), isFromSSCL: userAgent === 'SSCL' });
|
|
2057
2101
|
if (isRevokeUserSession) {
|
|
2058
2102
|
res.status(401).send(tracer ? tracer.wrapResponse({ successful, returnMessage }) : { successful, returnMessage });
|
|
2059
2103
|
success = false;
|
|
@@ -2062,7 +2106,7 @@ function createCoreRouter() {
|
|
|
2062
2106
|
if (extraDataTracking) {
|
|
2063
2107
|
extraData = extraDataTracking;
|
|
2064
2108
|
}
|
|
2065
|
-
res.status(200).send(tracer ? tracer.wrapResponse({ successful, logId, returnMessage
|
|
2109
|
+
res.status(200).send(tracer ? tracer.wrapResponse({ successful, logId, returnMessage }) : { successful, logId, returnMessage });
|
|
2066
2110
|
success = true;
|
|
2067
2111
|
}
|
|
2068
2112
|
}
|
|
@@ -2116,11 +2160,11 @@ function createCoreRouter() {
|
|
|
2116
2160
|
}
|
|
2117
2161
|
const { id: userId, platform } = decodedToken;
|
|
2118
2162
|
platformName = platform;
|
|
2119
|
-
const { successful, logId, updatedNote, returnMessage, extraDataTracking
|
|
2163
|
+
const { successful, logId, updatedNote, returnMessage, extraDataTracking } = await logCore.updateCallLog({ jwtToken, platform, userId, incomingData: req.body, hashedAccountId: hashedAccountId ?? util.getHashValue(req.body.accountId, process.env.HASH_KEY), isFromSSCL: userAgent === 'SSCL' });
|
|
2120
2164
|
if (extraDataTracking) {
|
|
2121
2165
|
extraData = extraDataTracking;
|
|
2122
2166
|
}
|
|
2123
|
-
res.status(200).send(tracer ? tracer.wrapResponse({ successful, logId, updatedNote, returnMessage
|
|
2167
|
+
res.status(200).send(tracer ? tracer.wrapResponse({ successful, logId, updatedNote, returnMessage }) : { successful, logId, updatedNote, returnMessage });
|
|
2124
2168
|
success = true;
|
|
2125
2169
|
}
|
|
2126
2170
|
else {
|
|
@@ -2685,53 +2729,21 @@ function createCoreRouter() {
|
|
|
2685
2729
|
});
|
|
2686
2730
|
});
|
|
2687
2731
|
|
|
2688
|
-
router.post('/
|
|
2689
|
-
const requestStartTime = new Date().getTime();
|
|
2732
|
+
router.post('/plugin/async-callback/:taskId', async function (req, res) {
|
|
2690
2733
|
const tracer = req.headers['is-debug'] === 'true' ? DebugTracer.fromRequest(req) : null;
|
|
2691
|
-
tracer?.trace('
|
|
2692
|
-
let platformName = null;
|
|
2693
|
-
let success = false;
|
|
2694
|
-
const { hashedExtensionId, hashedAccountId, userAgent, ip, author, eventAddedVia } = getAnalyticsVariablesInReqHeaders({ headers: req.headers })
|
|
2695
|
-
const jwtToken = req.jwtToken || req.query.jwtToken;
|
|
2734
|
+
tracer?.trace('pluginAsyncCallback:start', { taskId: req.params.taskId, body: req.body });
|
|
2696
2735
|
try {
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
const unAuthData = jwt.decodeJwt(jwtToken);
|
|
2703
|
-
const user = await UserModel.findByPk(unAuthData?.id);
|
|
2704
|
-
if (!user) {
|
|
2705
|
-
tracer?.trace('pluginAsyncTask:userNotFound', {});
|
|
2706
|
-
res.status(400).send(tracer ? tracer.wrapResponse('User not found') : 'User not found');
|
|
2707
|
-
return;
|
|
2708
|
-
}
|
|
2709
|
-
const { asyncTaskIds } = req.body;
|
|
2710
|
-
const filteredTasksIds = asyncTaskIds.filter(taskId => taskId.startsWith(user.id));
|
|
2711
|
-
const tasks = await pluginCore.getPluginAsyncTasks({ asyncTaskIds: filteredTasksIds });
|
|
2712
|
-
res.status(200).send(tracer ? tracer.wrapResponse({ tasks }) : { tasks });
|
|
2713
|
-
success = true;
|
|
2736
|
+
const result = await logCore.handleAsyncPluginCallback({
|
|
2737
|
+
taskId: req.params.taskId,
|
|
2738
|
+
body: req.body || {},
|
|
2739
|
+
});
|
|
2740
|
+
res.status(result.statusCode).send(tracer ? tracer.wrapResponse(result.body) : result.body);
|
|
2714
2741
|
}
|
|
2715
2742
|
catch (e) {
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
tracer
|
|
2719
|
-
success = false;
|
|
2743
|
+
logger.error('Plugin async callback failed', { taskId: req.params.taskId, stack: e.stack });
|
|
2744
|
+
tracer?.traceError('pluginAsyncCallback:error', e);
|
|
2745
|
+
res.status(500).send(tracer ? tracer.wrapResponse({ successful: false, message: e.message || e }) : { successful: false, message: e.message || e });
|
|
2720
2746
|
}
|
|
2721
|
-
const requestEndTime = new Date().getTime();
|
|
2722
|
-
analytics.track({
|
|
2723
|
-
eventName: 'Plugin Async Task',
|
|
2724
|
-
interfaceName: 'pluginAsyncTask',
|
|
2725
|
-
connectorName: platformName,
|
|
2726
|
-
accountId: hashedAccountId,
|
|
2727
|
-
extensionId: hashedExtensionId,
|
|
2728
|
-
success,
|
|
2729
|
-
requestDuration: (requestEndTime - requestStartTime) / 1000,
|
|
2730
|
-
userAgent,
|
|
2731
|
-
ip,
|
|
2732
|
-
author,
|
|
2733
|
-
eventAddedVia
|
|
2734
|
-
});
|
|
2735
2747
|
});
|
|
2736
2748
|
|
|
2737
2749
|
router.post('/plugin/register', async function (req, res) {
|
package/mcp/README.md
CHANGED
|
@@ -62,11 +62,13 @@ A stateless, hand-rolled JSON-RPC handler — no `@modelcontextprotocol/sdk`, no
|
|
|
62
62
|
**Key Features:**
|
|
63
63
|
- Defines `WIDGET_VERSION` — the **single source of truth** for the widget cache-busting URI
|
|
64
64
|
- Handles `initialize`, `tools/list`, `tools/call`, `resources/list`, `resources/read`, and `ping` methods
|
|
65
|
+
- Defines `outputSchema` (JSON Schema) for AI-visible tools so clients can understand and validate structured results
|
|
65
66
|
- Defines `inputSchema` (JSON Schema) for every tool that takes parameters — required so ChatGPT forwards arguments
|
|
66
67
|
- Injects `rcAccessToken`, `openaiSessionId`, and `rcExtensionId` into every `tools/call` request
|
|
67
68
|
- 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
|
|
68
69
|
- 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`
|
|
69
70
|
- Stamps `WIDGET_URI` into `getPublicConnectors`'s `_meta['openai/outputTemplate']` at response time
|
|
71
|
+
- Returns `structuredContent` for schema-bearing tool calls and includes serialized JSON text content for backwards compatibility
|
|
70
72
|
- Serves the widget HTML via `resources/read`
|
|
71
73
|
- Exposes `handleWidgetToolCall` which searches both `tools.tools` and `tools.widgetTools`
|
|
72
74
|
|
|
@@ -127,6 +129,12 @@ New or refreshed LLM session JWTs are written only when the user record has an `
|
|
|
127
129
|
|
|
128
130
|
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.
|
|
129
131
|
|
|
132
|
+
### Output Handling
|
|
133
|
+
|
|
134
|
+
`tools/list` includes an `outputSchema` for every AI-visible MCP tool. For `tools/call`, `mcpHandler.js` converts each tool's returned object into MCP `structuredContent` and also returns the same payload as serialized JSON in a text content block for clients that still read only `content`.
|
|
135
|
+
|
|
136
|
+
Tools that return `{ success: false, ... }` are surfaced as regular MCP tool results with `isError: true`, so the model can see the error payload and self-correct.
|
|
137
|
+
|
|
130
138
|
### AI-Visible Tools (`tools`)
|
|
131
139
|
|
|
132
140
|
#### `getHelp`
|
package/mcp/mcpHandler.js
CHANGED
|
@@ -85,6 +85,95 @@ const inputSchemas = {
|
|
|
85
85
|
},
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
+
const standardToolResultOutputSchema = {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
success: { type: 'boolean', description: 'Whether the tool operation completed successfully.' },
|
|
92
|
+
data: { description: 'Tool-specific successful result payload.' },
|
|
93
|
+
error: { type: 'string', description: 'Error message when the operation fails.' },
|
|
94
|
+
errorDetails: { type: 'string', description: 'Diagnostic details for troubleshooting.' },
|
|
95
|
+
message: { type: 'string', description: 'Human-readable status message.' },
|
|
96
|
+
},
|
|
97
|
+
required: ['success'],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* JSON Schema definitions for each tool's structuredContent response.
|
|
102
|
+
* MCP clients use this from tools/list to understand and validate tool output.
|
|
103
|
+
*/
|
|
104
|
+
const outputSchemas = {
|
|
105
|
+
getHelp: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
properties: {
|
|
108
|
+
success: { type: 'boolean' },
|
|
109
|
+
data: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
overview: { type: 'string' },
|
|
113
|
+
steps: {
|
|
114
|
+
type: 'array',
|
|
115
|
+
items: { type: 'string' },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
required: ['overview', 'steps'],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
required: ['success', 'data'],
|
|
122
|
+
},
|
|
123
|
+
getPublicConnectors: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
serverUrl: { type: 'string', description: 'Base URL for widget-to-server tool calls.' },
|
|
127
|
+
rcExtensionId: { description: 'Resolved RingCentral extension ID, or null when unavailable.' },
|
|
128
|
+
rcAccountId: { description: 'Resolved RingCentral account ID, or null when unavailable.' },
|
|
129
|
+
openaiSessionId: { description: 'OpenAI conversation session ID, or null when unavailable.' },
|
|
130
|
+
error: { type: 'boolean', description: 'Whether connector selection is blocked.' },
|
|
131
|
+
errorMessage: { type: 'string', description: 'Reason connector selection is blocked.' },
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
getSessionInfo: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
properties: {
|
|
137
|
+
success: { type: 'boolean' },
|
|
138
|
+
data: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
openaiSessionId: { description: 'OpenAI conversation session ID, or null when unavailable.' },
|
|
142
|
+
dataToShow: {
|
|
143
|
+
type: 'object',
|
|
144
|
+
description: 'Non-sensitive session status for the current RingCentral and CRM connection.',
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
error: { type: 'string' },
|
|
149
|
+
errorDetails: { type: 'string' },
|
|
150
|
+
},
|
|
151
|
+
required: ['success'],
|
|
152
|
+
},
|
|
153
|
+
rcGetCallLogs: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: {
|
|
156
|
+
records: {
|
|
157
|
+
type: 'array',
|
|
158
|
+
description: 'RingCentral call log records.',
|
|
159
|
+
items: { type: 'object' },
|
|
160
|
+
},
|
|
161
|
+
success: { type: 'boolean', description: 'False when fetching call logs fails.' },
|
|
162
|
+
error: { type: 'string', description: 'Error message when fetching call logs fails.' },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
logout: standardToolResultOutputSchema,
|
|
166
|
+
findContactByPhone: standardToolResultOutputSchema,
|
|
167
|
+
findContactByName: standardToolResultOutputSchema,
|
|
168
|
+
createContact: standardToolResultOutputSchema,
|
|
169
|
+
createCallLog: standardToolResultOutputSchema,
|
|
170
|
+
listAppointments: standardToolResultOutputSchema,
|
|
171
|
+
createAppointment: standardToolResultOutputSchema,
|
|
172
|
+
updateAppointment: standardToolResultOutputSchema,
|
|
173
|
+
confirmAppointment: standardToolResultOutputSchema,
|
|
174
|
+
cancelAppointment: standardToolResultOutputSchema,
|
|
175
|
+
};
|
|
176
|
+
|
|
88
177
|
/**
|
|
89
178
|
* Verify an RC access token and return the caller's extension ID.
|
|
90
179
|
* Throws if the token is invalid.
|
|
@@ -138,7 +227,7 @@ async function resolveSessionContext(rcAccessToken, openaiSessionId) {
|
|
|
138
227
|
|
|
139
228
|
/**
|
|
140
229
|
* Build the tools list to return in tools/list responses.
|
|
141
|
-
* Injects
|
|
230
|
+
* Injects schema metadata and stamps WIDGET_URI into getPublicConnectors _meta.
|
|
142
231
|
*/
|
|
143
232
|
function getToolsList() {
|
|
144
233
|
return tools.tools.map(tool => {
|
|
@@ -149,10 +238,41 @@ function getToolsList() {
|
|
|
149
238
|
if (inputSchemas[def.name]) {
|
|
150
239
|
def.inputSchema = inputSchemas[def.name];
|
|
151
240
|
}
|
|
241
|
+
if (outputSchemas[def.name]) {
|
|
242
|
+
def.outputSchema = outputSchemas[def.name];
|
|
243
|
+
}
|
|
152
244
|
return def;
|
|
153
245
|
});
|
|
154
246
|
}
|
|
155
247
|
|
|
248
|
+
function getStructuredContent(result) {
|
|
249
|
+
if (result?.structuredContent && typeof result.structuredContent === 'object' && !Array.isArray(result.structuredContent)) {
|
|
250
|
+
return result.structuredContent;
|
|
251
|
+
}
|
|
252
|
+
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
return { result: result ?? null };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function buildToolResult(toolDefinition, result) {
|
|
259
|
+
const structuredContent = getStructuredContent(result);
|
|
260
|
+
const responseResult = {
|
|
261
|
+
content: Array.isArray(result?.content)
|
|
262
|
+
? result.content
|
|
263
|
+
: [{ type: 'text', text: JSON.stringify(structuredContent, null, 2) }],
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
if (toolDefinition?.outputSchema || outputSchemas[toolDefinition?.name] || result?.structuredContent) {
|
|
267
|
+
responseResult.structuredContent = structuredContent;
|
|
268
|
+
}
|
|
269
|
+
if (structuredContent?.success === false) {
|
|
270
|
+
responseResult.isError = true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return responseResult;
|
|
274
|
+
}
|
|
275
|
+
|
|
156
276
|
/**
|
|
157
277
|
* Handle incoming MCP HTTP requests.
|
|
158
278
|
* Stateless: each POST is handled independently with no session state between requests.
|
|
@@ -265,26 +385,11 @@ async function handleMcpRequest(req, res) {
|
|
|
265
385
|
|
|
266
386
|
const result = await tool.execute(toolArgs);
|
|
267
387
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
structuredContent: result.structuredContent,
|
|
274
|
-
content: Array.isArray(result.content)
|
|
275
|
-
? result.content
|
|
276
|
-
: [{ type: 'text', text: '[Interactive widget displayed above - no additional response needed]' }],
|
|
277
|
-
},
|
|
278
|
-
};
|
|
279
|
-
} else {
|
|
280
|
-
response = {
|
|
281
|
-
jsonrpc: '2.0',
|
|
282
|
-
id,
|
|
283
|
-
result: {
|
|
284
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
285
|
-
},
|
|
286
|
-
};
|
|
287
|
-
}
|
|
388
|
+
response = {
|
|
389
|
+
jsonrpc: '2.0',
|
|
390
|
+
id,
|
|
391
|
+
result: buildToolResult(tool.definition, result),
|
|
392
|
+
};
|
|
288
393
|
} catch (toolError) {
|
|
289
394
|
response = {
|
|
290
395
|
jsonrpc: '2.0',
|
package/package.json
CHANGED
package/releaseNotes.json
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
+
"1.7.33": {
|
|
3
|
+
"global": [
|
|
4
|
+
{
|
|
5
|
+
"type": "New",
|
|
6
|
+
"description": "Developer Console manifest flag to disable Contact Cache"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"type": "New",
|
|
10
|
+
"description": "Interface refreshUserInfo, called when user opens extension"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"type": "New",
|
|
14
|
+
"description": "Async plugin callback"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"type": "New",
|
|
18
|
+
"description": "Developer Console manifest flag to hide plugin config fields"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "Fix",
|
|
22
|
+
"description": "Mark outbound call on answer call pop option deprecated"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
2
26
|
"1.7.30": {
|
|
3
27
|
"global": [
|
|
4
28
|
{
|