@app-connect/core 1.7.24 → 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.
Files changed (137) hide show
  1. package/.env.test +5 -5
  2. package/README.md +441 -441
  3. package/connector/developerPortal.js +31 -42
  4. package/connector/mock.js +84 -77
  5. package/connector/proxy/engine.js +164 -163
  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 -117
  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 +296 -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 +984 -973
  23. package/handlers/managedAuth.js +446 -446
  24. package/handlers/plugin.js +208 -208
  25. package/handlers/user.js +142 -142
  26. package/index.js +3140 -2652
  27. package/jest.config.js +56 -56
  28. package/lib/analytics.js +54 -54
  29. package/lib/authSession.js +109 -109
  30. package/lib/cacheCleanup.js +21 -0
  31. package/lib/callLogComposer.js +898 -898
  32. package/lib/callLogLookup.js +34 -0
  33. package/lib/constants.js +8 -8
  34. package/lib/debugTracer.js +177 -177
  35. package/lib/encode.js +30 -30
  36. package/lib/errorHandler.js +218 -206
  37. package/lib/generalErrorMessage.js +41 -41
  38. package/lib/jwt.js +18 -18
  39. package/lib/logger.js +190 -190
  40. package/lib/migrateCallLogsSchema.js +116 -0
  41. package/lib/ringcentral.js +266 -266
  42. package/lib/s3ErrorLogReport.js +65 -65
  43. package/lib/sharedSMSComposer.js +471 -471
  44. package/lib/util.js +67 -67
  45. package/mcp/README.md +412 -395
  46. package/mcp/lib/validator.js +91 -91
  47. package/mcp/mcpHandler.js +425 -425
  48. package/mcp/tools/cancelAppointment.js +101 -0
  49. package/mcp/tools/checkAuthStatus.js +105 -105
  50. package/mcp/tools/confirmAppointment.js +101 -0
  51. package/mcp/tools/createAppointment.js +157 -0
  52. package/mcp/tools/createCallLog.js +327 -316
  53. package/mcp/tools/createContact.js +117 -117
  54. package/mcp/tools/createMessageLog.js +287 -287
  55. package/mcp/tools/doAuth.js +60 -60
  56. package/mcp/tools/findContactByName.js +93 -93
  57. package/mcp/tools/findContactByPhone.js +101 -101
  58. package/mcp/tools/getCallLog.js +111 -102
  59. package/mcp/tools/getGoogleFilePicker.js +99 -99
  60. package/mcp/tools/getHelp.js +43 -43
  61. package/mcp/tools/getPublicConnectors.js +94 -94
  62. package/mcp/tools/getSessionInfo.js +90 -90
  63. package/mcp/tools/index.js +51 -41
  64. package/mcp/tools/listAppointments.js +163 -0
  65. package/mcp/tools/logout.js +96 -96
  66. package/mcp/tools/rcGetCallLogs.js +65 -65
  67. package/mcp/tools/updateAppointment.js +154 -0
  68. package/mcp/tools/updateCallLog.js +130 -126
  69. package/mcp/ui/App/App.tsx +358 -358
  70. package/mcp/ui/App/components/AuthInfoForm.tsx +113 -113
  71. package/mcp/ui/App/components/AuthSuccess.tsx +22 -22
  72. package/mcp/ui/App/components/ConnectorList.tsx +82 -82
  73. package/mcp/ui/App/components/DebugPanel.tsx +43 -43
  74. package/mcp/ui/App/components/OAuthConnect.tsx +270 -270
  75. package/mcp/ui/App/lib/callTool.ts +130 -130
  76. package/mcp/ui/App/lib/debugLog.ts +41 -41
  77. package/mcp/ui/App/lib/developerPortal.ts +111 -111
  78. package/mcp/ui/App/main.css +5 -5
  79. package/mcp/ui/App/root.tsx +13 -13
  80. package/mcp/ui/index.html +13 -13
  81. package/mcp/ui/package-lock.json +6356 -6356
  82. package/mcp/ui/package.json +25 -25
  83. package/mcp/ui/tsconfig.json +26 -26
  84. package/mcp/ui/vite.config.ts +16 -16
  85. package/models/accountDataModel.js +33 -33
  86. package/models/adminConfigModel.js +35 -35
  87. package/models/cacheModel.js +30 -26
  88. package/models/callDownListModel.js +34 -34
  89. package/models/callLogModel.js +33 -27
  90. package/models/dynamo/connectorSchema.js +146 -146
  91. package/models/dynamo/lockSchema.js +24 -24
  92. package/models/dynamo/noteCacheSchema.js +29 -29
  93. package/models/llmSessionModel.js +17 -17
  94. package/models/messageLogModel.js +25 -25
  95. package/models/sequelize.js +16 -16
  96. package/models/userModel.js +45 -45
  97. package/package.json +72 -72
  98. package/releaseNotes.json +1093 -1073
  99. package/test/connector/proxy/engine.test.js +126 -93
  100. package/test/connector/proxy/index.test.js +279 -279
  101. package/test/connector/proxy/sample.json +161 -161
  102. package/test/connector/registry.test.js +415 -415
  103. package/test/handlers/admin.test.js +616 -616
  104. package/test/handlers/auth.test.js +1018 -1015
  105. package/test/handlers/contact.test.js +1014 -1014
  106. package/test/handlers/log.test.js +1298 -1160
  107. package/test/handlers/managedAuth.test.js +458 -458
  108. package/test/handlers/plugin.test.js +380 -380
  109. package/test/index.test.js +105 -105
  110. package/test/lib/cacheCleanup.test.js +42 -0
  111. package/test/lib/callLogComposer.test.js +1231 -1231
  112. package/test/lib/debugTracer.test.js +328 -328
  113. package/test/lib/jwt.test.js +176 -176
  114. package/test/lib/logger.test.js +206 -206
  115. package/test/lib/oauth.test.js +359 -359
  116. package/test/lib/ringcentral.test.js +467 -467
  117. package/test/lib/sharedSMSComposer.test.js +1084 -1084
  118. package/test/lib/util.test.js +329 -329
  119. package/test/mcp/tools/checkAuthStatus.test.js +83 -82
  120. package/test/mcp/tools/createCallLog.test.js +436 -436
  121. package/test/mcp/tools/createContact.test.js +58 -58
  122. package/test/mcp/tools/createMessageLog.test.js +595 -595
  123. package/test/mcp/tools/doAuth.test.js +113 -113
  124. package/test/mcp/tools/findContactByName.test.js +275 -275
  125. package/test/mcp/tools/findContactByPhone.test.js +296 -296
  126. package/test/mcp/tools/getCallLog.test.js +298 -298
  127. package/test/mcp/tools/getGoogleFilePicker.test.js +281 -281
  128. package/test/mcp/tools/getPublicConnectors.test.js +107 -107
  129. package/test/mcp/tools/getSessionInfo.test.js +127 -127
  130. package/test/mcp/tools/logout.test.js +233 -233
  131. package/test/mcp/tools/rcGetCallLogs.test.js +56 -56
  132. package/test/mcp/tools/updateCallLog.test.js +360 -360
  133. package/test/models/accountDataModel.test.js +98 -98
  134. package/test/models/dynamo/connectorSchema.test.js +189 -189
  135. package/test/models/models.test.js +568 -539
  136. package/test/routes/managedAuthRoutes.test.js +104 -129
  137. 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
+