@app-connect/core 1.7.25 → 1.7.27

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 (138) hide show
  1. package/.env.test +5 -5
  2. package/README.md +441 -441
  3. package/connector/developerPortal.js +31 -31
  4. package/connector/mock.js +84 -77
  5. package/connector/proxy/engine.js +164 -164
  6. package/connector/proxy/index.js +500 -500
  7. package/connector/registry.js +252 -252
  8. package/docs/README.md +50 -50
  9. package/docs/architecture.md +93 -93
  10. package/docs/connectors.md +116 -116
  11. package/docs/handlers.md +125 -125
  12. package/docs/libraries.md +101 -101
  13. package/docs/models.md +144 -144
  14. package/docs/routes.md +115 -115
  15. package/docs/tests.md +73 -73
  16. package/handlers/admin.js +523 -523
  17. package/handlers/appointment.js +193 -0
  18. package/handlers/auth.js +320 -296
  19. package/handlers/calldown.js +99 -99
  20. package/handlers/contact.js +280 -280
  21. package/handlers/disposition.js +82 -80
  22. package/handlers/log.js +990 -973
  23. package/handlers/managedAuth.js +446 -446
  24. package/handlers/managedOAuth.js +247 -0
  25. package/handlers/plugin.js +208 -208
  26. package/handlers/user.js +142 -142
  27. package/index.js +3162 -2652
  28. package/jest.config.js +56 -56
  29. package/lib/analytics.js +54 -54
  30. package/lib/authSession.js +109 -109
  31. package/lib/cacheCleanup.js +21 -0
  32. package/lib/callLogComposer.js +898 -898
  33. package/lib/callLogLookup.js +34 -0
  34. package/lib/constants.js +8 -8
  35. package/lib/debugTracer.js +177 -177
  36. package/lib/encode.js +30 -30
  37. package/lib/errorHandler.js +218 -206
  38. package/lib/generalErrorMessage.js +41 -41
  39. package/lib/jwt.js +18 -18
  40. package/lib/logger.js +190 -190
  41. package/lib/migrateCallLogsSchema.js +116 -0
  42. package/lib/ringcentral.js +266 -266
  43. package/lib/s3ErrorLogReport.js +65 -65
  44. package/lib/sharedSMSComposer.js +471 -471
  45. package/lib/util.js +67 -67
  46. package/mcp/README.md +412 -395
  47. package/mcp/lib/validator.js +91 -91
  48. package/mcp/mcpHandler.js +425 -425
  49. package/mcp/tools/cancelAppointment.js +101 -0
  50. package/mcp/tools/checkAuthStatus.js +105 -105
  51. package/mcp/tools/confirmAppointment.js +101 -0
  52. package/mcp/tools/createAppointment.js +157 -0
  53. package/mcp/tools/createCallLog.js +327 -316
  54. package/mcp/tools/createContact.js +117 -117
  55. package/mcp/tools/createMessageLog.js +287 -287
  56. package/mcp/tools/doAuth.js +60 -60
  57. package/mcp/tools/findContactByName.js +93 -93
  58. package/mcp/tools/findContactByPhone.js +101 -101
  59. package/mcp/tools/getCallLog.js +111 -102
  60. package/mcp/tools/getGoogleFilePicker.js +99 -99
  61. package/mcp/tools/getHelp.js +43 -43
  62. package/mcp/tools/getPublicConnectors.js +94 -94
  63. package/mcp/tools/getSessionInfo.js +90 -90
  64. package/mcp/tools/index.js +51 -41
  65. package/mcp/tools/listAppointments.js +163 -0
  66. package/mcp/tools/logout.js +99 -96
  67. package/mcp/tools/rcGetCallLogs.js +65 -65
  68. package/mcp/tools/updateAppointment.js +154 -0
  69. package/mcp/tools/updateCallLog.js +130 -126
  70. package/mcp/ui/App/App.tsx +358 -358
  71. package/mcp/ui/App/components/AuthInfoForm.tsx +113 -113
  72. package/mcp/ui/App/components/AuthSuccess.tsx +22 -22
  73. package/mcp/ui/App/components/ConnectorList.tsx +82 -82
  74. package/mcp/ui/App/components/DebugPanel.tsx +43 -43
  75. package/mcp/ui/App/components/OAuthConnect.tsx +270 -270
  76. package/mcp/ui/App/lib/callTool.ts +130 -130
  77. package/mcp/ui/App/lib/debugLog.ts +41 -41
  78. package/mcp/ui/App/lib/developerPortal.ts +111 -111
  79. package/mcp/ui/App/main.css +5 -5
  80. package/mcp/ui/App/root.tsx +13 -13
  81. package/mcp/ui/index.html +13 -13
  82. package/mcp/ui/package-lock.json +6356 -6356
  83. package/mcp/ui/package.json +25 -25
  84. package/mcp/ui/tsconfig.json +26 -26
  85. package/mcp/ui/vite.config.ts +16 -16
  86. package/models/accountDataModel.js +33 -33
  87. package/models/adminConfigModel.js +35 -35
  88. package/models/cacheModel.js +30 -26
  89. package/models/callDownListModel.js +34 -34
  90. package/models/callLogModel.js +33 -27
  91. package/models/dynamo/connectorSchema.js +146 -146
  92. package/models/dynamo/lockSchema.js +24 -24
  93. package/models/dynamo/noteCacheSchema.js +29 -29
  94. package/models/llmSessionModel.js +17 -17
  95. package/models/messageLogModel.js +25 -25
  96. package/models/sequelize.js +16 -16
  97. package/models/userModel.js +45 -45
  98. package/package.json +1 -1
  99. package/releaseNotes.json +1101 -1081
  100. package/test/connector/proxy/engine.test.js +126 -126
  101. package/test/connector/proxy/index.test.js +279 -279
  102. package/test/connector/proxy/sample.json +161 -161
  103. package/test/connector/registry.test.js +415 -415
  104. package/test/handlers/admin.test.js +616 -616
  105. package/test/handlers/auth.test.js +1092 -1018
  106. package/test/handlers/contact.test.js +1014 -1014
  107. package/test/handlers/log.test.js +1298 -1160
  108. package/test/handlers/managedAuth.test.js +457 -457
  109. package/test/handlers/plugin.test.js +380 -380
  110. package/test/index.test.js +105 -105
  111. package/test/lib/cacheCleanup.test.js +42 -0
  112. package/test/lib/callLogComposer.test.js +1231 -1231
  113. package/test/lib/debugTracer.test.js +328 -328
  114. package/test/lib/jwt.test.js +176 -176
  115. package/test/lib/logger.test.js +206 -206
  116. package/test/lib/oauth.test.js +359 -359
  117. package/test/lib/ringcentral.test.js +467 -467
  118. package/test/lib/sharedSMSComposer.test.js +1084 -1084
  119. package/test/lib/util.test.js +329 -329
  120. package/test/mcp/tools/checkAuthStatus.test.js +83 -83
  121. package/test/mcp/tools/createCallLog.test.js +436 -436
  122. package/test/mcp/tools/createContact.test.js +58 -58
  123. package/test/mcp/tools/createMessageLog.test.js +595 -595
  124. package/test/mcp/tools/doAuth.test.js +113 -113
  125. package/test/mcp/tools/findContactByName.test.js +275 -275
  126. package/test/mcp/tools/findContactByPhone.test.js +296 -296
  127. package/test/mcp/tools/getCallLog.test.js +298 -298
  128. package/test/mcp/tools/getGoogleFilePicker.test.js +281 -281
  129. package/test/mcp/tools/getPublicConnectors.test.js +107 -107
  130. package/test/mcp/tools/getSessionInfo.test.js +127 -127
  131. package/test/mcp/tools/logout.test.js +233 -233
  132. package/test/mcp/tools/rcGetCallLogs.test.js +56 -56
  133. package/test/mcp/tools/updateCallLog.test.js +360 -360
  134. package/test/models/accountDataModel.test.js +98 -98
  135. package/test/models/dynamo/connectorSchema.test.js +189 -189
  136. package/test/models/models.test.js +568 -539
  137. package/test/routes/managedAuthRoutes.test.js +235 -104
  138. package/test/setup.js +178 -178
