@app-connect/core 1.7.18 → 1.7.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/connector/proxy/index.js +2 -1
- package/handlers/log.js +181 -10
- package/handlers/plugin.js +27 -0
- package/handlers/user.js +31 -2
- package/index.js +99 -22
- package/lib/authSession.js +21 -12
- package/lib/callLogComposer.js +1 -1
- package/lib/debugTracer.js +20 -2
- package/lib/util.js +21 -4
- package/mcp/README.md +392 -0
- package/mcp/mcpHandler.js +293 -82
- package/mcp/tools/checkAuthStatus.js +27 -34
- package/mcp/tools/createCallLog.js +13 -9
- package/mcp/tools/createContact.js +2 -6
- package/mcp/tools/doAuth.js +27 -157
- package/mcp/tools/findContactByName.js +6 -9
- package/mcp/tools/findContactByPhone.js +2 -6
- package/mcp/tools/getGoogleFilePicker.js +5 -9
- package/mcp/tools/getHelp.js +2 -3
- package/mcp/tools/getPublicConnectors.js +41 -28
- package/mcp/tools/index.js +11 -36
- package/mcp/tools/logout.js +5 -10
- package/mcp/tools/rcGetCallLogs.js +3 -20
- package/mcp/ui/App/App.tsx +361 -0
- package/mcp/ui/App/components/AuthInfoForm.tsx +113 -0
- package/mcp/ui/App/components/AuthSuccess.tsx +22 -0
- package/mcp/ui/App/components/ConnectorList.tsx +82 -0
- package/mcp/ui/App/components/DebugPanel.tsx +43 -0
- package/mcp/ui/App/components/OAuthConnect.tsx +270 -0
- package/mcp/ui/App/lib/callTool.ts +130 -0
- package/mcp/ui/App/lib/debugLog.ts +41 -0
- package/mcp/ui/App/lib/developerPortal.ts +111 -0
- package/mcp/ui/App/main.css +6 -0
- package/mcp/ui/App/root.tsx +13 -0
- package/mcp/ui/dist/index.html +53 -0
- package/mcp/ui/index.html +13 -0
- package/mcp/ui/package-lock.json +6356 -0
- package/mcp/ui/package.json +25 -0
- package/mcp/ui/tsconfig.json +26 -0
- package/mcp/ui/vite.config.ts +16 -0
- package/models/llmSessionModel.js +14 -0
- package/package.json +72 -72
- package/releaseNotes.json +12 -0
- package/test/handlers/plugin.test.js +287 -0
- package/test/lib/util.test.js +379 -1
- package/test/mcp/tools/createCallLog.test.js +3 -3
- package/test/mcp/tools/doAuth.test.js +40 -303
- package/test/mcp/tools/findContactByName.test.js +3 -3
- package/test/mcp/tools/findContactByPhone.test.js +3 -3
- package/test/mcp/tools/getGoogleFilePicker.test.js +7 -7
- package/test/mcp/tools/getPublicConnectors.test.js +49 -70
- package/test/mcp/tools/logout.test.js +2 -2
- package/mcp/SupportedPlatforms.md +0 -12
- package/mcp/tools/collectAuthInfo.js +0 -91
- package/mcp/tools/setConnector.js +0 -69
- package/test/mcp/tools/collectAuthInfo.test.js +0 -234
- package/test/mcp/tools/setConnector.test.js +0 -177
package/lib/debugTracer.js
CHANGED
|
@@ -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.
|