@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.
Files changed (57) hide show
  1. package/connector/proxy/index.js +2 -1
  2. package/handlers/log.js +181 -10
  3. package/handlers/plugin.js +27 -0
  4. package/handlers/user.js +31 -2
  5. package/index.js +99 -22
  6. package/lib/authSession.js +21 -12
  7. package/lib/callLogComposer.js +1 -1
  8. package/lib/debugTracer.js +20 -2
  9. package/lib/util.js +21 -4
  10. package/mcp/README.md +392 -0
  11. package/mcp/mcpHandler.js +293 -82
  12. package/mcp/tools/checkAuthStatus.js +27 -34
  13. package/mcp/tools/createCallLog.js +13 -9
  14. package/mcp/tools/createContact.js +2 -6
  15. package/mcp/tools/doAuth.js +27 -157
  16. package/mcp/tools/findContactByName.js +6 -9
  17. package/mcp/tools/findContactByPhone.js +2 -6
  18. package/mcp/tools/getGoogleFilePicker.js +5 -9
  19. package/mcp/tools/getHelp.js +2 -3
  20. package/mcp/tools/getPublicConnectors.js +41 -28
  21. package/mcp/tools/index.js +11 -36
  22. package/mcp/tools/logout.js +5 -10
  23. package/mcp/tools/rcGetCallLogs.js +3 -20
  24. package/mcp/ui/App/App.tsx +361 -0
  25. package/mcp/ui/App/components/AuthInfoForm.tsx +113 -0
  26. package/mcp/ui/App/components/AuthSuccess.tsx +22 -0
  27. package/mcp/ui/App/components/ConnectorList.tsx +82 -0
  28. package/mcp/ui/App/components/DebugPanel.tsx +43 -0
  29. package/mcp/ui/App/components/OAuthConnect.tsx +270 -0
  30. package/mcp/ui/App/lib/callTool.ts +130 -0
  31. package/mcp/ui/App/lib/debugLog.ts +41 -0
  32. package/mcp/ui/App/lib/developerPortal.ts +111 -0
  33. package/mcp/ui/App/main.css +6 -0
  34. package/mcp/ui/App/root.tsx +13 -0
  35. package/mcp/ui/dist/index.html +53 -0
  36. package/mcp/ui/index.html +13 -0
  37. package/mcp/ui/package-lock.json +6356 -0
  38. package/mcp/ui/package.json +25 -0
  39. package/mcp/ui/tsconfig.json +26 -0
  40. package/mcp/ui/vite.config.ts +16 -0
  41. package/models/llmSessionModel.js +14 -0
  42. package/package.json +72 -72
  43. package/releaseNotes.json +12 -0
  44. package/test/handlers/plugin.test.js +287 -0
  45. package/test/lib/util.test.js +379 -1
  46. package/test/mcp/tools/createCallLog.test.js +3 -3
  47. package/test/mcp/tools/doAuth.test.js +40 -303
  48. package/test/mcp/tools/findContactByName.test.js +3 -3
  49. package/test/mcp/tools/findContactByPhone.test.js +3 -3
  50. package/test/mcp/tools/getGoogleFilePicker.test.js +7 -7
  51. package/test/mcp/tools/getPublicConnectors.test.js +49 -70
  52. package/test/mcp/tools/logout.test.js +2 -2
  53. package/mcp/SupportedPlatforms.md +0 -12
  54. package/mcp/tools/collectAuthInfo.js +0 -91
  55. package/mcp/tools/setConnector.js +0 -69
  56. package/test/mcp/tools/collectAuthInfo.test.js +0 -234
  57. package/test/mcp/tools/setConnector.test.js +0 -177