@@ -1,130 +1,130 @@
1
- import { dbg } from './debugLog';
2
-
3
- /**
4
- * Server URL for direct HTTP tool calls.
5
- * Set from structuredContent.serverUrl when the initial tool output arrives.
6
- */
7
- let _serverUrl: string | null = null;
8
-
9
- /**
10
- * Send a JSON-RPC 2.0 request to the host (ChatGPT) and await the response.
11
- * Used for MCP Apps bridge calls that require a round-trip (e.g. ui/update-model-context).
12
- */
13
- function rpcRequest(method: string, params: unknown, timeoutMs = 10_000): Promise<unknown> {
14
- return new Promise((resolve, reject) => {
15
- const id = `${method}-${Date.now()}-${Math.random().toString(36).slice(2)}`
16
-
17
- const timer = setTimeout(() => {
18
- window.removeEventListener('message', onMessage)
19
- reject(new Error(`rpcRequest "${method}" timed out after ${timeoutMs}ms`))
20
- }, timeoutMs)
21
-
22
- function onMessage(event: MessageEvent) {
23
- if (event.source !== window.parent) return
24
- const msg = event.data
25
- if (!msg || msg.jsonrpc !== '2.0' || msg.id !== id) return
26
- clearTimeout(timer)
27
- window.removeEventListener('message', onMessage)
28
- if (msg.error) reject(new Error(msg.error.message ?? JSON.stringify(msg.error)))
29
- else resolve(msg.result)
30
- }
31
-
32
- window.addEventListener('message', onMessage, { passive: true })
33
- window.parent.postMessage({ jsonrpc: '2.0', id, method, params }, '*')
34
- })
35
- }
36
-
37
- /**
38
- * Inject text into the model's context without showing it as a visible chat message.
39
- * Uses the MCP Apps bridge ui/update-model-context (preferred), with ui/message as fallback.
40
- */
41
- export async function updateModelContext(text: string): Promise<void> {
42
- const content = [{ type: 'text', text }]
43
-
44
- // Primary: MCP Apps standard — invisible to user, visible to model
45
- try {
46
- await rpcRequest('ui/update-model-context', { content })
47
- dbg.info('updateModelContext: ui/update-model-context succeeded')
48
- return
49
- } catch (err: any) {
50
- dbg.warn('updateModelContext: ui/update-model-context failed, trying ui/message:', err.message)
51
- }
52
-
53
- // Fallback: visible user message (ui/message notification — fire and forget)
54
- try {
55
- window.parent.postMessage(
56
- { jsonrpc: '2.0', method: 'ui/message', params: { role: 'user', content } },
57
- '*',
58
- )
59
- dbg.info('updateModelContext: ui/message sent')
60
- return
61
- } catch (err: any) {
62
- dbg.warn('updateModelContext: ui/message failed, trying window.openai.sendMessage:', err.message)
63
- }
64
-
65
- // Last resort: legacy ChatGPT Apps SDK
66
- const openai = (window as any).openai
67
- if (typeof openai?.sendMessage === 'function') {
68
- openai.sendMessage(text)
69
- dbg.info('updateModelContext: window.openai.sendMessage used')
70
- } else {
71
- dbg.error('updateModelContext: no mechanism available to update model context')
72
- }
73
- }
74
-
75
- export function setServerUrl(url: string) {
76
- _serverUrl = url.replace(/\/+$/, '');
77
- dbg.info('serverUrl set to:', _serverUrl);
78
- }
79
-
80
- export function getServerUrl(): string | null {
81
- return _serverUrl;
82
- }
83
-
84
- /**
85
- * Call a widget-accessible tool on the server via direct fetch to /mcp/widget-tool-call.
86
- *
87
- * NOTE: window.openai.callTool() is intentionally NOT used here.
88
- * It routes through ChatGPT's LLM which drops widget-provided args when the MCP
89
- * tool has no Zod inputSchema registered — args arrive as undefined on the server.
90
- * Direct fetch correctly forwards all args.
91
- */
92
- export async function callTool(
93
- toolName: string,
94
- args: Record<string, unknown> = {},
95
- ): Promise<any> {
96
- dbg.info(`callTool("${toolName}", ${JSON.stringify(args)})`);
97
- dbg.info('_serverUrl:', _serverUrl);
98
-
99
- if (!_serverUrl) {
100
- const msg = 'serverUrl not set — ensure APP_SERVER env var is set and server restarted';
101
- dbg.error(msg);
102
- throw new Error(msg);
103
- }
104
-
105
- const endpoint = `${_serverUrl}/mcp/widget-tool-call`;
106
- const body = JSON.stringify({ tool: toolName, toolArgs: args });
107
- dbg.info('fetch POST', endpoint, 'body:', body);
108
-
109
- try {
110
- const res = await fetch(endpoint, {
111
- method: 'POST',
112
- headers: { 'Content-Type': 'application/json' },
113
- body,
114
- });
115
-
116
- const text = await res.text();
117
- dbg.info('fetch response:', res.status, text.slice(0, 300));
118
-
119
- if (!res.ok) {
120
- let parsed: any = null;
121
- try { parsed = JSON.parse(text); } catch { /* ignore */ }
122
- throw new Error(parsed?.error || `HTTP ${res.status}: ${text}`);
123
- }
124
-
125
- return JSON.parse(text);
126
- } catch (err: any) {
127
- dbg.error('fetch threw:', err.message);
128
- throw err;
129
- }
130
- }
1
+ import { dbg } from './debugLog';
2
+
3
+ /**
4
+ * Server URL for direct HTTP tool calls.
5
+ * Set from structuredContent.serverUrl when the initial tool output arrives.
6
+ */
7
+ let _serverUrl: string | null = null;
8
+
9
+ /**
10
+ * Send a JSON-RPC 2.0 request to the host (ChatGPT) and await the response.
11
+ * Used for MCP Apps bridge calls that require a round-trip (e.g. ui/update-model-context).
12
+ */
13
+ function rpcRequest(method: string, params: unknown, timeoutMs = 10_000): Promise<unknown> {
14
+ return new Promise((resolve, reject) => {
15
+ const id = `${method}-${Date.now()}-${Math.random().toString(36).slice(2)}`
16
+
17
+ const timer = setTimeout(() => {
18
+ window.removeEventListener('message', onMessage)
19
+ reject(new Error(`rpcRequest "${method}" timed out after ${timeoutMs}ms`))
20
+ }, timeoutMs)
21
+
22
+ function onMessage(event: MessageEvent) {
23
+ if (event.source !== window.parent) return
24
+ const msg = event.data
25
+ if (!msg || msg.jsonrpc !== '2.0' || msg.id !== id) return
26
+ clearTimeout(timer)
27
+ window.removeEventListener('message', onMessage)
28
+ if (msg.error) reject(new Error(msg.error.message ?? JSON.stringify(msg.error)))
29
+ else resolve(msg.result)
30
+ }
31
+
32
+ window.addEventListener('message', onMessage, { passive: true })
33
+ window.parent.postMessage({ jsonrpc: '2.0', id, method, params }, '*')
34
+ })
35
+ }
36
+
37
+ /**
38
+ * Inject text into the model's context without showing it as a visible chat message.
39
+ * Uses the MCP Apps bridge ui/update-model-context (preferred), with ui/message as fallback.
40
+ */
41
+ export async function updateModelContext(text: string): Promise<void> {
42
+ const content = [{ type: 'text', text }]
43
+
44
+ // Primary: MCP Apps standard — invisible to user, visible to model
45
+ try {
46
+ await rpcRequest('ui/update-model-context', { content })
47
+ dbg.info('updateModelContext: ui/update-model-context succeeded')
48
+ return
49
+ } catch (err: any) {
50
+ dbg.warn('updateModelContext: ui/update-model-context failed, trying ui/message:', err.message)
51
+ }
52
+
53
+ // Fallback: visible user message (ui/message notification — fire and forget)
54
+ try {
55
+ window.parent.postMessage(
56
+ { jsonrpc: '2.0', method: 'ui/message', params: { role: 'user', content } },
57
+ '*',
58
+ )
59
+ dbg.info('updateModelContext: ui/message sent')
60
+ return
61
+ } catch (err: any) {
62
+ dbg.warn('updateModelContext: ui/message failed, trying window.openai.sendMessage:', err.message)
63
+ }
64
+
65
+ // Last resort: legacy ChatGPT Apps SDK
66
+ const openai = (window as any).openai
67
+ if (typeof openai?.sendMessage === 'function') {
68
+ openai.sendMessage(text)
69
+ dbg.info('updateModelContext: window.openai.sendMessage used')
70
+ } else {
71
+ dbg.error('updateModelContext: no mechanism available to update model context')
72
+ }
73
+ }
74
+
75
+ export function setServerUrl(url: string) {
76
+ _serverUrl = url.replace(/\/+$/, '');
77
+ dbg.info('serverUrl set to:', _serverUrl);
78
+ }
79
+
80
+ export function getServerUrl(): string | null {
81
+ return _serverUrl;
82
+ }
83
+
84
+ /**
85
+ * Call a widget-accessible tool on the server via direct fetch to /mcp/widget-tool-call.
86
+ *
87
+ * NOTE: window.openai.callTool() is intentionally NOT used here.
88
+ * It routes through ChatGPT's LLM which drops widget-provided args when the MCP
89
+ * tool has no Zod inputSchema registered — args arrive as undefined on the server.
90
+ * Direct fetch correctly forwards all args.
91
+ */
92
+ export async function callTool(
93
+ toolName: string,
94
+ args: Record<string, unknown> = {},
95
+ ): Promise<any> {
96
+ dbg.info(`callTool("${toolName}", ${JSON.stringify(args)})`);
97
+ dbg.info('_serverUrl:', _serverUrl);
98
+
99
+ if (!_serverUrl) {
100
+ const msg = 'serverUrl not set — ensure APP_SERVER env var is set and server restarted';
101
+ dbg.error(msg);
102
+ throw new Error(msg);
103
+ }
104
+
105
+ const endpoint = `${_serverUrl}/mcp/widget-tool-call`;
106
+ const body = JSON.stringify({ tool: toolName, toolArgs: args });
107
+ dbg.info('fetch POST', endpoint, 'body:', body);
108
+
109
+ try {
110
+ const res = await fetch(endpoint, {
111
+ method: 'POST',
112
+ headers: { 'Content-Type': 'application/json' },
113
+ body,
114
+ });
115
+
116
+ const text = await res.text();
117
+ dbg.info('fetch response:', res.status, text.slice(0, 300));
118
+
119
+ if (!res.ok) {
120
+ let parsed: any = null;
121
+ try { parsed = JSON.parse(text); } catch { /* ignore */ }
122
+ throw new Error(parsed?.error || `HTTP ${res.status}: ${text}`);
123
+ }
124
+
125
+ return JSON.parse(text);
126
+ } catch (err: any) {
127
+ dbg.error('fetch threw:', err.message);
128
+ throw err;
129
+ }
130
+ }
@@ -1,41 +1,41 @@
1
- export interface DebugEntry {
2
- time: string
3
- level: 'info' | 'warn' | 'error'
4
- msg: string
5
- }
6
-
7
- const MAX_ENTRIES = 50;
8
- const entries: DebugEntry[] = [];
9
- const listeners = new Set<() => void>();
10
-
11
- function ts() {
12
- return new Date().toISOString().slice(11, 23); // HH:MM:SS.mmm
13
- }
14
-
15
- function push(level: DebugEntry['level'], parts: unknown[]) {
16
- const msg = parts
17
- .map((p) => (typeof p === 'object' ? JSON.stringify(p) : String(p)))
18
- .join(' ');
19
- entries.push({ time: ts(), level, msg });
20
- if (entries.length > MAX_ENTRIES) entries.shift();
21
- // Mirror to real console
22
- if (level === 'error') console.error('[debug]', msg);
23
- else if (level === 'warn') console.warn('[debug]', msg);
24
- else console.log('[debug]', msg);
25
- listeners.forEach((fn) => fn());
26
- }
27
-
28
- export const dbg = {
29
- info: (...args: unknown[]) => push('info', args),
30
- warn: (...args: unknown[]) => push('warn', args),
31
- error: (...args: unknown[]) => push('error', args),
32
- };
33
-
34
- export function getEntries(): DebugEntry[] {
35
- return [...entries];
36
- }
37
-
38
- export function subscribe(fn: () => void): () => void {
39
- listeners.add(fn);
40
- return () => listeners.delete(fn);
41
- }
1
+ export interface DebugEntry {
2
+ time: string
3
+ level: 'info' | 'warn' | 'error'
4
+ msg: string
5
+ }
6
+
7
+ const MAX_ENTRIES = 50;
8
+ const entries: DebugEntry[] = [];
9
+ const listeners = new Set<() => void>();
10
+
11
+ function ts() {
12
+ return new Date().toISOString().slice(11, 23); // HH:MM:SS.mmm
13
+ }
14
+
15
+ function push(level: DebugEntry['level'], parts: unknown[]) {
16
+ const msg = parts
17
+ .map((p) => (typeof p === 'object' ? JSON.stringify(p) : String(p)))
18
+ .join(' ');
19
+ entries.push({ time: ts(), level, msg });
20
+ if (entries.length > MAX_ENTRIES) entries.shift();
21
+ // Mirror to real console
22
+ if (level === 'error') console.error('[debug]', msg);
23
+ else if (level === 'warn') console.warn('[debug]', msg);
24
+ else console.log('[debug]', msg);
25
+ listeners.forEach((fn) => fn());
26
+ }
27
+
28
+ export const dbg = {
29
+ info: (...args: unknown[]) => push('info', args),
30
+ warn: (...args: unknown[]) => push('warn', args),
31
+ error: (...args: unknown[]) => push('error', args),
32
+ };
33
+
34
+ export function getEntries(): DebugEntry[] {
35
+ return [...entries];
36
+ }
37
+
38
+ export function subscribe(fn: () => void): () => void {
39
+ listeners.add(fn);
40
+ return () => listeners.delete(fn);
41
+ }
@@ -1,111 +1,111 @@
1
- import { dbg } from './debugLog'
2
-
3
- const PORTAL_BASE = 'https://appconnect.labs.ringcentral.com/public-api'
4
-
5
- export const SUPPORTED_PLATFORMS = ['clio']
6
-
7
- export interface Connector {
8
- id: string
9
- name: string
10
- displayName: string
11
- description?: string
12
- status?: 'public' | 'private'
13
- }
14
-
15
- /**
16
- * Fetch the list of available connectors directly from the developer portal.
17
- * Merges public connectors with private ones (if rcAccountId is provided),
18
- * then filters to SUPPORTED_PLATFORMS.
19
- */
20
- export async function fetchConnectors(rcAccountId?: string | null): Promise<Connector[]> {
21
- dbg.info('fetchConnectors: rcAccountId=', rcAccountId ?? '(none)')
22
-
23
- const results: Connector[] = []
24
-
25
- // Public connectors
26
- const publicUrl = `${PORTAL_BASE}/connectors`
27
- dbg.info('fetchConnectors: GET', publicUrl)
28
- try {
29
- const resp = await fetch(publicUrl)
30
- dbg.info('fetchConnectors: public response status=', resp.status, 'ok=', resp.ok)
31
- if (!resp.ok) {
32
- const body = await resp.text().catch(() => '(unreadable)')
33
- throw new Error(`HTTP ${resp.status}: ${body.slice(0, 200)}`)
34
- }
35
- const data = await resp.json()
36
- dbg.info('fetchConnectors: public raw keys=', Object.keys(data).join(', '))
37
- const list: any[] = data?.connectors ?? data ?? []
38
- results.push(...list)
39
- dbg.info('fetchConnectors: public count=', list.length)
40
- } catch (err: any) {
41
- dbg.error('fetchConnectors: failed to fetch public connectors:', err.message, '| name=', err.name, '| url=', publicUrl)
42
- throw new Error(`Failed to load connectors: ${err.message}`)
43
- }
44
-
45
- // Private connectors (only if account ID is available)
46
- if (rcAccountId) {
47
- const privateUrl = `${PORTAL_BASE}/connectors/internal?accountId=${encodeURIComponent(rcAccountId)}`
48
- dbg.info('fetchConnectors: GET', privateUrl)
49
- try {
50
- const resp = await fetch(privateUrl)
51
- dbg.info('fetchConnectors: private response status=', resp.status, 'ok=', resp.ok)
52
- if (resp.ok) {
53
- const data = await resp.json()
54
- dbg.info('fetchConnectors: private raw keys=', Object.keys(data).join(', '))
55
- const list: any[] = data?.privateConnectors ?? data ?? []
56
- results.push(...list)
57
- dbg.info('fetchConnectors: private count=', list.length)
58
- } else {
59
- const body = await resp.text().catch(() => '(unreadable)')
60
- dbg.warn('fetchConnectors: private non-ok response:', resp.status, body.slice(0, 200))
61
- }
62
- } catch (err: any) {
63
- dbg.warn('fetchConnectors: failed to fetch private connectors (non-fatal):', err.message, '| name=', err.name)
64
- }
65
- }
66
-
67
- // Filter to supported platforms and normalise shape
68
- const supported = results
69
- .filter((c) => SUPPORTED_PLATFORMS.includes(c.name))
70
- .map((c): Connector => ({
71
- id: c.id,
72
- name: c.name,
73
- displayName: c.displayName,
74
- description: c.description || `Connect to ${c.displayName}`,
75
- status: (c.status as 'public' | 'private') ?? 'public',
76
- }))
77
-
78
- dbg.info('fetchConnectors: supported=', supported.map((c) => c.name).join(', '))
79
- return supported
80
- }
81
-
82
- /**
83
- * Fetch the manifest for a specific connector directly from the developer portal.
84
- */
85
- export async function fetchManifest(
86
- connectorId: string,
87
- isPrivate: boolean,
88
- rcAccountId?: string | null,
89
- ): Promise<any> {
90
- dbg.info('fetchManifest: connectorId=', connectorId, 'isPrivate=', isPrivate)
91
-
92
- const url = isPrivate && rcAccountId
93
- ? `${PORTAL_BASE}/connectors/${connectorId}/manifest?access=internal&type=connector&accountId=${encodeURIComponent(rcAccountId)}`
94
- : `${PORTAL_BASE}/connectors/${connectorId}/manifest`
95
-
96
- dbg.info('fetchManifest: GET', url)
97
- try {
98
- const resp = await fetch(url)
99
- dbg.info('fetchManifest: response status=', resp.status, 'ok=', resp.ok)
100
- if (!resp.ok) {
101
- const body = await resp.text().catch(() => '(unreadable)')
102
- throw new Error(`HTTP ${resp.status}: ${body.slice(0, 200)}`)
103
- }
104
- const manifest = await resp.json()
105
- dbg.info('fetchManifest: loaded platforms=', Object.keys(manifest?.platforms ?? {}).join(', '))
106
- return manifest
107
- } catch (err: any) {
108
- dbg.error('fetchManifest: failed:', err.message, '| name=', err.name, '| url=', url)
109
- throw err
110
- }
111
- }
1
+ import { dbg } from './debugLog'
2
+
3
+ const PORTAL_BASE = 'https://appconnect.labs.ringcentral.com/public-api'
4
+
5
+ export const SUPPORTED_PLATFORMS = ['clio']
6
+
7
+ export interface Connector {
8
+ id: string
9
+ name: string
10
+ displayName: string
11
+ description?: string
12
+ status?: 'public' | 'private'
13
+ }
14
+
15
+ /**
16
+ * Fetch the list of available connectors directly from the developer portal.
17
+ * Merges public connectors with private ones (if rcAccountId is provided),
18
+ * then filters to SUPPORTED_PLATFORMS.
19
+ */
20
+ export async function fetchConnectors(rcAccountId?: string | null): Promise<Connector[]> {
21
+ dbg.info('fetchConnectors: rcAccountId=', rcAccountId ?? '(none)')
22
+
23
+ const results: Connector[] = []
24
+
25
+ // Public connectors
26
+ const publicUrl = `${PORTAL_BASE}/connectors`
27
+ dbg.info('fetchConnectors: GET', publicUrl)
28
+ try {
29
+ const resp = await fetch(publicUrl)
30
+ dbg.info('fetchConnectors: public response status=', resp.status, 'ok=', resp.ok)
31
+ if (!resp.ok) {
32
+ const body = await resp.text().catch(() => '(unreadable)')
33
+ throw new Error(`HTTP ${resp.status}: ${body.slice(0, 200)}`)
34
+ }
35
+ const data = await resp.json()
36
+ dbg.info('fetchConnectors: public raw keys=', Object.keys(data).join(', '))
37
+ const list: any[] = data?.connectors ?? data ?? []
38
+ results.push(...list)
39
+ dbg.info('fetchConnectors: public count=', list.length)
40
+ } catch (err: any) {
41
+ dbg.error('fetchConnectors: failed to fetch public connectors:', err.message, '| name=', err.name, '| url=', publicUrl)
42
+ throw new Error(`Failed to load connectors: ${err.message}`)
43
+ }
44
+
45
+ // Private connectors (only if account ID is available)
46
+ if (rcAccountId) {
47
+ const privateUrl = `${PORTAL_BASE}/connectors/internal?accountId=${encodeURIComponent(rcAccountId)}`
48
+ dbg.info('fetchConnectors: GET', privateUrl)
49
+ try {
50
+ const resp = await fetch(privateUrl)
51
+ dbg.info('fetchConnectors: private response status=', resp.status, 'ok=', resp.ok)
52
+ if (resp.ok) {
53
+ const data = await resp.json()
54
+ dbg.info('fetchConnectors: private raw keys=', Object.keys(data).join(', '))
55
+ const list: any[] = data?.privateConnectors ?? data ?? []
56
+ results.push(...list)
57
+ dbg.info('fetchConnectors: private count=', list.length)
58
+ } else {
59
+ const body = await resp.text().catch(() => '(unreadable)')
60
+ dbg.warn('fetchConnectors: private non-ok response:', resp.status, body.slice(0, 200))
61
+ }
62
+ } catch (err: any) {
63
+ dbg.warn('fetchConnectors: failed to fetch private connectors (non-fatal):', err.message, '| name=', err.name)
64
+ }
65
+ }
66
+
67
+ // Filter to supported platforms and normalise shape
68
+ const supported = results
69
+ .filter((c) => SUPPORTED_PLATFORMS.includes(c.name))
70
+ .map((c): Connector => ({
71
+ id: c.id,
72
+ name: c.name,
73
+ displayName: c.displayName,
74
+ description: c.description || `Connect to ${c.displayName}`,
75
+ status: (c.status as 'public' | 'private') ?? 'public',
76
+ }))
77
+
78
+ dbg.info('fetchConnectors: supported=', supported.map((c) => c.name).join(', '))
79
+ return supported
80
+ }
81
+
82
+ /**
83
+ * Fetch the manifest for a specific connector directly from the developer portal.
84
+ */
85
+ export async function fetchManifest(
86
+ connectorId: string,
87
+ isPrivate: boolean,
88
+ rcAccountId?: string | null,
89
+ ): Promise<any> {
90
+ dbg.info('fetchManifest: connectorId=', connectorId, 'isPrivate=', isPrivate)
91
+
92
+ const url = isPrivate && rcAccountId
93
+ ? `${PORTAL_BASE}/connectors/${connectorId}/manifest?access=internal&type=connector&accountId=${encodeURIComponent(rcAccountId)}`
94
+ : `${PORTAL_BASE}/connectors/${connectorId}/manifest`
95
+
96
+ dbg.info('fetchManifest: GET', url)
97
+ try {
98
+ const resp = await fetch(url)
99
+ dbg.info('fetchManifest: response status=', resp.status, 'ok=', resp.ok)
100
+ if (!resp.ok) {
101
+ const body = await resp.text().catch(() => '(unreadable)')
102
+ throw new Error(`HTTP ${resp.status}: ${body.slice(0, 200)}`)
103
+ }
104
+ const manifest = await resp.json()
105
+ dbg.info('fetchManifest: loaded platforms=', Object.keys(manifest?.platforms ?? {}).join(', '))
106
+ return manifest
107
+ } catch (err: any) {
108
+ dbg.error('fetchManifest: failed:', err.message, '| name=', err.name, '| url=', url)
109
+ throw err
110
+ }
111
+ }
@@ -1,6 +1,6 @@
1
- @import "tailwindcss";
2
- @import "@openai/apps-sdk-ui/css";
3
- /* Required for Tailwind to find class references in Apps SDK UI components. */
4
- @source "../../../node_modules/@openai/apps-sdk-ui";
5
-
1
+ @import "tailwindcss";
2
+ @import "@openai/apps-sdk-ui/css";
3
+ /* Required for Tailwind to find class references in Apps SDK UI components. */
4
+ @source "../../../node_modules/@openai/apps-sdk-ui";
5
+
6
6
  /* The rest of your application CSS */
