@app-connect/core 1.7.25 → 1.7.26
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/.env.test +5 -5
- package/README.md +441 -441
- package/connector/developerPortal.js +31 -31
- package/connector/mock.js +84 -77
- package/connector/proxy/engine.js +164 -164
- package/connector/proxy/index.js +500 -500
- package/connector/registry.js +252 -252
- package/docs/README.md +50 -50
- package/docs/architecture.md +93 -93
- package/docs/connectors.md +116 -116
- package/docs/handlers.md +125 -125
- package/docs/libraries.md +101 -101
- package/docs/models.md +144 -144
- package/docs/routes.md +115 -115
- package/docs/tests.md +73 -73
- package/handlers/admin.js +523 -523
- package/handlers/appointment.js +193 -0
- package/handlers/auth.js +296 -296
- package/handlers/calldown.js +99 -99
- package/handlers/contact.js +280 -280
- package/handlers/disposition.js +82 -80
- package/handlers/log.js +984 -973
- package/handlers/managedAuth.js +446 -446
- package/handlers/plugin.js +208 -208
- package/handlers/user.js +142 -142
- package/index.js +3140 -2652
- package/jest.config.js +56 -56
- package/lib/analytics.js +54 -54
- package/lib/authSession.js +109 -109
- package/lib/cacheCleanup.js +21 -0
- package/lib/callLogComposer.js +898 -898
- package/lib/callLogLookup.js +34 -0
- package/lib/constants.js +8 -8
- package/lib/debugTracer.js +177 -177
- package/lib/encode.js +30 -30
- package/lib/errorHandler.js +218 -206
- package/lib/generalErrorMessage.js +41 -41
- package/lib/jwt.js +18 -18
- package/lib/logger.js +190 -190
- package/lib/migrateCallLogsSchema.js +116 -0
- package/lib/ringcentral.js +266 -266
- package/lib/s3ErrorLogReport.js +65 -65
- package/lib/sharedSMSComposer.js +471 -471
- package/lib/util.js +67 -67
- package/mcp/README.md +412 -395
- package/mcp/lib/validator.js +91 -91
- package/mcp/mcpHandler.js +425 -425
- package/mcp/tools/cancelAppointment.js +101 -0
- package/mcp/tools/checkAuthStatus.js +105 -105
- package/mcp/tools/confirmAppointment.js +101 -0
- package/mcp/tools/createAppointment.js +157 -0
- package/mcp/tools/createCallLog.js +327 -316
- package/mcp/tools/createContact.js +117 -117
- package/mcp/tools/createMessageLog.js +287 -287
- package/mcp/tools/doAuth.js +60 -60
- package/mcp/tools/findContactByName.js +93 -93
- package/mcp/tools/findContactByPhone.js +101 -101
- package/mcp/tools/getCallLog.js +111 -102
- package/mcp/tools/getGoogleFilePicker.js +99 -99
- package/mcp/tools/getHelp.js +43 -43
- package/mcp/tools/getPublicConnectors.js +94 -94
- package/mcp/tools/getSessionInfo.js +90 -90
- package/mcp/tools/index.js +51 -41
- package/mcp/tools/listAppointments.js +163 -0
- package/mcp/tools/logout.js +96 -96
- package/mcp/tools/rcGetCallLogs.js +65 -65
- package/mcp/tools/updateAppointment.js +154 -0
- package/mcp/tools/updateCallLog.js +130 -126
- package/mcp/ui/App/App.tsx +358 -358
- package/mcp/ui/App/components/AuthInfoForm.tsx +113 -113
- package/mcp/ui/App/components/AuthSuccess.tsx +22 -22
- package/mcp/ui/App/components/ConnectorList.tsx +82 -82
- package/mcp/ui/App/components/DebugPanel.tsx +43 -43
- package/mcp/ui/App/components/OAuthConnect.tsx +270 -270
- package/mcp/ui/App/lib/callTool.ts +130 -130
- package/mcp/ui/App/lib/debugLog.ts +41 -41
- package/mcp/ui/App/lib/developerPortal.ts +111 -111
- package/mcp/ui/App/main.css +5 -5
- package/mcp/ui/App/root.tsx +13 -13
- package/mcp/ui/index.html +13 -13
- package/mcp/ui/package-lock.json +6356 -6356
- package/mcp/ui/package.json +25 -25
- package/mcp/ui/tsconfig.json +26 -26
- package/mcp/ui/vite.config.ts +16 -16
- package/models/accountDataModel.js +33 -33
- package/models/adminConfigModel.js +35 -35
- package/models/cacheModel.js +30 -26
- package/models/callDownListModel.js +34 -34
- package/models/callLogModel.js +33 -27
- package/models/dynamo/connectorSchema.js +146 -146
- package/models/dynamo/lockSchema.js +24 -24
- package/models/dynamo/noteCacheSchema.js +29 -29
- package/models/llmSessionModel.js +17 -17
- package/models/messageLogModel.js +25 -25
- package/models/sequelize.js +16 -16
- package/models/userModel.js +45 -45
- package/package.json +1 -1
- package/releaseNotes.json +1093 -1081
- package/test/connector/proxy/engine.test.js +126 -126
- package/test/connector/proxy/index.test.js +279 -279
- package/test/connector/proxy/sample.json +161 -161
- package/test/connector/registry.test.js +415 -415
- package/test/handlers/admin.test.js +616 -616
- package/test/handlers/auth.test.js +1018 -1018
- package/test/handlers/contact.test.js +1014 -1014
- package/test/handlers/log.test.js +1298 -1160
- package/test/handlers/managedAuth.test.js +457 -457
- package/test/handlers/plugin.test.js +380 -380
- package/test/index.test.js +105 -105
- package/test/lib/cacheCleanup.test.js +42 -0
- package/test/lib/callLogComposer.test.js +1231 -1231
- package/test/lib/debugTracer.test.js +328 -328
- package/test/lib/jwt.test.js +176 -176
- package/test/lib/logger.test.js +206 -206
- package/test/lib/oauth.test.js +359 -359
- package/test/lib/ringcentral.test.js +467 -467
- package/test/lib/sharedSMSComposer.test.js +1084 -1084
- package/test/lib/util.test.js +329 -329
- package/test/mcp/tools/checkAuthStatus.test.js +83 -83
- package/test/mcp/tools/createCallLog.test.js +436 -436
- package/test/mcp/tools/createContact.test.js +58 -58
- package/test/mcp/tools/createMessageLog.test.js +595 -595
- package/test/mcp/tools/doAuth.test.js +113 -113
- package/test/mcp/tools/findContactByName.test.js +275 -275
- package/test/mcp/tools/findContactByPhone.test.js +296 -296
- package/test/mcp/tools/getCallLog.test.js +298 -298
- package/test/mcp/tools/getGoogleFilePicker.test.js +281 -281
- package/test/mcp/tools/getPublicConnectors.test.js +107 -107
- package/test/mcp/tools/getSessionInfo.test.js +127 -127
- package/test/mcp/tools/logout.test.js +233 -233
- package/test/mcp/tools/rcGetCallLogs.test.js +56 -56
- package/test/mcp/tools/updateCallLog.test.js +360 -360
- package/test/models/accountDataModel.test.js +98 -98
- package/test/models/dynamo/connectorSchema.test.js +189 -189
- package/test/models/models.test.js +568 -539
- package/test/routes/managedAuthRoutes.test.js +104 -104
- package/test/setup.js +178 -178
package/mcp/ui/App/App.tsx
CHANGED
|
@@ -1,358 +1,358 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from 'react'
|
|
2
|
-
import { ConnectorList, type Connector } from './components/ConnectorList'
|
|
3
|
-
import { AuthInfoForm } from './components/AuthInfoForm'
|
|
4
|
-
import { OAuthConnect } from './components/OAuthConnect'
|
|
5
|
-
import { AuthSuccess } from './components/AuthSuccess'
|
|
6
|
-
import { setServerUrl } from './lib/callTool'
|
|
7
|
-
import { fetchConnectors, fetchManifest } from './lib/developerPortal'
|
|
8
|
-
import { dbg } from './lib/debugLog'
|
|
9
|
-
|
|
10
|
-
// Initial structuredContent from getPublicConnectors — serverUrl, rcAccountId, rcExtensionId, openaiSessionId
|
|
11
|
-
interface ToolOutput {
|
|
12
|
-
serverUrl?: string
|
|
13
|
-
rcAccountId?: string | null
|
|
14
|
-
rcExtensionId?: string | null
|
|
15
|
-
openaiSessionId?: string | null
|
|
16
|
-
error?: boolean
|
|
17
|
-
errorMessage?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
type Step = 'loadingConnectors' | 'select' | 'loading' | 'authInfo' | 'oauth' | 'success' | 'error'
|
|
21
|
-
|
|
22
|
-
interface FlowState {
|
|
23
|
-
connectorManifest: any | null
|
|
24
|
-
connectorName: string | null
|
|
25
|
-
connectorDisplayName: string | null
|
|
26
|
-
hostname: string
|
|
27
|
-
userInfo: any | null
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const INITIAL_FLOW_STATE: FlowState = {
|
|
31
|
-
connectorManifest: null,
|
|
32
|
-
connectorName: null,
|
|
33
|
-
connectorDisplayName: null,
|
|
34
|
-
hostname: '',
|
|
35
|
-
userInfo: null,
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Extract and apply serverUrl from tool output as early as possible.
|
|
40
|
-
* Must be called synchronously so it's set before any tool calls.
|
|
41
|
-
*/
|
|
42
|
-
function applyServerUrl(output: ToolOutput | null) {
|
|
43
|
-
dbg.info('applyServerUrl: received output keys:', output ? Object.keys(output).join(', ') : 'null');
|
|
44
|
-
dbg.info('applyServerUrl: serverUrl =', output?.serverUrl ?? '(none)');
|
|
45
|
-
if (output?.serverUrl) {
|
|
46
|
-
setServerUrl(output.serverUrl);
|
|
47
|
-
}
|
|
48
|
-
return output;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Hook to receive the initial tool output (serverUrl + rcAccountId).
|
|
53
|
-
*
|
|
54
|
-
* Listens via three mechanisms per the ChatGPT Apps SDK docs:
|
|
55
|
-
* 1. window.openai.toolOutput — synchronous read on mount
|
|
56
|
-
* 2. openai:set_globals event — ChatGPT pushes globals into the iframe
|
|
57
|
-
* 3. ui/notifications/tool-result postMessage — MCP Apps bridge notification
|
|
58
|
-
* 4. Polling window.openai.toolOutput — fallback for async population
|
|
59
|
-
*/
|
|
60
|
-
function useToolResult() {
|
|
61
|
-
const [toolResult, setToolResult] = useState<ToolOutput | null>(() => {
|
|
62
|
-
const openai = (window as any).openai
|
|
63
|
-
dbg.info('init: window.openai exists?', !!openai);
|
|
64
|
-
dbg.info('init: window.openai.toolOutput?', JSON.stringify(openai?.toolOutput)?.slice(0, 200) ?? 'null');
|
|
65
|
-
const output = openai?.toolOutput
|
|
66
|
-
if (output && Object.keys(output).length > 0) {
|
|
67
|
-
return applyServerUrl(output)
|
|
68
|
-
}
|
|
69
|
-
return null
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
useEffect(() => {
|
|
73
|
-
if (toolResult) return
|
|
74
|
-
|
|
75
|
-
// MCP Apps bridge: tool-result notification
|
|
76
|
-
const onMessage = (event: MessageEvent) => {
|
|
77
|
-
if (event.source !== window.parent) return
|
|
78
|
-
const message = event.data
|
|
79
|
-
if (!message || message.jsonrpc !== '2.0') return
|
|
80
|
-
if (message.method !== 'ui/notifications/tool-result') return
|
|
81
|
-
const payload = message.params?.structuredContent ?? message.params ?? null
|
|
82
|
-
if (payload) setToolResult(applyServerUrl(payload))
|
|
83
|
-
}
|
|
84
|
-
window.addEventListener('message', onMessage, { passive: true })
|
|
85
|
-
|
|
86
|
-
// ChatGPT extension: openai:set_globals event
|
|
87
|
-
const onSetGlobals = (event: Event) => {
|
|
88
|
-
const detail = (event as CustomEvent).detail
|
|
89
|
-
const output = detail?.globals?.toolOutput ?? (window as any).openai?.toolOutput
|
|
90
|
-
if (output && Object.keys(output).length > 0) {
|
|
91
|
-
setToolResult(applyServerUrl(output))
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
window.addEventListener('openai:set_globals', onSetGlobals, { passive: true })
|
|
95
|
-
|
|
96
|
-
// Fallback: poll window.openai.toolOutput for async population
|
|
97
|
-
const interval = setInterval(() => {
|
|
98
|
-
const output = (window as any).openai?.toolOutput
|
|
99
|
-
if (output && Object.keys(output).length > 0) {
|
|
100
|
-
setToolResult(applyServerUrl(output))
|
|
101
|
-
clearInterval(interval)
|
|
102
|
-
}
|
|
103
|
-
}, 100)
|
|
104
|
-
|
|
105
|
-
return () => {
|
|
106
|
-
window.removeEventListener('message', onMessage)
|
|
107
|
-
window.removeEventListener('openai:set_globals', onSetGlobals)
|
|
108
|
-
clearInterval(interval)
|
|
109
|
-
}
|
|
110
|
-
}, [toolResult])
|
|
111
|
-
|
|
112
|
-
return toolResult
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function App() {
|
|
116
|
-
const data = useToolResult()
|
|
117
|
-
const [step, setStep] = useState<Step>('loadingConnectors')
|
|
118
|
-
const [connectors, setConnectors] = useState<Connector[]>([])
|
|
119
|
-
const [flow, setFlow] = useState<FlowState>(INITIAL_FLOW_STATE)
|
|
120
|
-
const [errorMsg, setErrorMsg] = useState<string | null>(null)
|
|
121
|
-
|
|
122
|
-
// Fetch connector list from developer portal once serverUrl (and optionally rcAccountId) is known
|
|
123
|
-
useEffect(() => {
|
|
124
|
-
if (!data || data.error) return
|
|
125
|
-
|
|
126
|
-
async function loadConnectors() {
|
|
127
|
-
try {
|
|
128
|
-
dbg.info('loadConnectors: fetching with rcAccountId=', data!.rcAccountId ?? '(none)')
|
|
129
|
-
const list = await fetchConnectors(data!.rcAccountId)
|
|
130
|
-
setConnectors(list)
|
|
131
|
-
setStep('select')
|
|
132
|
-
} catch (err: any) {
|
|
133
|
-
dbg.error('loadConnectors failed:', err.message)
|
|
134
|
-
setStep('error')
|
|
135
|
-
setErrorMsg(err.message || 'Failed to load connectors')
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
loadConnectors()
|
|
140
|
-
}, [data])
|
|
141
|
-
|
|
142
|
-
const resetToSelect = useCallback(() => {
|
|
143
|
-
setStep('select')
|
|
144
|
-
setFlow(INITIAL_FLOW_STATE)
|
|
145
|
-
setErrorMsg(null)
|
|
146
|
-
}, [])
|
|
147
|
-
|
|
148
|
-
const handleConnectorSelect = useCallback(async (connector: Connector) => {
|
|
149
|
-
dbg.info('connector selected:', JSON.stringify(connector))
|
|
150
|
-
setStep('loading')
|
|
151
|
-
setErrorMsg(null)
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
const manifest = await fetchManifest(
|
|
155
|
-
connector.id,
|
|
156
|
-
connector.status === 'private',
|
|
157
|
-
data?.rcAccountId,
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
if (!manifest) {
|
|
161
|
-
setStep('error')
|
|
162
|
-
setErrorMsg('Failed to load connector configuration')
|
|
163
|
-
return
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const connectorName = connector.name
|
|
167
|
-
const platform = manifest?.platforms?.[connectorName]
|
|
168
|
-
|
|
169
|
-
if (!platform) {
|
|
170
|
-
setStep('error')
|
|
171
|
-
setErrorMsg('Invalid connector configuration')
|
|
172
|
-
return
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
setFlow((prev) => ({
|
|
176
|
-
...prev,
|
|
177
|
-
connectorManifest: manifest,
|
|
178
|
-
connectorName,
|
|
179
|
-
connectorDisplayName: connector.displayName,
|
|
180
|
-
}))
|
|
181
|
-
|
|
182
|
-
const envType = platform.environment?.type
|
|
183
|
-
const authType = platform.auth?.type
|
|
184
|
-
|
|
185
|
-
// For oauth with dynamic/selectable environments, collect auth info first
|
|
186
|
-
if (authType === 'oauth' && (envType === 'dynamic' || envType === 'selectable')) {
|
|
187
|
-
setStep('authInfo')
|
|
188
|
-
return
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// For fixed environments, extract hostname from the fixed URL
|
|
192
|
-
if (envType === 'fixed' && platform.environment?.url) {
|
|
193
|
-
try {
|
|
194
|
-
const url = new URL(platform.environment.url)
|
|
195
|
-
setFlow((prev) => ({ ...prev, hostname: url.hostname }))
|
|
196
|
-
} catch {
|
|
197
|
-
// hostname stays empty
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
setStep('oauth')
|
|
202
|
-
} catch (err: any) {
|
|
203
|
-
setStep('error')
|
|
204
|
-
setErrorMsg(err.message || 'Failed to load connector')
|
|
205
|
-
}
|
|
206
|
-
}, [data?.rcAccountId])
|
|
207
|
-
|
|
208
|
-
const handleAuthInfoSubmit = useCallback(
|
|
209
|
-
(value: { hostname?: string; selection?: string }) => {
|
|
210
|
-
try {
|
|
211
|
-
const platform = flow.connectorManifest?.platforms?.[flow.connectorName!]
|
|
212
|
-
const envType = platform?.environment?.type
|
|
213
|
-
let resolvedHostname = ''
|
|
214
|
-
|
|
215
|
-
if (envType === 'dynamic' && value.hostname) {
|
|
216
|
-
resolvedHostname = new URL(value.hostname).hostname
|
|
217
|
-
} else if (envType === 'selectable' && value.selection) {
|
|
218
|
-
const sel = platform.environment.selections?.find(
|
|
219
|
-
(s: { name: string; const: string }) => s.name === value.selection,
|
|
220
|
-
)
|
|
221
|
-
if (sel?.const) {
|
|
222
|
-
resolvedHostname = new URL(sel.const).hostname
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
setFlow((prev) => ({ ...prev, hostname: resolvedHostname }))
|
|
227
|
-
setStep('oauth')
|
|
228
|
-
} catch (err: any) {
|
|
229
|
-
setStep('error')
|
|
230
|
-
setErrorMsg(err.message || 'Failed to process environment selection')
|
|
231
|
-
}
|
|
232
|
-
},
|
|
233
|
-
[flow.connectorManifest, flow.connectorName],
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
const handleAuthSuccess = useCallback(
|
|
237
|
-
(authData: { jwtToken?: string; userInfo?: any }) => {
|
|
238
|
-
setFlow((prev) => ({ ...prev, userInfo: authData.userInfo ?? null }))
|
|
239
|
-
setStep('success')
|
|
240
|
-
},
|
|
241
|
-
[],
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
const handleAuthError = useCallback(
|
|
245
|
-
(error: string) => {
|
|
246
|
-
if (error === 'retry') {
|
|
247
|
-
setStep('oauth')
|
|
248
|
-
return
|
|
249
|
-
}
|
|
250
|
-
setStep('error')
|
|
251
|
-
setErrorMsg(error)
|
|
252
|
-
},
|
|
253
|
-
[],
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
// Waiting for initial tool output from ChatGPT
|
|
257
|
-
if (!data) {
|
|
258
|
-
return (
|
|
259
|
-
<div className="flex items-center justify-center min-h-[150px] p-4">
|
|
260
|
-
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary" />
|
|
261
|
-
</div>
|
|
262
|
-
)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Error from initial tool call
|
|
266
|
-
if (data.error) {
|
|
267
|
-
return (
|
|
268
|
-
<div className="p-4">
|
|
269
|
-
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-4">
|
|
270
|
-
<p className="text-yellow-800">{data.errorMessage || 'An error occurred'}</p>
|
|
271
|
-
</div>
|
|
272
|
-
</div>
|
|
273
|
-
)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return (
|
|
277
|
-
<div className="p-4">
|
|
278
|
-
{step === 'loadingConnectors' && (
|
|
279
|
-
<div className="flex items-center justify-center min-h-[150px]">
|
|
280
|
-
<div className="text-center space-y-3">
|
|
281
|
-
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mx-auto" />
|
|
282
|
-
<p className="text-sm text-secondary">Loading connectors...</p>
|
|
283
|
-
</div>
|
|
284
|
-
</div>
|
|
285
|
-
)}
|
|
286
|
-
|
|
287
|
-
{step === 'select' && (
|
|
288
|
-
<ConnectorList connectors={connectors} onSelect={handleConnectorSelect} />
|
|
289
|
-
)}
|
|
290
|
-
|
|
291
|
-
{step === 'loading' && (
|
|
292
|
-
<div className="flex items-center justify-center min-h-[150px]">
|
|
293
|
-
<div className="text-center space-y-3">
|
|
294
|
-
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mx-auto" />
|
|
295
|
-
<p className="text-sm text-secondary">Loading...</p>
|
|
296
|
-
</div>
|
|
297
|
-
</div>
|
|
298
|
-
)}
|
|
299
|
-
|
|
300
|
-
{step === 'authInfo' && flow.connectorManifest && flow.connectorName && (
|
|
301
|
-
<AuthInfoForm
|
|
302
|
-
environmentType={
|
|
303
|
-
flow.connectorManifest.platforms[flow.connectorName].environment.type
|
|
304
|
-
}
|
|
305
|
-
urlIdentifier={
|
|
306
|
-
flow.connectorManifest.platforms[flow.connectorName].environment.urlIdentifier
|
|
307
|
-
}
|
|
308
|
-
instructions={
|
|
309
|
-
flow.connectorManifest.platforms[flow.connectorName].environment.instructions
|
|
310
|
-
}
|
|
311
|
-
selections={
|
|
312
|
-
flow.connectorManifest.platforms[flow.connectorName].environment.selections
|
|
313
|
-
}
|
|
314
|
-
connectorDisplayName={flow.connectorDisplayName!}
|
|
315
|
-
onSubmit={handleAuthInfoSubmit}
|
|
316
|
-
onBack={resetToSelect}
|
|
317
|
-
/>
|
|
318
|
-
)}
|
|
319
|
-
|
|
320
|
-
{step === 'oauth' && flow.connectorManifest && flow.connectorName && (
|
|
321
|
-
<OAuthConnect
|
|
322
|
-
connectorManifest={flow.connectorManifest}
|
|
323
|
-
connectorName={flow.connectorName}
|
|
324
|
-
connectorDisplayName={flow.connectorDisplayName!}
|
|
325
|
-
hostname={flow.hostname}
|
|
326
|
-
openaiSessionId={data?.openaiSessionId ?? null}
|
|
327
|
-
rcExtensionId={data?.rcExtensionId ?? null}
|
|
328
|
-
onSuccess={handleAuthSuccess}
|
|
329
|
-
onError={handleAuthError}
|
|
330
|
-
onBack={resetToSelect}
|
|
331
|
-
/>
|
|
332
|
-
)}
|
|
333
|
-
|
|
334
|
-
{step === 'success' && (
|
|
335
|
-
<AuthSuccess
|
|
336
|
-
connectorDisplayName={flow.connectorDisplayName!}
|
|
337
|
-
userInfo={flow.userInfo ?? undefined}
|
|
338
|
-
/>
|
|
339
|
-
)}
|
|
340
|
-
|
|
341
|
-
{step === 'error' && (
|
|
342
|
-
<div className="w-full max-w-md">
|
|
343
|
-
<div className="rounded-lg border border-red-200 bg-red-50 p-4 mb-4">
|
|
344
|
-
<p className="text-red-700 text-sm font-semibold">Error</p>
|
|
345
|
-
<p className="text-red-700 text-sm mt-1">{errorMsg || 'An error occurred'}</p>
|
|
346
|
-
</div>
|
|
347
|
-
<button
|
|
348
|
-
type="button"
|
|
349
|
-
onClick={resetToSelect}
|
|
350
|
-
className="text-sm text-primary hover:underline cursor-pointer"
|
|
351
|
-
>
|
|
352
|
-
← Back to connector list
|
|
353
|
-
</button>
|
|
354
|
-
</div>
|
|
355
|
-
)}
|
|
356
|
-
</div>
|
|
357
|
-
)
|
|
358
|
-
}
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
2
|
+
import { ConnectorList, type Connector } from './components/ConnectorList'
|
|
3
|
+
import { AuthInfoForm } from './components/AuthInfoForm'
|
|
4
|
+
import { OAuthConnect } from './components/OAuthConnect'
|
|
5
|
+
import { AuthSuccess } from './components/AuthSuccess'
|
|
6
|
+
import { setServerUrl } from './lib/callTool'
|
|
7
|
+
import { fetchConnectors, fetchManifest } from './lib/developerPortal'
|
|
8
|
+
import { dbg } from './lib/debugLog'
|
|
9
|
+
|
|
10
|
+
// Initial structuredContent from getPublicConnectors — serverUrl, rcAccountId, rcExtensionId, openaiSessionId
|
|
11
|
+
interface ToolOutput {
|
|
12
|
+
serverUrl?: string
|
|
13
|
+
rcAccountId?: string | null
|
|
14
|
+
rcExtensionId?: string | null
|
|
15
|
+
openaiSessionId?: string | null
|
|
16
|
+
error?: boolean
|
|
17
|
+
errorMessage?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type Step = 'loadingConnectors' | 'select' | 'loading' | 'authInfo' | 'oauth' | 'success' | 'error'
|
|
21
|
+
|
|
22
|
+
interface FlowState {
|
|
23
|
+
connectorManifest: any | null
|
|
24
|
+
connectorName: string | null
|
|
25
|
+
connectorDisplayName: string | null
|
|
26
|
+
hostname: string
|
|
27
|
+
userInfo: any | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const INITIAL_FLOW_STATE: FlowState = {
|
|
31
|
+
connectorManifest: null,
|
|
32
|
+
connectorName: null,
|
|
33
|
+
connectorDisplayName: null,
|
|
34
|
+
hostname: '',
|
|
35
|
+
userInfo: null,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract and apply serverUrl from tool output as early as possible.
|
|
40
|
+
* Must be called synchronously so it's set before any tool calls.
|
|
41
|
+
*/
|
|
42
|
+
function applyServerUrl(output: ToolOutput | null) {
|
|
43
|
+
dbg.info('applyServerUrl: received output keys:', output ? Object.keys(output).join(', ') : 'null');
|
|
44
|
+
dbg.info('applyServerUrl: serverUrl =', output?.serverUrl ?? '(none)');
|
|
45
|
+
if (output?.serverUrl) {
|
|
46
|
+
setServerUrl(output.serverUrl);
|
|
47
|
+
}
|
|
48
|
+
return output;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Hook to receive the initial tool output (serverUrl + rcAccountId).
|
|
53
|
+
*
|
|
54
|
+
* Listens via three mechanisms per the ChatGPT Apps SDK docs:
|
|
55
|
+
* 1. window.openai.toolOutput — synchronous read on mount
|
|
56
|
+
* 2. openai:set_globals event — ChatGPT pushes globals into the iframe
|
|
57
|
+
* 3. ui/notifications/tool-result postMessage — MCP Apps bridge notification
|
|
58
|
+
* 4. Polling window.openai.toolOutput — fallback for async population
|
|
59
|
+
*/
|
|
60
|
+
function useToolResult() {
|
|
61
|
+
const [toolResult, setToolResult] = useState<ToolOutput | null>(() => {
|
|
62
|
+
const openai = (window as any).openai
|
|
63
|
+
dbg.info('init: window.openai exists?', !!openai);
|
|
64
|
+
dbg.info('init: window.openai.toolOutput?', JSON.stringify(openai?.toolOutput)?.slice(0, 200) ?? 'null');
|
|
65
|
+
const output = openai?.toolOutput
|
|
66
|
+
if (output && Object.keys(output).length > 0) {
|
|
67
|
+
return applyServerUrl(output)
|
|
68
|
+
}
|
|
69
|
+
return null
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (toolResult) return
|
|
74
|
+
|
|
75
|
+
// MCP Apps bridge: tool-result notification
|
|
76
|
+
const onMessage = (event: MessageEvent) => {
|
|
77
|
+
if (event.source !== window.parent) return
|
|
78
|
+
const message = event.data
|
|
79
|
+
if (!message || message.jsonrpc !== '2.0') return
|
|
80
|
+
if (message.method !== 'ui/notifications/tool-result') return
|
|
81
|
+
const payload = message.params?.structuredContent ?? message.params ?? null
|
|
82
|
+
if (payload) setToolResult(applyServerUrl(payload))
|
|
83
|
+
}
|
|
84
|
+
window.addEventListener('message', onMessage, { passive: true })
|
|
85
|
+
|
|
86
|
+
// ChatGPT extension: openai:set_globals event
|
|
87
|
+
const onSetGlobals = (event: Event) => {
|
|
88
|
+
const detail = (event as CustomEvent).detail
|
|
89
|
+
const output = detail?.globals?.toolOutput ?? (window as any).openai?.toolOutput
|
|
90
|
+
if (output && Object.keys(output).length > 0) {
|
|
91
|
+
setToolResult(applyServerUrl(output))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
window.addEventListener('openai:set_globals', onSetGlobals, { passive: true })
|
|
95
|
+
|
|
96
|
+
// Fallback: poll window.openai.toolOutput for async population
|
|
97
|
+
const interval = setInterval(() => {
|
|
98
|
+
const output = (window as any).openai?.toolOutput
|
|
99
|
+
if (output && Object.keys(output).length > 0) {
|
|
100
|
+
setToolResult(applyServerUrl(output))
|
|
101
|
+
clearInterval(interval)
|
|
102
|
+
}
|
|
103
|
+
}, 100)
|
|
104
|
+
|
|
105
|
+
return () => {
|
|
106
|
+
window.removeEventListener('message', onMessage)
|
|
107
|
+
window.removeEventListener('openai:set_globals', onSetGlobals)
|
|
108
|
+
clearInterval(interval)
|
|
109
|
+
}
|
|
110
|
+
}, [toolResult])
|
|
111
|
+
|
|
112
|
+
return toolResult
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function App() {
|
|
116
|
+
const data = useToolResult()
|
|
117
|
+
const [step, setStep] = useState<Step>('loadingConnectors')
|
|
118
|
+
const [connectors, setConnectors] = useState<Connector[]>([])
|
|
119
|
+
const [flow, setFlow] = useState<FlowState>(INITIAL_FLOW_STATE)
|
|
120
|
+
const [errorMsg, setErrorMsg] = useState<string | null>(null)
|
|
121
|
+
|
|
122
|
+
// Fetch connector list from developer portal once serverUrl (and optionally rcAccountId) is known
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (!data || data.error) return
|
|
125
|
+
|
|
126
|
+
async function loadConnectors() {
|
|
127
|
+
try {
|
|
128
|
+
dbg.info('loadConnectors: fetching with rcAccountId=', data!.rcAccountId ?? '(none)')
|
|
129
|
+
const list = await fetchConnectors(data!.rcAccountId)
|
|
130
|
+
setConnectors(list)
|
|
131
|
+
setStep('select')
|
|
132
|
+
} catch (err: any) {
|
|
133
|
+
dbg.error('loadConnectors failed:', err.message)
|
|
134
|
+
setStep('error')
|
|
135
|
+
setErrorMsg(err.message || 'Failed to load connectors')
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
loadConnectors()
|
|
140
|
+
}, [data])
|
|
141
|
+
|
|
142
|
+
const resetToSelect = useCallback(() => {
|
|
143
|
+
setStep('select')
|
|
144
|
+
setFlow(INITIAL_FLOW_STATE)
|
|
145
|
+
setErrorMsg(null)
|
|
146
|
+
}, [])
|
|
147
|
+
|
|
148
|
+
const handleConnectorSelect = useCallback(async (connector: Connector) => {
|
|
149
|
+
dbg.info('connector selected:', JSON.stringify(connector))
|
|
150
|
+
setStep('loading')
|
|
151
|
+
setErrorMsg(null)
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const manifest = await fetchManifest(
|
|
155
|
+
connector.id,
|
|
156
|
+
connector.status === 'private',
|
|
157
|
+
data?.rcAccountId,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if (!manifest) {
|
|
161
|
+
setStep('error')
|
|
162
|
+
setErrorMsg('Failed to load connector configuration')
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const connectorName = connector.name
|
|
167
|
+
const platform = manifest?.platforms?.[connectorName]
|
|
168
|
+
|
|
169
|
+
if (!platform) {
|
|
170
|
+
setStep('error')
|
|
171
|
+
setErrorMsg('Invalid connector configuration')
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
setFlow((prev) => ({
|
|
176
|
+
...prev,
|
|
177
|
+
connectorManifest: manifest,
|
|
178
|
+
connectorName,
|
|
179
|
+
connectorDisplayName: connector.displayName,
|
|
180
|
+
}))
|
|
181
|
+
|
|
182
|
+
const envType = platform.environment?.type
|
|
183
|
+
const authType = platform.auth?.type
|
|
184
|
+
|
|
185
|
+
// For oauth with dynamic/selectable environments, collect auth info first
|
|
186
|
+
if (authType === 'oauth' && (envType === 'dynamic' || envType === 'selectable')) {
|
|
187
|
+
setStep('authInfo')
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// For fixed environments, extract hostname from the fixed URL
|
|
192
|
+
if (envType === 'fixed' && platform.environment?.url) {
|
|
193
|
+
try {
|
|
194
|
+
const url = new URL(platform.environment.url)
|
|
195
|
+
setFlow((prev) => ({ ...prev, hostname: url.hostname }))
|
|
196
|
+
} catch {
|
|
197
|
+
// hostname stays empty
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
setStep('oauth')
|
|
202
|
+
} catch (err: any) {
|
|
203
|
+
setStep('error')
|
|
204
|
+
setErrorMsg(err.message || 'Failed to load connector')
|
|
205
|
+
}
|
|
206
|
+
}, [data?.rcAccountId])
|
|
207
|
+
|
|
208
|
+
const handleAuthInfoSubmit = useCallback(
|
|
209
|
+
(value: { hostname?: string; selection?: string }) => {
|
|
210
|
+
try {
|
|
211
|
+
const platform = flow.connectorManifest?.platforms?.[flow.connectorName!]
|
|
212
|
+
const envType = platform?.environment?.type
|
|
213
|
+
let resolvedHostname = ''
|
|
214
|
+
|
|
215
|
+
if (envType === 'dynamic' && value.hostname) {
|
|
216
|
+
resolvedHostname = new URL(value.hostname).hostname
|
|
217
|
+
} else if (envType === 'selectable' && value.selection) {
|
|
218
|
+
const sel = platform.environment.selections?.find(
|
|
219
|
+
(s: { name: string; const: string }) => s.name === value.selection,
|
|
220
|
+
)
|
|
221
|
+
if (sel?.const) {
|
|
222
|
+
resolvedHostname = new URL(sel.const).hostname
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
setFlow((prev) => ({ ...prev, hostname: resolvedHostname }))
|
|
227
|
+
setStep('oauth')
|
|
228
|
+
} catch (err: any) {
|
|
229
|
+
setStep('error')
|
|
230
|
+
setErrorMsg(err.message || 'Failed to process environment selection')
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
[flow.connectorManifest, flow.connectorName],
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
const handleAuthSuccess = useCallback(
|
|
237
|
+
(authData: { jwtToken?: string; userInfo?: any }) => {
|
|
238
|
+
setFlow((prev) => ({ ...prev, userInfo: authData.userInfo ?? null }))
|
|
239
|
+
setStep('success')
|
|
240
|
+
},
|
|
241
|
+
[],
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
const handleAuthError = useCallback(
|
|
245
|
+
(error: string) => {
|
|
246
|
+
if (error === 'retry') {
|
|
247
|
+
setStep('oauth')
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
setStep('error')
|
|
251
|
+
setErrorMsg(error)
|
|
252
|
+
},
|
|
253
|
+
[],
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
// Waiting for initial tool output from ChatGPT
|
|
257
|
+
if (!data) {
|
|
258
|
+
return (
|
|
259
|
+
<div className="flex items-center justify-center min-h-[150px] p-4">
|
|
260
|
+
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary" />
|
|
261
|
+
</div>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Error from initial tool call
|
|
266
|
+
if (data.error) {
|
|
267
|
+
return (
|
|
268
|
+
<div className="p-4">
|
|
269
|
+
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-4">
|
|
270
|
+
<p className="text-yellow-800">{data.errorMessage || 'An error occurred'}</p>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<div className="p-4">
|
|
278
|
+
{step === 'loadingConnectors' && (
|
|
279
|
+
<div className="flex items-center justify-center min-h-[150px]">
|
|
280
|
+
<div className="text-center space-y-3">
|
|
281
|
+
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mx-auto" />
|
|
282
|
+
<p className="text-sm text-secondary">Loading connectors...</p>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
|
|
287
|
+
{step === 'select' && (
|
|
288
|
+
<ConnectorList connectors={connectors} onSelect={handleConnectorSelect} />
|
|
289
|
+
)}
|
|
290
|
+
|
|
291
|
+
{step === 'loading' && (
|
|
292
|
+
<div className="flex items-center justify-center min-h-[150px]">
|
|
293
|
+
<div className="text-center space-y-3">
|
|
294
|
+
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mx-auto" />
|
|
295
|
+
<p className="text-sm text-secondary">Loading...</p>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
)}
|
|
299
|
+
|
|
300
|
+
{step === 'authInfo' && flow.connectorManifest && flow.connectorName && (
|
|
301
|
+
<AuthInfoForm
|
|
302
|
+
environmentType={
|
|
303
|
+
flow.connectorManifest.platforms[flow.connectorName].environment.type
|
|
304
|
+
}
|
|
305
|
+
urlIdentifier={
|
|
306
|
+
flow.connectorManifest.platforms[flow.connectorName].environment.urlIdentifier
|
|
307
|
+
}
|
|
308
|
+
instructions={
|
|
309
|
+
flow.connectorManifest.platforms[flow.connectorName].environment.instructions
|
|
310
|
+
}
|
|
311
|
+
selections={
|
|
312
|
+
flow.connectorManifest.platforms[flow.connectorName].environment.selections
|
|
313
|
+
}
|
|
314
|
+
connectorDisplayName={flow.connectorDisplayName!}
|
|
315
|
+
onSubmit={handleAuthInfoSubmit}
|
|
316
|
+
onBack={resetToSelect}
|
|
317
|
+
/>
|
|
318
|
+
)}
|
|
319
|
+
|
|
320
|
+
{step === 'oauth' && flow.connectorManifest && flow.connectorName && (
|
|
321
|
+
<OAuthConnect
|
|
322
|
+
connectorManifest={flow.connectorManifest}
|
|
323
|
+
connectorName={flow.connectorName}
|
|
324
|
+
connectorDisplayName={flow.connectorDisplayName!}
|
|
325
|
+
hostname={flow.hostname}
|
|
326
|
+
openaiSessionId={data?.openaiSessionId ?? null}
|
|
327
|
+
rcExtensionId={data?.rcExtensionId ?? null}
|
|
328
|
+
onSuccess={handleAuthSuccess}
|
|
329
|
+
onError={handleAuthError}
|
|
330
|
+
onBack={resetToSelect}
|
|
331
|
+
/>
|
|
332
|
+
)}
|
|
333
|
+
|
|
334
|
+
{step === 'success' && (
|
|
335
|
+
<AuthSuccess
|
|
336
|
+
connectorDisplayName={flow.connectorDisplayName!}
|
|
337
|
+
userInfo={flow.userInfo ?? undefined}
|
|
338
|
+
/>
|
|
339
|
+
)}
|
|
340
|
+
|
|
341
|
+
{step === 'error' && (
|
|
342
|
+
<div className="w-full max-w-md">
|
|
343
|
+
<div className="rounded-lg border border-red-200 bg-red-50 p-4 mb-4">
|
|
344
|
+
<p className="text-red-700 text-sm font-semibold">Error</p>
|
|
345
|
+
<p className="text-red-700 text-sm mt-1">{errorMsg || 'An error occurred'}</p>
|
|
346
|
+
</div>
|
|
347
|
+
<button
|
|
348
|
+
type="button"
|
|
349
|
+
onClick={resetToSelect}
|
|
350
|
+
className="text-sm text-primary hover:underline cursor-pointer"
|
|
351
|
+
>
|
|
352
|
+
← Back to connector list
|
|
353
|
+
</button>
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
357
|
+
)
|
|
358
|
+
}
|