@app-connect/core 1.7.32 → 1.7.34

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.
@@ -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
- const configFromadminSettings = userSettingsByAdmin.userSettings[key].value.config ?? {};
68
- for (const k in config) {
69
- // use admin setting to replace, if not customizable
70
- if (configFromadminSettings[k] && !configFromadminSettings[k].customizable || !config[k].value && configFromadminSettings[k].value) {
71
- config[k] = configFromadminSettings[k];
72
- }
73
- else {
74
- config[k].customizable = configFromadminSettings[k]?.customizable ?? true;
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
- result[key].value.config = userSettingsByAdmin.userSettings[key].value.config;
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, pluginAsyncTaskIds, 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' });
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, pluginAsyncTaskIds }) : { successful, logId, returnMessage, pluginAsyncTaskIds });
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, pluginAsyncTaskIds } = await logCore.updateCallLog({ jwtToken, platform, userId, incomingData: req.body, hashedAccountId: hashedAccountId ?? util.getHashValue(req.body.accountId, process.env.HASH_KEY), isFromSSCL: userAgent === 'SSCL' });
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, pluginAsyncTaskIds }) : { successful, logId, updatedNote, returnMessage, pluginAsyncTaskIds });
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('/pluginAsyncTask', async function (req, res) {
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('pluginAsyncTask:start', { query: req.query });
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
- if (!jwtToken) {
2698
- tracer?.trace('pluginAsyncTask:noToken', {});
2699
- res.status(400).send(tracer ? tracer.wrapResponse('Please go to Settings and authorize CRM platform') : 'Please go to Settings and authorize CRM platform');
2700
- return;
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
- console.log(`platform: ${platformName} \n${e.stack}`);
2717
- res.status(400).send(tracer ? tracer.wrapResponse({ error: e.message || e }) : { error: e.message || e });
2718
- tracer?.traceError('pluginAsyncTask:error', e, { platform: platformName });
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 inputSchema and stamps WIDGET_URI into getPublicConnectors _meta.
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
- if (result?.structuredContent) {
269
- response = {
270
- jsonrpc: '2.0',
271
- id,
272
- result: {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@app-connect/core",
3
- "version": "1.7.32",
3
+ "version": "1.7.34",
4
4
  "description": "RingCentral App Connect Core",
5
5
  "main": "index.js",
6
6
  "repository": {
package/releaseNotes.json CHANGED
@@ -1,4 +1,48 @@
1
1
  {
2
+ "1.7.34": {
3
+ "global": [
4
+ {
5
+ "type": "New",
6
+ "description": "Call pop fallback url when no matched contact"
7
+ },
8
+ {
9
+ "type": "Fix",
10
+ "description": "Hostname page input debounce"
11
+ },
12
+ {
13
+ "type": "Fix",
14
+ "description": "RingCentral additional info submission during auth"
15
+ },
16
+ {
17
+ "type": "Fix",
18
+ "description": "Managed auth, admins can now see hidden fields"
19
+ }
20
+ ]
21
+ },
22
+ "1.7.33": {
23
+ "global": [
24
+ {
25
+ "type": "New",
26
+ "description": "Developer Console manifest flag to disable Contact Cache"
27
+ },
28
+ {
29
+ "type": "New",
30
+ "description": "Interface refreshUserInfo, called when user opens extension"
31
+ },
32
+ {
33
+ "type": "New",
34
+ "description": "Async plugin callback"
35
+ },
36
+ {
37
+ "type": "New",
38
+ "description": "Developer Console manifest flag to hide plugin config fields"
39
+ },
40
+ {
41
+ "type": "Fix",
42
+ "description": "Mark outbound call on answer call pop option deprecated"
43
+ }
44
+ ]
45
+ },
2
46
  "1.7.30": {
3
47
  "global": [
4
48
  {