@@ -1,13 +1,13 @@
1
- // Must be imported first to ensure Tailwind layers and style foundations are defined before any potential component styles
2
- import "./main.css"
3
-
4
- import { StrictMode } from "react"
5
- import { createRoot } from "react-dom/client"
6
- import { App } from "./App"
7
-
8
- createRoot(document.getElementById("root")!).render(
9
- <StrictMode>
10
- <App />
11
- </StrictMode>,
12
- )
13
-
1
+ // Must be imported first to ensure Tailwind layers and style foundations are defined before any potential component styles
2
+ import "./main.css"
3
+
4
+ import { StrictMode } from "react"
5
+ import { createRoot } from "react-dom/client"
6
+ import { App } from "./App"
7
+
8
+ createRoot(document.getElementById("root")!).render(
9
+ <StrictMode>
10
+ <App />
11
+ </StrictMode>,
12
+ )
13
+
package/mcp/ui/index.html CHANGED
@@ -1,13 +1,13 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>RC App Connect</title>
7
- </head>
8
- <body>
9
- <div id="root"></div>
10
- <script type="module" src="/App/root.tsx"></script>
11
- </body>
12
- </html>
13
-
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>RC App Connect</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/App/root.tsx"></script>
11
+ </body>
12
+ </html>
13
+