@@ -40,7 +40,7 @@ class DebugTracer {
40
40
  */
41
41
  trace(methodName, data = {}, options = {}) {
42
42
  const { includeStack = true, level = 'info' } = options;
43
-
43
+
44
44
  const traceEntry = {
45
45
  timestamp: new Date().toISOString(),
46
46
  elapsed: Date.now() - this.startTime,
@@ -85,7 +85,7 @@ class DebugTracer {
85
85
  }
86
86
 
87
87
  const sensitiveFields = [
88
- 'accessToken', 'refreshToken', 'apiKey', 'password',
88
+ 'accessToken', 'refreshToken', 'apiKey', 'password',
89
89
  'secret', 'token', 'authorization', 'auth', 'key',
90
90
  'credential', 'credentials', 'privateKey', 'clientSecret'
91
91
  ];
@@ -115,12 +115,30 @@ class DebugTracer {
115
115
  return sanitizeRecursive(sanitized);
116
116
  }
117
117
 
118
+ /**
119
+ * Builds a compact summary of all recorded actions, one entry per trace.
120
+ * Each entry contains the method name, log level, and elapsed time at the
121
+ * point the trace was recorded, making it easy to skim what happened without
122
+ * reading the full trace list.
123
+ * @returns {string[]} Array of human-readable action summary strings
124
+ */
125
+ _buildActionSummary() {
126
+ return this.traces.map((t, i) => ({
127
+ index: i + 1,
128
+ timestamp: t.timestamp,
129
+ level: t.level.toUpperCase(),
130
+ method: t.methodName,
131
+ elapsedMs: t.elapsed
132
+ }));
133
+ }
134
+
118
135
  /**
119
136
  * Gets the complete trace data for inclusion in response
120
137
  * @returns {Object} Trace data object
121
138
  */
122
139
  getTraceData() {
123
140
  return {
141
+ sum: this._buildActionSummary(),
124
142
  requestId: this.requestId,
125
143
  totalDuration: `${Date.now() - this.startTime}ms`,
126
144
  traceCount: this.traces.length,
package/lib/util.js CHANGED
@@ -40,8 +40,7 @@ function secondsToHoursMinutesSeconds(seconds) {
40
40
  function getMostRecentDate({ allDateValues }) {
41
41
  var result = 0;
42
42
  for (const date of allDateValues) {
43
- if(!date)
44
- {
43
+ if (!date) {
45
44
  continue;
46
45
  }
47
46
  if (date > result) {
@@ -53,16 +52,34 @@ function getMostRecentDate({ allDateValues }) {
53
52
 
54
53
  // media reader link: https://ringcentral.github.io/ringcentral-media-reader/?media=https://media.ringcentral.com/restapi/v1.0/account/{accountId}/extension/{extensionId}/message-store/{messageId}/content/{contentId}
55
54
  // platform media link: https://media.ringcentral.com/restapi/v1.0/account/{accountId}/extension/{extensionId}/message-store/{messageId}/content/{contentId}
56
- function getMediaReaderLinkByPlatformMediaLink(platformMediaLink){
57
- if(!platformMediaLink){
55
+ function getMediaReaderLinkByPlatformMediaLink(platformMediaLink) {
56
+ if (!platformMediaLink) {
58
57
  return null;
59
58
  }
60
59
  const encodedPlatformMediaLink = encodeURIComponent(platformMediaLink);
61
60
  return `https://ringcentral.github.io/ringcentral-media-reader/?media=${encodedPlatformMediaLink}`;
62
61
  }
63
62
 
63
+ function getPluginsFromUserSettings({ userSettings, logType }) {
64
+ const result = [];
65
+ if (!userSettings) {
66
+ return result;
67
+ }
68
+ for (const userSettingKey in userSettings) {
69
+ if (!userSettingKey.startsWith('plugin_')) {
70
+ continue;
71
+ }
72
+ const pluginUserSetting = userSettings[userSettingKey];
73
+ if (pluginUserSetting.value.logTypes.includes(logType)) {
74
+ result.push({ id: userSettingKey.replace('plugin_', ''), value: pluginUserSetting.value });
75
+ }
76
+ }
77
+ return result;
78
+ }
79
+
64
80
  exports.getTimeZone = getTimeZone;
65
81
  exports.getHashValue = getHashValue;
66
82
  exports.secondsToHoursMinutesSeconds = secondsToHoursMinutesSeconds;
67
83
  exports.getMostRecentDate = getMostRecentDate;
68
84
  exports.getMediaReaderLinkByPlatformMediaLink = getMediaReaderLinkByPlatformMediaLink;
85
+ exports.getPluginsFromUserSettings = getPluginsFromUserSettings;
package/mcp/README.md ADDED
@@ -0,0 +1,392 @@
1
+ # MCP Module Documentation
2
+
3
+ ## Overview
4
+
5
+ The MCP (Model Context Protocol) module provides an AI assistant interface for the RingCentral Unified CRM Extension. It enables AI assistants like ChatGPT to interact with the CRM integration through a standardized protocol, allowing users to authenticate, manage contacts, and log calls via conversational AI.
6
+
7
+ ## Architecture
8
+
9
+ ```
10
+ packages/core/mcp/
11
+ ├── mcpHandler.js # Main MCP server handler + WIDGET_VERSION constant
12
+ ├── lib/
13
+ │ └── validator.js # Connector manifest validation
14
+ ├── tools/ # MCP tool implementations
15
+ │ ├── index.js # Tool registry (tools + widgetTools)
16
+ │ ├── getHelp.js # Help/onboarding tool
17
+ │ ├── getPublicConnectors.js # Triggers widget, resolves RC account ID + rcExtensionId + openaiSessionId
18
+ │ ├── doAuth.js # OAuth session creation (widget-only)
19
+ │ ├── checkAuthStatus.js # Poll OAuth status (widget-only)
20
+ │ ├── logout.js # Logout from CRM
21
+ │ ├── findContactByPhone.js # Search contact by phone
22
+ │ ├── findContactByName.js # Search contact by name
23
+ │ ├── createContact.js # Create new contact
24
+ │ ├── createCallLog.js # Create call log entry
25
+ │ ├── rcGetCallLogs.js # Fetch RingCentral call logs
26
+ │ ├── getGoogleFilePicker.js # Google Sheets picker (disabled)
27
+ │ ├── getCallLog.js # Get call log (disabled)
28
+ │ ├── updateCallLog.js # Update call log (disabled)
29
+ │ └── createMessageLog.js # Create message log (disabled)
30
+ └── ui/ # ChatGPT Widget UI
31
+ ├── index.html # Entry HTML
32
+ ├── package.json # UI dependencies
33
+ ├── vite.config.ts # Vite build config
34
+ ├── App/
35
+ │ ├── root.tsx # React entry point
36
+ │ ├── App.tsx # Multi-step auth flow orchestrator
37
+ │ ├── main.css # Tailwind + OpenAI styles
38
+ │ ├── lib/
39
+ │ │ ├── callTool.ts # Direct fetch to /mcp/widget-tool-call
40
+ │ │ ├── developerPortal.ts # Client-side developer portal API calls
41
+ │ │ └── debugLog.ts # Debug logger
42
+ │ └── components/
43
+ │ ├── ConnectorList.tsx # Connector selection widget
44
+ │ ├── AuthInfoForm.tsx # Hostname/environment input form
45
+ │ ├── OAuthConnect.tsx # OAuth link + status polling
46
+ │ ├── AuthSuccess.tsx # Success banner
47
+ │ └── DebugPanel.tsx # Collapsible debug log panel
48
+ └── dist/ # Built widget output
49
+ ```
50
+
51
+ ## Core Components
52
+
53
+ ### MCP Handler (`mcpHandler.js`)
54
+
55
+ A stateless, hand-rolled JSON-RPC handler — no `@modelcontextprotocol/sdk`, no SSE, no in-memory sessions. Each POST request is handled independently, making it fully compatible with stateless deployments like AWS Lambda.
56
+
57
+ **Key Features:**
58
+ - Defines `WIDGET_VERSION` — the **single source of truth** for the widget cache-busting URI
59
+ - Handles `initialize`, `tools/list`, `tools/call`, `resources/list`, `resources/read`, and `ping` methods
60
+ - Defines `inputSchema` (JSON Schema) for every tool that takes parameters — required so ChatGPT forwards arguments
61
+ - Injects `rcAccessToken`, `openaiSessionId`, and `rcExtensionId` into every `tools/call` request
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)
64
+ - Stamps `WIDGET_URI` into `getPublicConnectors`'s `_meta['openai/outputTemplate']` at response time
65
+ - Serves the widget HTML via `resources/read`
66
+ - Exposes `handleWidgetToolCall` which searches both `tools.tools` and `tools.widgetTools`
67
+
68
+ **Request Flow:**
69
+ 1. Receives `POST /mcp` with a JSON-RPC body
70
+ 2. Extracts `rcAccessToken` from `Authorization` header and `openaiSessionId` from `params._meta['openai/session']`
71
+ 3. On `tools/call`: checks `CacheModel` for a cached `rcExtensionId`; if missing, verifies via RC API and persists to cache
72
+ 4. Injects server-side context (`rcAccessToken`, `openaiSessionId`, `rcExtensionId`, `jwtToken`) into tool args
73
+ 5. Routes to the appropriate tool handler via a `switch` on `method`
74
+ 6. Returns a JSON-RPC response immediately — no streaming, no SSE
75
+
76
+ ### Widget Version Management
77
+
78
+ `WIDGET_VERSION` in `mcpHandler.js` is the **only place** that needs to change when bumping the widget version:
79
+
80
+ ```js
81
+ // mcpHandler.js
82
+ const WIDGET_VERSION = 6;
83
+ const WIDGET_URI = `ui://widget/ConnectorList-v${WIDGET_VERSION}.html`;
84
+ ```
85
+
86
+ At registration time, `mcpHandler.js` stamps `WIDGET_URI` into `getPublicConnectors`'s `_meta['openai/outputTemplate']`. `getPublicConnectors.js` itself does **not** contain a version number.
87
+
88
+ **To deploy a new widget build:**
89
+ 1. Rebuild the widget: `cd packages/core/mcp/ui && npm run build`
90
+ 2. Increment `WIDGET_VERSION` in `mcpHandler.js`
91
+ 3. Restart the server
92
+
93
+ ### Manifest Validator (`lib/validator.js`)
94
+
95
+ Validates connector manifest structures before authentication operations.
96
+
97
+ ## MCP Tools
98
+
99
+ ### Tool Registry
100
+
101
+ Tools are split into two registries in `tools/index.js`:
102
+
103
+ | Registry | Purpose |
104
+ |----------|---------|
105
+ | `tools` | Registered in the MCP server — visible to and callable by the AI model |
106
+ | `widgetTools` | Accessible only via `POST /mcp/widget-tool-call` — hidden from the AI model |
107
+
108
+ ### Argument Handling
109
+
110
+ `mcpHandler.js` automatically injects server-side values into every tool's args before calling `execute()`:
111
+
112
+ | Injected arg | Source | Purpose |
113
+ |---|---|---|
114
+ | `rcAccessToken` | `Authorization` request header | RingCentral API calls |
115
+ | `openaiSessionId` | `params._meta['openai/session']` | Stable ChatGPT conversation ID |
116
+ | `rcExtensionId` | RC API (`/extension/~`), verified once and cached in `sessionContext` | Cryptographically verified RC identity; used as `LlmSessionModel` key |
117
+ | `jwtToken` | `LlmSessionModel.findByPk(rcExtensionId)` (fallback: `findByPk(openaiSessionId)`) | CRM auth token (after OAuth) |
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).
120
+
121
+ 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
+
123
+ ### AI-Visible Tools (`tools`)
124
+
125
+ #### `getHelp`
126
+ Provides onboarding guidance for new users.
127
+
128
+ | Property | Value |
129
+ |----------|-------|
130
+ | Read-only | Yes |
131
+ | Parameters | None |
132
+ | Returns | Overview, steps |
133
+
134
+ #### `getPublicConnectors`
135
+ Triggers the interactive connector selection widget. The widget fetches the connector list and manifests directly from the developer portal on the client side.
136
+
137
+ | Property | Value |
138
+ |----------|-------|
139
+ | Read-only | Yes |
140
+ | Parameters | None (server injects `rcAccessToken` and `openaiSessionId`) |
141
+ | Returns | `structuredContent` with `serverUrl`, `rcAccountId`, `rcExtensionId`, and `openaiSessionId` |
142
+ | Widget | `ui://widget/ConnectorList-v{WIDGET_VERSION}.html` (versioned by `mcpHandler.js`) |
143
+
144
+ #### `logout`
145
+ Logs out user from the CRM platform.
146
+
147
+ | Property | Value |
148
+ |----------|-------|
149
+ | Destructive | Yes |
150
+ | Parameters | `jwtToken` (optional — injected from session if not passed) |
151
+ | Action | Clears user credentials |
152
+
153
+ #### Contact & Call Log Tools
154
+
155
+ `jwtToken` is injected automatically from the session — ChatGPT does not need to pass it:
156
+
157
+ | Tool | Parameters | Description |
158
+ |------|-----------|-------------|
159
+ | `findContactByPhone` | `phoneNumber` (E.164) | Search contact by phone |
160
+ | `findContactByName` | `name` | Search contact by name |
161
+ | `createContact` | `phoneNumber`, `newContactName?` | Create new CRM contact |
162
+ | `rcGetCallLogs` | `timeFrom`, `timeTo` (ISO 8601) | Fetch RingCentral call logs. Each `records[i]` item can be passed directly as `incomingData.logInfo` to `createCallLog`. |
163
+ | `createCallLog` | `incomingData` (with `logInfo` = single `rcGetCallLogs` record), `contactId?`, `note?` | Create call log in CRM. Pass a `records[i]` item from `rcGetCallLogs` directly as `incomingData.logInfo`. |
164
+
165
+ ### Widget-Only Tools (`widgetTools`)
166
+
167
+ Not registered in the MCP server; only callable by the widget iframe via `POST /mcp/widget-tool-call`. No server-side arg injection — the widget passes all required values directly.
168
+
169
+ #### `doAuth`
170
+ Creates a server-side OAuth session for the given `sessionId`.
171
+
172
+ | Property | Value |
173
+ |----------|-------|
174
+ | Parameters | `sessionId`, `connectorName`, `hostname` |
175
+ | Returns | `{ success: true }` |
176
+ | Note | The widget generates `sessionId` and the OAuth URL client-side; `doAuth` just registers the session in the DB so the callback can resolve it |
177
+
178
+ #### `checkAuthStatus`
179
+ Polls the OAuth session status. Called exclusively by the widget during the OAuth flow — the AI model never calls this directly.
180
+
181
+ | Property | Value |
182
+ |----------|-------|
183
+ | Parameters | `sessionId`, `rcExtensionId?` (passed by widget from `getPublicConnectors` structuredContent) |
184
+ | Returns | `{ data: { status, ... } }` for all states |
185
+ | On Success | `data.jwtToken` and `data.userInfo` included; JWT stored in `LlmSessionModel` keyed by `rcExtensionId` |
186
+ | Statuses | `pending` · `completed` · `failed` — all return consistent `{ data: { status } }` for reliable widget parsing |
187
+
188
+ ## ChatGPT Widget UI
189
+
190
+ The UI module provides a single interactive widget that drives the full authentication flow inside a ChatGPT iframe. Users select a connector, provide any required environment info, authorize via OAuth (opened in a new tab), and see a success confirmation — all without leaving the widget.
191
+
192
+ ### Technology Stack
193
+ - **React 18** with TypeScript
194
+ - **Vite** with `vite-plugin-singlefile` for self-contained HTML
195
+ - **Tailwind CSS 4** with `@openai/apps-sdk-ui` components
196
+ - **OpenAI Apps SDK UI** for consistent ChatGPT styling
197
+
198
+ ### Widget Communication
199
+
200
+ **1. Receiving the initial server context** — via four mechanisms (whichever fires first):
201
+
202
+ | Mechanism | Description |
203
+ |-----------|-------------|
204
+ | `window.openai.toolOutput` | Synchronous read on mount |
205
+ | `openai:set_globals` event | ChatGPT pushes globals into the iframe |
206
+ | `ui/notifications/tool-result` postMessage | MCP Apps bridge notification |
207
+ | Polling `window.openai.toolOutput` | Fallback for async population |
208
+
209
+ The initial payload is `{ serverUrl, rcAccountId, rcExtensionId, openaiSessionId }`.
210
+
211
+ **2. Fetching connectors and manifests** — via direct `fetch()` to `appconnect.labs.ringcentral.com`:
212
+
213
+ ```typescript
214
+ import { fetchConnectors, fetchManifest } from './lib/developerPortal'
215
+
216
+ const connectors = await fetchConnectors(rcAccountId) // list with id, name, displayName
217
+ const manifest = await fetchManifest(connector.id, isPrivate, rcAccountId)
218
+ ```
219
+
220
+ **3. Calling widget tools** — via direct `fetch()` to `POST /mcp/widget-tool-call`:
221
+
222
+ ```typescript
223
+ import { callTool } from './lib/callTool'
224
+
225
+ const result = await callTool('doAuth', { sessionId, connectorName, hostname })
226
+ const status = await callTool('checkAuthStatus', { sessionId, rcExtensionId })
227
+ ```
228
+
229
+ `window.openai.callTool()` is **intentionally not used** for widget tool calls. Direct `fetch()` to `/mcp/widget-tool-call` forwards all arguments correctly and works for both `doAuth` and `checkAuthStatus`.
230
+
231
+ ### Widget Auth Flow
232
+
233
+ The widget (`App.tsx`) acts as a multi-step wizard:
234
+
235
+ ```
236
+ loadingConnectors → select → loading → authInfo (if dynamic/selectable env) → oauth → success
237
+ → oauth (if fixed env) → error
238
+ ```
239
+
240
+ | Step | Component | Description |
241
+ |------|-----------|-------------|
242
+ | `loadingConnectors` | (spinner) | Widget fetches connector list from developer portal |
243
+ | `select` | `ConnectorList` | Displays available connectors |
244
+ | `loading` | (spinner) | Shown while manifest is being fetched |
245
+ | `authInfo` | `AuthInfoForm` | Collects hostname (dynamic) or environment (selectable). Resolved locally — no server call |
246
+ | `oauth` | `OAuthConnect` | Uses `openaiSessionId` as the OAuth session ID (falls back to `crypto.randomUUID()`). Generates the OAuth URL client-side, calls `doAuth` in background to register the session in the DB, then shows "Authorize" button. After click, polls `checkAuthStatus` every 5 seconds via direct fetch |
247
+ | `success` | `AuthSuccess` | Shows connected CRM name and user info |
248
+ | `error` | (inline) | Shows error with "Back to connector list" link |
249
+
250
+ ### Components
251
+
252
+ #### ConnectorList
253
+ Displays available CRM connectors with public/private badges. On selection, delegates to `App.tsx` which fetches the manifest directly.
254
+
255
+ #### AuthInfoForm
256
+ Form for environment info collection. Handles `dynamic` (text input) and `selectable` (button list) environment types. Hostname resolution happens inline in `App.tsx`.
257
+
258
+ #### OAuthConnect
259
+ Handles the full OAuth step. Uses `openaiSessionId` (from the initial tool output) as the session ID so the OAuth callback can be correlated with the ChatGPT conversation — falls back to `crypto.randomUUID()` when running outside ChatGPT. Calls `doAuth` in the background, shows an "Authorize in [CRM]" button (disabled until session is created), then polls `checkAuthStatus` every 5 seconds via `callTool()` (direct fetch). On success, fires `updateModelContext` to push the jwtToken into ChatGPT's context, then calls `onSuccess`.
260
+
261
+ #### AuthSuccess
262
+ Success banner showing the connected CRM name and optional user info.
263
+
264
+ ### Developer Portal Client (`lib/developerPortal.ts`)
265
+
266
+ Calls `appconnect.labs.ringcentral.com/public-api` directly from the browser:
267
+
268
+ | Function | Description |
269
+ |----------|-------------|
270
+ | `fetchConnectors(rcAccountId?)` | Fetches public + private connectors, filters to `SUPPORTED_PLATFORMS` |
271
+ | `fetchManifest(connectorId, isPrivate, rcAccountId?)` | Fetches connector manifest by ID |
272
+
273
+ `SUPPORTED_PLATFORMS` is defined in this file: `['clio']`.
274
+
275
+ ### Building the Widget
276
+
277
+ **Production build** (generates `dist/index.html` with all JS, CSS, and assets inlined):
278
+
279
+ ```bash
280
+ cd packages/core/mcp/ui
281
+ npm install
282
+ npm run build # Output: dist/index.html (single file)
283
+ ```
284
+
285
+ **Development server** (hot reload for local testing):
286
+
287
+ ```bash
288
+ cd packages/core/mcp/ui
289
+ npm run dev
290
+ ```
291
+
292
+ **Cache busting after a build:** Increment `WIDGET_VERSION` in `mcpHandler.js` and restart the server. That is the only file that needs to change — `getPublicConnectors.js` and the README do not contain a version number.
293
+
294
+ ## API Integration
295
+
296
+ ### Endpoints
297
+
298
+ | Endpoint | Purpose |
299
+ |----------|---------|
300
+ | `POST /mcp` | Full MCP protocol endpoint for AI assistants (ChatGPT, etc.) |
301
+ | `POST /mcp/widget-tool-call` | Lightweight direct tool call for the widget iframe (bypasses MCP session protocol) |
302
+
303
+ #### `POST /mcp/widget-tool-call`
304
+
305
+ Called by the widget via `fetch()` to invoke `doAuth` and `checkAuthStatus` with full argument support.
306
+
307
+ **`doAuth` request:**
308
+ ```json
309
+ {
310
+ "tool": "doAuth",
311
+ "toolArgs": {
312
+ "sessionId": "<openaiSessionId or random UUID>",
313
+ "connectorName": "clio",
314
+ "hostname": "app.clio.com"
315
+ }
316
+ }
317
+ ```
318
+
319
+ **`checkAuthStatus` request:**
320
+ ```json
321
+ {
322
+ "tool": "checkAuthStatus",
323
+ "toolArgs": {
324
+ "sessionId": "<same sessionId used in doAuth>",
325
+ "rcExtensionId": "<from getPublicConnectors structuredContent>"
326
+ }
327
+ }
328
+ ```
329
+
330
+ **Response:** The raw result from `tool.execute()` — shape varies by tool.
331
+
332
+ ### Headers (for `/mcp`)
333
+
334
+ | Header | Value |
335
+ |--------|-------|
336
+ | `Content-Type` | `application/json` |
337
+ | `Authorization` | `Bearer <RC_ACCESS_TOKEN>` (optional, for RingCentral API calls) |
338
+
339
+ ## Supported Platforms
340
+
341
+ Currently supported for MCP integration:
342
+ - **Clio** - Legal practice management
343
+
344
+ ## Security Considerations
345
+
346
+ 1. **JWT Tokens**: Stored server-side in `LlmSessionModel`, **keyed by `rcExtensionId`** (a cryptographically verified RingCentral identity). Tools receive the JWT via server injection — it is never sent back to ChatGPT as a visible parameter.
347
+ 2. **RC Identity Verification**: On the first tool call of each session, `mcpHandler.js` calls `GET /restapi/v1.0/extension/~` with the Bearer token from the request. If the RC token is invalid, the call throws and `rcExtensionId` remains `null`. The result is cached in `sessionContext` so at most **one RC API call** is made per conversation.
348
+ 3. **Session key binding**: The sessions Map is keyed on the stable `openai/session` ID so the same `sessionContext` (and its verified `rcExtensionId`) is reused for all tool calls within a ChatGPT conversation. A session can only access credentials stored under its own verified `rcExtensionId`.
349
+ 4. **Session Management**: MCP sessions are server-side and automatically cleaned up on transport close.
350
+ 5. **OAuth Flows**: Uses secure OAuth 2.0 with server-side callback handling.
351
+ 6. **RC Account ID**: Resolved server-side via RC API and passed to the widget — never requires exposing secrets to the browser.
352
+ 7. **CORS**: Widget calls `appconnect.labs.ringcentral.com` directly; the developer portal public API supports browser fetch.
353
+
354
+ | | Before | After |
355
+ |---|---|---|
356
+ | Identity proof | `openai/session` (unverified, from request body) | `rcExtensionId` (verified via RC API) |
357
+ | RC token verified? | No | Yes — first tool call per conversation |
358
+ | API calls per session | 0 (no verify) | 1 (cached after first call) |
359
+ | `LlmSessionModel` key | arbitrary session ID | stable, verified RC extension ID |
360
+
361
+ ## Error Handling
362
+
363
+ All tools return standardized response objects:
364
+
365
+ **Success:**
366
+ ```json
367
+ { "success": true, "data": { ... } }
368
+ ```
369
+
370
+ **Failure:**
371
+ ```json
372
+ { "success": false, "error": "Error message" }
373
+ ```
374
+
375
+ ## Usage Example
376
+
377
+ A typical conversation flow:
378
+
379
+ 1. **User**: "Connect me to my CRM"
380
+ 2. **AI**: Calls `getPublicConnectors` → server verifies RC token, resolves `rcExtensionId` + RC account ID + `openaiSessionId` → shows widget
381
+ 3. **Widget**: Fetches connector list from developer portal → displays connector cards
382
+ 4. **User**: Clicks "Clio" in the widget
383
+ 5. **Widget**: Fetches Clio manifest → shows environment selector (US/EU/AU/CA)
384
+ 6. **User**: Selects region → widget calls `doAuth` with `openaiSessionId` as OAuth session key → shows "Authorize in Clio" button
385
+ 7. **User**: Clicks button → Clio OAuth page opens in new tab → user authorizes
386
+ 8. **Widget**: Polls `checkAuthStatus` every 5 seconds via direct fetch (passes `sessionId` + `rcExtensionId`) → "Waiting for authorization..."
387
+ 9. **Widget**: Auth completes → jwtToken stored in `LlmSessionModel[rcExtensionId]` → widget fires `updateModelContext` with jwtToken → shows success banner
388
+ 10. **AI**: "You're now connected! What would you like to do?"
389
+ 11. **User**: "Find contacts named Test"
390
+ 12. **AI**: Calls `findContactByName(name="Test")` — server injects jwtToken automatically using cached `rcExtensionId` as lookup key → returns results
391
+
392
+ Steps 3–9 happen entirely within the widget iframe. The AI assistant is not involved until authentication is complete.