@danainnovations/cortex-mcp 1.0.60 → 1.0.62
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/dist/browser-WFTDXNKP.js +8 -0
- package/dist/{chunk-DIHPZXVU.js → chunk-AUMRZ53L.js} +14 -3
- package/dist/chunk-AUMRZ53L.js.map +1 -0
- package/dist/{chunk-WDD2YERC.js → chunk-JJURHODD.js} +13 -13
- package/dist/chunk-JJURHODD.js.map +1 -0
- package/dist/cli.js +2497 -1018
- package/dist/cli.js.map +1 -1
- package/dist/{constants-YJ6WYI46.js → constants-2D6W3HYY.js} +2 -2
- package/dist/index.d.ts +12 -12
- package/dist/index.js +142 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/browser-6BYJNEZN.js +0 -8
- package/dist/chunk-DIHPZXVU.js.map +0 -1
- package/dist/chunk-WDD2YERC.js.map +0 -1
- /package/dist/{browser-6BYJNEZN.js.map → browser-WFTDXNKP.js.map} +0 -0
- /package/dist/{constants-YJ6WYI46.js.map → constants-2D6W3HYY.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -9,74 +9,1368 @@ import {
|
|
|
9
9
|
DEFAULT_SERVER_URL,
|
|
10
10
|
MCP_NAMES,
|
|
11
11
|
PROTOCOL_VERSION
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-JJURHODD.js";
|
|
13
13
|
import {
|
|
14
|
+
getAntigravityConfigPath,
|
|
14
15
|
getClaudeDesktopConfigPath,
|
|
15
16
|
getCodexConfigPath,
|
|
16
17
|
getCursorConfigPath,
|
|
17
18
|
getHomeDir,
|
|
19
|
+
getPlatform,
|
|
20
|
+
getVSCodeMcpConfigPath,
|
|
18
21
|
openBrowser
|
|
19
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-AUMRZ53L.js";
|
|
20
23
|
|
|
21
24
|
// bin/cli.ts
|
|
22
25
|
import { Command } from "commander";
|
|
23
26
|
|
|
24
|
-
// src/
|
|
25
|
-
import * as
|
|
27
|
+
// src/wizard/server.ts
|
|
28
|
+
import * as http from "http";
|
|
26
29
|
|
|
27
|
-
// src/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
30
|
+
// src/wizard/html.ts
|
|
31
|
+
function getWizardHtml() {
|
|
32
|
+
return `<!DOCTYPE html>
|
|
33
|
+
<html lang="en">
|
|
34
|
+
<head>
|
|
35
|
+
<meta charset="UTF-8">
|
|
36
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
37
|
+
<title>Cortex MCP Setup</title>
|
|
38
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
39
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
40
|
+
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600&display=swap" rel="stylesheet">
|
|
41
|
+
<style>
|
|
42
|
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
43
|
+
|
|
44
|
+
body {
|
|
45
|
+
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
46
|
+
background: #1a1f24;
|
|
47
|
+
color: #fff;
|
|
48
|
+
min-height: 100vh;
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
line-height: 1.6;
|
|
53
|
+
-webkit-font-smoothing: antialiased;
|
|
54
|
+
-moz-osx-font-smoothing: grayscale;
|
|
55
|
+
overflow-x: hidden;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* \u2500\u2500 Blue Orb Background \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
59
|
+
.orb {
|
|
60
|
+
position: fixed;
|
|
61
|
+
border-radius: 50%;
|
|
62
|
+
pointer-events: none;
|
|
63
|
+
z-index: 0;
|
|
64
|
+
will-change: transform;
|
|
65
|
+
transition: transform 0.8s cubic-bezier(0.16, 1, 0.3, 1);
|
|
66
|
+
}
|
|
67
|
+
.orb-primary {
|
|
68
|
+
width: 700px;
|
|
69
|
+
height: 700px;
|
|
70
|
+
background: radial-gradient(circle, rgba(0, 163, 225, 0.07) 0%, transparent 70%);
|
|
71
|
+
filter: blur(100px);
|
|
72
|
+
top: 50%;
|
|
73
|
+
left: 50%;
|
|
74
|
+
transform: translate(-50%, -50%);
|
|
75
|
+
}
|
|
76
|
+
.orb-secondary {
|
|
77
|
+
width: 500px;
|
|
78
|
+
height: 500px;
|
|
79
|
+
background: radial-gradient(circle, rgba(120, 80, 220, 0.05) 0%, transparent 70%);
|
|
80
|
+
filter: blur(80px);
|
|
81
|
+
top: 50%;
|
|
82
|
+
left: 50%;
|
|
83
|
+
transform: translate(-50%, -50%);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#app {
|
|
87
|
+
width: 100%;
|
|
88
|
+
max-width: 900px;
|
|
89
|
+
padding: 24px;
|
|
90
|
+
position: relative;
|
|
91
|
+
z-index: 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* \u2500\u2500 Persistent Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
95
|
+
.wizard-header {
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
justify-content: space-between;
|
|
99
|
+
padding: 0 8px 24px;
|
|
100
|
+
}
|
|
101
|
+
.wizard-header-left {
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
gap: 12px;
|
|
105
|
+
}
|
|
106
|
+
.header-logo {
|
|
107
|
+
height: 18px;
|
|
108
|
+
width: auto;
|
|
109
|
+
opacity: 0.85;
|
|
110
|
+
}
|
|
111
|
+
.header-divider {
|
|
112
|
+
width: 1px;
|
|
113
|
+
height: 16px;
|
|
114
|
+
background: rgba(255, 255, 255, 0.15);
|
|
115
|
+
}
|
|
116
|
+
.header-product {
|
|
117
|
+
font-size: 13px;
|
|
118
|
+
font-weight: 500;
|
|
119
|
+
color: #00A3E1;
|
|
120
|
+
letter-spacing: 0.08em;
|
|
121
|
+
text-transform: uppercase;
|
|
122
|
+
}
|
|
123
|
+
.header-steps {
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
gap: 6px;
|
|
127
|
+
}
|
|
128
|
+
.header-dot {
|
|
129
|
+
width: 8px;
|
|
130
|
+
height: 8px;
|
|
131
|
+
border-radius: 50%;
|
|
132
|
+
background: rgba(255, 255, 255, 0.1);
|
|
133
|
+
transition: all 0.4s ease;
|
|
134
|
+
}
|
|
135
|
+
.header-dot.done { background: #00A3E1; }
|
|
136
|
+
.header-dot.current { background: #00A3E1; opacity: 0.5; }
|
|
137
|
+
|
|
138
|
+
/* \u2500\u2500 Steps \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
139
|
+
.step { display: none; }
|
|
140
|
+
.step.active { display: block; }
|
|
141
|
+
.step.active.slide-forward { animation: slideInRight 0.35s cubic-bezier(0.16, 1, 0.3, 1); }
|
|
142
|
+
.step.active.slide-backward { animation: slideInLeft 0.35s cubic-bezier(0.16, 1, 0.3, 1); }
|
|
143
|
+
|
|
144
|
+
@keyframes slideInRight {
|
|
145
|
+
from { opacity: 0; transform: translateX(40px); }
|
|
146
|
+
to { opacity: 1; transform: translateX(0); }
|
|
147
|
+
}
|
|
148
|
+
@keyframes slideInLeft {
|
|
149
|
+
from { opacity: 0; transform: translateX(-40px); }
|
|
150
|
+
to { opacity: 1; transform: translateX(0); }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* \u2500\u2500 Liquid Glass Card \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
154
|
+
.card {
|
|
155
|
+
background: rgba(255, 255, 255, 0.03);
|
|
156
|
+
backdrop-filter: blur(20px);
|
|
157
|
+
-webkit-backdrop-filter: blur(20px);
|
|
158
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
159
|
+
border-radius: 20px;
|
|
160
|
+
padding: 40px;
|
|
161
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* \u2500\u2500 Typography \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
165
|
+
h1 {
|
|
166
|
+
font-size: 26px;
|
|
167
|
+
font-weight: 300;
|
|
168
|
+
letter-spacing: -0.02em;
|
|
169
|
+
color: #fff;
|
|
170
|
+
margin-bottom: 8px;
|
|
171
|
+
}
|
|
172
|
+
h2 {
|
|
173
|
+
font-size: 20px;
|
|
174
|
+
font-weight: 300;
|
|
175
|
+
letter-spacing: -0.02em;
|
|
176
|
+
color: #fff;
|
|
177
|
+
margin-bottom: 6px;
|
|
178
|
+
}
|
|
179
|
+
.subtitle {
|
|
180
|
+
color: #8f999f;
|
|
181
|
+
font-size: 14px;
|
|
182
|
+
font-weight: 400;
|
|
183
|
+
margin-bottom: 28px;
|
|
184
|
+
line-height: 1.6;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* \u2500\u2500 Welcome \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
188
|
+
.welcome-tagline {
|
|
189
|
+
font-size: 15px;
|
|
190
|
+
color: #00A3E1;
|
|
191
|
+
font-weight: 400;
|
|
192
|
+
margin-bottom: 8px;
|
|
193
|
+
}
|
|
194
|
+
.welcome-hero { padding: 16px 0 28px; }
|
|
195
|
+
.welcome-footer {
|
|
196
|
+
font-size: 11px;
|
|
197
|
+
color: #6b7780;
|
|
198
|
+
margin-top: 20px;
|
|
199
|
+
letter-spacing: 0.04em;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* \u2500\u2500 Buttons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
203
|
+
.btn {
|
|
204
|
+
display: inline-flex;
|
|
205
|
+
align-items: center;
|
|
206
|
+
justify-content: center;
|
|
207
|
+
gap: 8px;
|
|
208
|
+
padding: 11px 24px;
|
|
209
|
+
border-radius: 12px;
|
|
210
|
+
font-family: 'Montserrat', sans-serif;
|
|
211
|
+
font-size: 13px;
|
|
212
|
+
font-weight: 500;
|
|
213
|
+
letter-spacing: 0.02em;
|
|
214
|
+
text-transform: uppercase;
|
|
215
|
+
cursor: pointer;
|
|
216
|
+
border: none;
|
|
217
|
+
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
218
|
+
text-decoration: none;
|
|
219
|
+
}
|
|
220
|
+
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
221
|
+
|
|
222
|
+
.btn-primary {
|
|
223
|
+
background: #333F48;
|
|
224
|
+
color: #fff;
|
|
225
|
+
}
|
|
226
|
+
.btn-primary:hover:not(:disabled) {
|
|
227
|
+
background: #3a444c;
|
|
228
|
+
box-shadow: 0 0 20px rgba(0, 163, 225, 0.15);
|
|
229
|
+
}
|
|
230
|
+
.btn-primary:active:not(:disabled) { transform: scale(0.98); }
|
|
231
|
+
|
|
232
|
+
.btn-accent {
|
|
233
|
+
background: #00A3E1;
|
|
234
|
+
color: #fff;
|
|
235
|
+
}
|
|
236
|
+
.btn-accent:hover:not(:disabled) {
|
|
237
|
+
background: #0093cb;
|
|
238
|
+
box-shadow: 0 0 24px rgba(0, 163, 225, 0.3);
|
|
239
|
+
}
|
|
240
|
+
.btn-accent:active:not(:disabled) { transform: scale(0.98); }
|
|
241
|
+
|
|
242
|
+
.btn-secondary {
|
|
243
|
+
background: rgba(255, 255, 255, 0.03);
|
|
244
|
+
color: #D9D9D6;
|
|
245
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
246
|
+
}
|
|
247
|
+
.btn-secondary:hover:not(:disabled) {
|
|
248
|
+
background: rgba(255, 255, 255, 0.06);
|
|
249
|
+
border-color: rgba(255, 255, 255, 0.12);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.btn-sm { padding: 7px 16px; font-size: 12px; }
|
|
253
|
+
.btn-block { width: 100%; }
|
|
254
|
+
|
|
255
|
+
.btn-row {
|
|
256
|
+
display: flex;
|
|
257
|
+
gap: 10px;
|
|
258
|
+
margin-top: 28px;
|
|
259
|
+
}
|
|
260
|
+
.btn-row .btn { flex: 1; }
|
|
261
|
+
|
|
262
|
+
/* \u2500\u2500 Spinner \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
263
|
+
.spinner {
|
|
264
|
+
display: inline-block;
|
|
265
|
+
width: 14px;
|
|
266
|
+
height: 14px;
|
|
267
|
+
border: 2px solid rgba(255, 255, 255, 0.1);
|
|
268
|
+
border-top-color: #00A3E1;
|
|
269
|
+
border-radius: 50%;
|
|
270
|
+
animation: spin 0.7s linear infinite;
|
|
271
|
+
}
|
|
272
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
273
|
+
|
|
274
|
+
/* \u2500\u2500 Status Messages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
275
|
+
.status {
|
|
276
|
+
display: flex;
|
|
277
|
+
align-items: center;
|
|
278
|
+
gap: 10px;
|
|
279
|
+
padding: 12px 16px;
|
|
280
|
+
border-radius: 12px;
|
|
281
|
+
font-size: 13px;
|
|
282
|
+
font-weight: 400;
|
|
283
|
+
margin-top: 16px;
|
|
284
|
+
}
|
|
285
|
+
.status-info {
|
|
286
|
+
background: rgba(0, 163, 225, 0.08);
|
|
287
|
+
color: #7dd3fc;
|
|
288
|
+
border: 1px solid rgba(0, 163, 225, 0.15);
|
|
289
|
+
}
|
|
290
|
+
.status-success {
|
|
291
|
+
background: rgba(76, 175, 80, 0.1);
|
|
292
|
+
color: #4CAF50;
|
|
293
|
+
border: 1px solid rgba(76, 175, 80, 0.2);
|
|
294
|
+
}
|
|
295
|
+
.status-error {
|
|
296
|
+
background: rgba(239, 83, 80, 0.1);
|
|
297
|
+
color: #EF5350;
|
|
298
|
+
border: 1px solid rgba(239, 83, 80, 0.2);
|
|
299
|
+
}
|
|
300
|
+
.status-waiting {
|
|
301
|
+
background: rgba(0, 163, 225, 0.06);
|
|
302
|
+
color: #8f999f;
|
|
303
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* \u2500\u2500 Auth Phases \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
307
|
+
.auth-phases {
|
|
308
|
+
display: flex;
|
|
309
|
+
flex-direction: column;
|
|
310
|
+
gap: 4px;
|
|
311
|
+
margin-top: 16px;
|
|
312
|
+
}
|
|
313
|
+
.auth-phase {
|
|
314
|
+
display: flex;
|
|
315
|
+
align-items: center;
|
|
316
|
+
gap: 10px;
|
|
317
|
+
font-size: 13px;
|
|
318
|
+
color: #6b7780;
|
|
319
|
+
padding: 10px 14px;
|
|
320
|
+
border-radius: 10px;
|
|
321
|
+
transition: all 0.3s ease;
|
|
322
|
+
}
|
|
323
|
+
.auth-phase.active {
|
|
324
|
+
color: #7dd3fc;
|
|
325
|
+
background: rgba(0, 163, 225, 0.06);
|
|
326
|
+
}
|
|
327
|
+
.auth-phase.done { color: #4CAF50; }
|
|
328
|
+
.auth-phase.pending { opacity: 0.35; }
|
|
329
|
+
.phase-icon { font-size: 12px; flex-shrink: 0; width: 14px; text-align: center; }
|
|
330
|
+
|
|
331
|
+
/* \u2500\u2500 MCP Section \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
332
|
+
.mcp-section { margin-bottom: 16px; }
|
|
333
|
+
.mcp-section-title {
|
|
334
|
+
font-size: 11px;
|
|
335
|
+
font-weight: 500;
|
|
336
|
+
color: #8f999f;
|
|
337
|
+
text-transform: uppercase;
|
|
338
|
+
letter-spacing: 0.08em;
|
|
339
|
+
margin-bottom: 8px;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/* \u2500\u2500 MCP List \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
343
|
+
.mcp-list {
|
|
344
|
+
display: grid;
|
|
345
|
+
grid-template-columns: repeat(2, 1fr);
|
|
346
|
+
gap: 6px;
|
|
347
|
+
margin-bottom: 8px;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.mcp-item {
|
|
351
|
+
display: flex;
|
|
352
|
+
align-items: center;
|
|
353
|
+
gap: 10px;
|
|
354
|
+
padding: 11px 14px;
|
|
355
|
+
border-radius: 12px;
|
|
356
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
357
|
+
cursor: pointer;
|
|
358
|
+
transition: all 0.15s ease;
|
|
359
|
+
user-select: none;
|
|
360
|
+
position: relative;
|
|
361
|
+
}
|
|
362
|
+
.mcp-item:hover { border-color: rgba(0, 163, 225, 0.3); }
|
|
363
|
+
.mcp-tooltip {
|
|
364
|
+
display: none;
|
|
365
|
+
position: absolute;
|
|
366
|
+
bottom: calc(100% + 8px);
|
|
367
|
+
left: 50%;
|
|
368
|
+
transform: translateX(-50%);
|
|
369
|
+
background: #2a3038;
|
|
370
|
+
color: #d0d5da;
|
|
371
|
+
font-size: 11px;
|
|
372
|
+
font-weight: 400;
|
|
373
|
+
padding: 6px 10px;
|
|
374
|
+
border-radius: 6px;
|
|
375
|
+
white-space: nowrap;
|
|
376
|
+
pointer-events: none;
|
|
377
|
+
z-index: 10;
|
|
378
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
379
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
380
|
+
}
|
|
381
|
+
.mcp-tooltip::after {
|
|
382
|
+
content: '';
|
|
383
|
+
position: absolute;
|
|
384
|
+
top: 100%;
|
|
385
|
+
left: 50%;
|
|
386
|
+
transform: translateX(-50%);
|
|
387
|
+
border: 5px solid transparent;
|
|
388
|
+
border-top-color: #2a3038;
|
|
389
|
+
}
|
|
390
|
+
.mcp-item:hover .mcp-tooltip { display: block; }
|
|
391
|
+
.mcp-item.checked {
|
|
392
|
+
border-color: rgba(0, 163, 225, 0.4);
|
|
393
|
+
background: rgba(0, 163, 225, 0.06);
|
|
394
|
+
}
|
|
395
|
+
.mcp-item input[type="checkbox"] { display: none; }
|
|
396
|
+
.mcp-item.required { cursor: default; }
|
|
397
|
+
.mcp-item.required .mcp-check { background: #00A3E1; border-color: #00A3E1; }
|
|
398
|
+
.mcp-item.required .mcp-check-icon { display: block; }
|
|
399
|
+
.mcp-required-label {
|
|
400
|
+
position: absolute;
|
|
401
|
+
top: 6px;
|
|
402
|
+
right: 10px;
|
|
403
|
+
font-size: 9px;
|
|
404
|
+
font-weight: 500;
|
|
405
|
+
color: #8f999f;
|
|
406
|
+
text-transform: uppercase;
|
|
407
|
+
letter-spacing: 0.05em;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.mcp-icon {
|
|
411
|
+
width: 28px;
|
|
412
|
+
height: 28px;
|
|
413
|
+
display: flex;
|
|
414
|
+
align-items: center;
|
|
415
|
+
justify-content: center;
|
|
416
|
+
border-radius: 7px;
|
|
417
|
+
background: rgba(255, 255, 255, 0.04);
|
|
418
|
+
flex-shrink: 0;
|
|
419
|
+
}
|
|
420
|
+
.mcp-icon svg { width: 16px; height: 16px; fill: #8f999f; }
|
|
421
|
+
.mcp-item.checked .mcp-icon svg { fill: #00A3E1; }
|
|
422
|
+
|
|
423
|
+
.mcp-check {
|
|
424
|
+
width: 18px;
|
|
425
|
+
height: 18px;
|
|
426
|
+
border: 1.5px solid rgba(255, 255, 255, 0.2);
|
|
427
|
+
border-radius: 5px;
|
|
428
|
+
display: flex;
|
|
429
|
+
align-items: center;
|
|
430
|
+
justify-content: center;
|
|
431
|
+
flex-shrink: 0;
|
|
432
|
+
transition: all 0.15s;
|
|
433
|
+
}
|
|
434
|
+
.mcp-item.checked .mcp-check,
|
|
435
|
+
.client-item.checked .mcp-check {
|
|
436
|
+
background: #00A3E1;
|
|
437
|
+
border-color: #00A3E1;
|
|
438
|
+
}
|
|
439
|
+
.mcp-check-icon { display: none; color: #fff; font-size: 11px; font-weight: 600; }
|
|
440
|
+
.mcp-item.checked .mcp-check-icon,
|
|
441
|
+
.client-item.checked .mcp-check-icon { display: block; }
|
|
442
|
+
|
|
443
|
+
.mcp-info { flex: 1; min-width: 0; }
|
|
444
|
+
.mcp-name { font-size: 13px; font-weight: 500; color: #fff; }
|
|
445
|
+
.mcp-desc { font-size: 11px; color: #8f999f; display: block; margin-top: 2px; font-weight: 400; }
|
|
446
|
+
.mcp-badge {
|
|
447
|
+
font-size: 10px;
|
|
448
|
+
font-weight: 500;
|
|
449
|
+
padding: 1px 7px;
|
|
450
|
+
border-radius: 6px;
|
|
451
|
+
margin-left: 8px;
|
|
452
|
+
letter-spacing: 0.03em;
|
|
453
|
+
text-transform: uppercase;
|
|
454
|
+
}
|
|
455
|
+
.mcp-badge.company {
|
|
456
|
+
background: rgba(76, 175, 80, 0.12);
|
|
457
|
+
color: #4CAF50;
|
|
458
|
+
border: 1px solid rgba(76, 175, 80, 0.2);
|
|
459
|
+
}
|
|
460
|
+
.mcp-badge.personal {
|
|
461
|
+
background: rgba(255, 183, 77, 0.12);
|
|
462
|
+
color: #FFB74D;
|
|
463
|
+
border: 1px solid rgba(255, 183, 77, 0.2);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/* \u2500\u2500 Client List \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
467
|
+
.client-list {
|
|
468
|
+
display: flex;
|
|
469
|
+
flex-direction: column;
|
|
470
|
+
gap: 8px;
|
|
471
|
+
margin-bottom: 8px;
|
|
472
|
+
}
|
|
473
|
+
.client-item {
|
|
474
|
+
display: flex;
|
|
475
|
+
align-items: center;
|
|
476
|
+
gap: 12px;
|
|
477
|
+
padding: 14px 16px;
|
|
478
|
+
border-radius: 12px;
|
|
479
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
480
|
+
cursor: pointer;
|
|
481
|
+
user-select: none;
|
|
482
|
+
transition: all 0.15s ease;
|
|
483
|
+
}
|
|
484
|
+
.client-item:hover { border-color: rgba(0, 163, 225, 0.3); }
|
|
485
|
+
.client-item.checked {
|
|
486
|
+
border-color: rgba(0, 163, 225, 0.4);
|
|
487
|
+
background: rgba(0, 163, 225, 0.06);
|
|
488
|
+
}
|
|
489
|
+
.client-item.disabled {
|
|
490
|
+
opacity: 0.3;
|
|
491
|
+
cursor: not-allowed;
|
|
492
|
+
}
|
|
493
|
+
.client-item.disabled:hover { border-color: rgba(255, 255, 255, 0.06); }
|
|
494
|
+
|
|
495
|
+
.client-icon {
|
|
496
|
+
width: 28px;
|
|
497
|
+
height: 28px;
|
|
498
|
+
display: flex;
|
|
499
|
+
align-items: center;
|
|
500
|
+
justify-content: center;
|
|
501
|
+
border-radius: 7px;
|
|
502
|
+
background: rgba(255, 255, 255, 0.04);
|
|
503
|
+
flex-shrink: 0;
|
|
504
|
+
}
|
|
505
|
+
.client-icon svg { width: 16px; height: 16px; fill: #8f999f; }
|
|
506
|
+
.client-item.checked .client-icon svg { fill: #00A3E1; }
|
|
507
|
+
|
|
508
|
+
.client-name { font-size: 13px; font-weight: 500; color: #fff; }
|
|
509
|
+
.client-status {
|
|
510
|
+
font-size: 11px;
|
|
511
|
+
font-weight: 500;
|
|
512
|
+
color: #8f999f;
|
|
513
|
+
margin-left: auto;
|
|
514
|
+
text-transform: uppercase;
|
|
515
|
+
letter-spacing: 0.05em;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/* \u2500\u2500 Connection List \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
519
|
+
.conn-list {
|
|
520
|
+
display: flex;
|
|
521
|
+
flex-direction: column;
|
|
522
|
+
gap: 8px;
|
|
523
|
+
margin-bottom: 8px;
|
|
524
|
+
}
|
|
525
|
+
.conn-item {
|
|
526
|
+
display: flex;
|
|
527
|
+
align-items: center;
|
|
528
|
+
gap: 12px;
|
|
529
|
+
padding: 14px 16px;
|
|
530
|
+
border-radius: 12px;
|
|
531
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
532
|
+
transition: all 0.2s ease;
|
|
533
|
+
}
|
|
534
|
+
.conn-item.connected {
|
|
535
|
+
border-left: 3px solid #4CAF50;
|
|
536
|
+
background: rgba(76, 175, 80, 0.04);
|
|
537
|
+
}
|
|
538
|
+
.conn-name { font-size: 13px; font-weight: 500; color: #fff; flex-shrink: 0; }
|
|
539
|
+
.conn-status { font-size: 12px; color: #8f999f; flex: 1; text-align: right; font-weight: 400; }
|
|
540
|
+
.conn-status.connected { color: #4CAF50; }
|
|
541
|
+
|
|
542
|
+
/* \u2500\u2500 Done / Success \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
543
|
+
.success-circle {
|
|
544
|
+
width: 64px;
|
|
545
|
+
height: 64px;
|
|
546
|
+
border-radius: 50%;
|
|
547
|
+
background: rgba(0, 163, 225, 0.1);
|
|
548
|
+
border: 2px solid #00A3E1;
|
|
549
|
+
display: flex;
|
|
550
|
+
align-items: center;
|
|
551
|
+
justify-content: center;
|
|
552
|
+
margin: 0 auto 24px;
|
|
553
|
+
animation: successPop 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
|
554
|
+
}
|
|
555
|
+
.success-check {
|
|
556
|
+
font-size: 28px;
|
|
557
|
+
color: #00A3E1;
|
|
558
|
+
animation: checkDraw 0.3s ease 0.2s both;
|
|
559
|
+
}
|
|
560
|
+
@keyframes successPop {
|
|
561
|
+
from { transform: scale(0.5); opacity: 0; }
|
|
562
|
+
to { transform: scale(1); opacity: 1; }
|
|
563
|
+
}
|
|
564
|
+
@keyframes checkDraw {
|
|
565
|
+
from { opacity: 0; transform: scale(0.5); }
|
|
566
|
+
to { opacity: 1; transform: scale(1); }
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.next-steps {
|
|
570
|
+
text-align: left;
|
|
571
|
+
margin-top: 24px;
|
|
572
|
+
padding: 20px;
|
|
573
|
+
border-radius: 12px;
|
|
574
|
+
background: rgba(255, 255, 255, 0.02);
|
|
575
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
576
|
+
}
|
|
577
|
+
.next-steps h3 {
|
|
578
|
+
font-size: 11px;
|
|
579
|
+
font-weight: 500;
|
|
580
|
+
color: #8f999f;
|
|
581
|
+
margin-bottom: 12px;
|
|
582
|
+
text-transform: uppercase;
|
|
583
|
+
letter-spacing: 0.08em;
|
|
584
|
+
}
|
|
585
|
+
.next-step-item {
|
|
586
|
+
font-size: 13px;
|
|
587
|
+
color: #D9D9D6;
|
|
588
|
+
padding: 4px 0;
|
|
589
|
+
font-weight: 400;
|
|
590
|
+
}
|
|
591
|
+
.next-step-item code {
|
|
592
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
593
|
+
font-size: 11px;
|
|
594
|
+
background: rgba(0, 163, 225, 0.1);
|
|
595
|
+
color: #7dd3fc;
|
|
596
|
+
padding: 2px 6px;
|
|
597
|
+
border-radius: 4px;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/* \u2500\u2500 Summary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
601
|
+
.summary-section { margin-bottom: 24px; }
|
|
602
|
+
.summary-section h3 {
|
|
603
|
+
font-size: 11px;
|
|
604
|
+
font-weight: 500;
|
|
605
|
+
color: #8f999f;
|
|
606
|
+
margin-bottom: 10px;
|
|
607
|
+
text-transform: uppercase;
|
|
608
|
+
letter-spacing: 0.08em;
|
|
609
|
+
}
|
|
610
|
+
.summary-item {
|
|
611
|
+
font-size: 13px;
|
|
612
|
+
color: #D9D9D6;
|
|
613
|
+
padding: 3px 0;
|
|
614
|
+
font-weight: 400;
|
|
615
|
+
}
|
|
616
|
+
.check-icon { color: #4CAF50; margin-right: 8px; }
|
|
617
|
+
|
|
618
|
+
/* \u2500\u2500 Utility \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
619
|
+
.text-center { text-align: center; }
|
|
620
|
+
.mt-4 { margin-top: 16px; }
|
|
621
|
+
.mt-2 { margin-top: 8px; }
|
|
622
|
+
.text-sm { font-size: 12px; font-weight: 400; }
|
|
623
|
+
.text-muted { color: #6b7780; }
|
|
624
|
+
.hidden { display: none !important; }
|
|
625
|
+
|
|
626
|
+
/* \u2500\u2500 Stdio Snippet \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
627
|
+
.stdio-snippet {
|
|
628
|
+
background: rgba(0, 0, 0, 0.3);
|
|
629
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
630
|
+
border-radius: 10px;
|
|
631
|
+
padding: 16px;
|
|
632
|
+
margin-top: 12px;
|
|
633
|
+
position: relative;
|
|
634
|
+
}
|
|
635
|
+
.stdio-snippet pre {
|
|
636
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
637
|
+
font-size: 11px;
|
|
638
|
+
color: #7dd3fc;
|
|
639
|
+
white-space: pre-wrap;
|
|
640
|
+
word-break: break-all;
|
|
641
|
+
margin: 0;
|
|
642
|
+
line-height: 1.5;
|
|
643
|
+
}
|
|
644
|
+
.stdio-snippet .copy-btn {
|
|
645
|
+
position: absolute;
|
|
646
|
+
top: 8px;
|
|
647
|
+
right: 8px;
|
|
648
|
+
padding: 4px 10px;
|
|
649
|
+
font-size: 11px;
|
|
650
|
+
border-radius: 6px;
|
|
651
|
+
background: rgba(0, 163, 225, 0.15);
|
|
652
|
+
color: #00A3E1;
|
|
653
|
+
border: 1px solid rgba(0, 163, 225, 0.3);
|
|
654
|
+
cursor: pointer;
|
|
655
|
+
font-family: 'Montserrat', sans-serif;
|
|
656
|
+
}
|
|
657
|
+
.stdio-snippet .copy-btn:hover {
|
|
658
|
+
background: rgba(0, 163, 225, 0.25);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/* \u2500\u2500 Mobile \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
662
|
+
@media (max-width: 640px) {
|
|
663
|
+
#app { padding: 16px; }
|
|
664
|
+
.card { padding: 24px 20px; border-radius: 16px; }
|
|
665
|
+
.mcp-list { grid-template-columns: 1fr; }
|
|
666
|
+
h1 { font-size: 22px; }
|
|
667
|
+
h2 { font-size: 18px; }
|
|
668
|
+
.wizard-header { flex-direction: column; gap: 12px; align-items: flex-start; }
|
|
669
|
+
.orb-primary { width: 400px; height: 400px; }
|
|
670
|
+
.orb-secondary { width: 300px; height: 300px; }
|
|
671
|
+
}
|
|
672
|
+
</style>
|
|
673
|
+
</head>
|
|
674
|
+
<body>
|
|
675
|
+
<!-- Blue Orb Background -->
|
|
676
|
+
<div class="orb orb-primary" id="orb-primary"></div>
|
|
677
|
+
<div class="orb orb-secondary" id="orb-secondary"></div>
|
|
678
|
+
|
|
679
|
+
<div id="app">
|
|
680
|
+
|
|
681
|
+
<!-- Persistent Header -->
|
|
682
|
+
<header class="wizard-header">
|
|
683
|
+
<div class="wizard-header-left">
|
|
684
|
+
<img class="header-logo" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAA/CAMAAAD5XG5xAAAANlBMVEVMaXHa2tfZ2dfa2tfQ1tbZ2dbZ2dbZ2dba2tfZ2dYAo+IApeQApeQApuYApuXn5+Tf39wArO3XFnpPAAAAD3RSTlMAv3SNEzKm+9xV/Gg11aD4UYCxAAAACXBIWXMAAC4jAAAuIwF4pT92AAAIgElEQVR42u2c6ZLbIAyAw2nYTRby/i9bLjs+0GHXyXam1o9OJ2ts0IcQCMHtdskll1xyySWXXHLJJf+pGO+llN4bvauYNl6qJHJvwTPknuTjH20tlu9ssfbWDTFkiYMT0nDLyalcCKyCRiTBn/LpCctq6vfz6+uLRSS/U+DvlPmzdLdVYmpwURX8UisIUbBaXX77KOl7g/Acm7LDvByroMktcahqZIghcoDoRwLy/OEAye8MAn3EpkccyTVVbdViC3UvF0ZyXYlgfYyrWo1F6pdS7Skk2saxXHl/LRki3v9NeUjgykuPaK6B8EzE59oFhT2iwhBxIL4pahpL8Ba7GbmeRAHVNZZvDM4J4fII1NSMDxt+CLVuMRWzVkwFQ1QUEFw1CUhkAXkUICwT8aWywaMWggNJHbAqKo9UdtHiQUJAUBtxMI8Y0/BfdKC1Vy7W6juPGnjtHXL0bNpMBZHR2rTe4f8eSDOQr4fmAhkGcxhIGkcKDme9njxvanEEW5yARCdhUR4eQlaqN2NngNSmRahGZDZjbOHkDAUEUQ0XyKMBeX6zgUTEe+FA/FAbtnbiXgSoxQlIsLe9kjXb4WuA3+c8Qm/s9LUjgfo2FQemGiaQe/bo34nKk2EiGUjuK4j3QoG0cURpuMX+FCDZ2cWu3mWEJ0O2VEEBQ23AuqJpBoKohgnkJ6Mo4xbDRKqFRMx7YUBM4QHYfW3xtj2HgJR3GWBOYeD5SKqevGF/hvRdgEiHqYYH5F4HK11MhAMkujLsBLkfiC71RWw69v56BEj+EthVNTaFhHmMRBQIJBoTEcfOA5IMpLhznokUC9Hl32h2A8kjNOZ+kko6fz0CJKsnyP0QiSm9hRtuKom81oAcDQvIffTmOv/nwQJiSleBNAsD8QGfheRHzO0cIAGfgULrNmLRW5kJBAimGhaQnylqkv/3vPOAtDnMPiClNXu1dBCI3w+kVG8gYlbwkDQCQVTDAXJ/rQjvnMXhCAQxbxBI6YH756/HLCTuHrJK9RRnstCtzQQEVg0HyM8srPjDCDGOQNL3oSZDQFg98DQgw274ef1Ju9w6u9UIENiKGED03Co4JjIBaVMSwwfiDxrIsWnvbvrFpixvPt3riC8goGNnAFnG3Rkm8gICei8ISG5JNJ8CUmon9M4Cnrfi7LmIGRBINTQQ/VjYxJ2Ows+AQN4LAJJHrChunwJSxo0g+B1ARHxGvmhH58E5EEA1NJDv1cTqQZrIHAjgvQAgPnJ85mlA2qp7UEwj0QP3K0AMYAGkrxoayHp1/k2ayBxIC6hKHhAZDs15DwMpvTSH9F9R5XOmZWU25nEgfcdOAtkuzh9PIgq/ANJ37AAQJLj0HiA1cls2mqwkd+19ILZ5lqYuCSB1d3DVXhLINnxFxk+WQLreCwDCHaNPA1LiutMGsRMKtRTJnnKY/ti7AtJTDQXkvtV+9vKoiayA9LwXAKT4wttBIEPahgVEEMkKcdq1jwOSPKL4QAYWkI5qKCCP53OzBfKdfsNMZA2k470QIOI4kAgIkU9Rdl+nPXkkeUSxR1RgzbkBslUNBeQ7yXpKpXs/IkA6jv09QMAMB8dMOCJSKc63kK1jZyc57AzaLTvSxrG/BUgEc4AcK10u572FMXlE8udOfwNks2L/DJCN93qHD4nCAyLZM2kjRfMoveWQPHmW1VPNh4Csvde/M8vqZjICiWX8dSuwoOoBWanmU0BW3utfWYdgWS9bb2HIvMNFQzQPyFI1nwKyCsUDQBR7THgrEHDTr0wduLEsIri4Dql5Goj2uJg9QJaOHYtl2d8HUrvPtg2W2WGgsa0PZLFix4CkpGlczB4gC+8FRXuHo07kZCDVf8vupq/gjVg97QBA5qpBgGgqgRnSAABk7r2g/RBxdMw6G0h/3lp3/A0nKtxtHgRkphoEiAzI4jci2bsQkJn3goBIZhd8OxDdf6EKHLeugA1DGMhLNTAQjYeHkLx6CMhsxY7uqR8JwJ8NBHihoZLXJ7V3B14QyMuxw0B8xawhceBwDwJ5OXYw60RRaXK/ayHVOxD1E5CBIEAmxw4DoRZpEvwsDGTSN5yXNRzbNDy2p+53ZzVqPFX61cbbTiBjMRCIp3bH4BkRAmT0XnDmYvVcHj+geE4qaVKB37nUbvXDPiWRFFoMSFWNBYEIcsUIui4MSPNeCs/txc8Yxc7RjQNAis7lviyFcREewBNvMiIZwyiQ6j/d0Nc7I2xjoMA2BqQuucpnHfYA3Hf7XmY/EBOQnBMLZygi53Wm0xL2dgBIC8UDQDhRJWjdigJpjh3ZpKgPAH3XNHX8PZDxnFYn56SoFQ5gtENDEjpPBFcEB9Ice/fLrCxLE/uLBhxI3XfDdo1kOzxpOpHYqkR7yvmQepIxnTZRpqdWj2dGxM2Ru/GMob0dBNJU0wPCyyAUfc9HAGm5N8jGh6wNW5+qzIHxogl10ixLq/FQdMpuMGUyb9LZ0kAd69b1FG45lNqO75rx+G7EzkVTQJoD3QJhxpmBjEkKSF1yYjtR7Rj47NxxarFtmuqeiy5O2GKisdPwZU+9rHfr7nqMePqVHM+prw+4o6epaSDVsW+BcHNYRTe0QwEZHTueLzUpKjU4HVNvm6sR8KbkOfUAVChfrDFuvY8RoYirtRrJmKsyiyTFQCRBkkCAxHntmBGlEv20Xa9syOOvzJscli2GNEUFQuEKaS+G8LrDI+akOckIFdRclfklKdFROanlrhNPTMbD9q4TRZebJhydvieRDjn7gqOSDsSwuhUmwtfPuECJQa8DEg18GriU56b6lluEYrv5QzDKmTx2Eq5ZdoZXlX7jJSD7fO2F732WKJ4+oTgpB9x7k5SlRFP3QJmy6bYzjJavj5LyV67L+h3RB28Wu+SSSy655JJLLrnkkv9a/gAogKu+LRoCQQAAAABJRU5ErkJggg==" alt="Sonance">
|
|
685
|
+
<span class="header-divider"></span>
|
|
686
|
+
<span class="header-product">Cortex</span>
|
|
687
|
+
</div>
|
|
688
|
+
<div class="header-steps" id="header-steps"></div>
|
|
689
|
+
</header>
|
|
690
|
+
|
|
691
|
+
<!-- Step 1: Welcome -->
|
|
692
|
+
<div id="step-welcome" class="step active slide-forward">
|
|
693
|
+
<div class="card text-center">
|
|
694
|
+
<div class="welcome-hero">
|
|
695
|
+
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" style="margin-bottom: 20px;">
|
|
696
|
+
<circle cx="24" cy="24" r="23" stroke="#00A3E1" stroke-width="1.5" opacity="0.3"/>
|
|
697
|
+
<circle cx="24" cy="24" r="16" stroke="#00A3E1" stroke-width="1" opacity="0.15"/>
|
|
698
|
+
<circle cx="24" cy="24" r="6" fill="#00A3E1" opacity="0.8"/>
|
|
699
|
+
</svg>
|
|
700
|
+
<h1>Connect Your AI Tools</h1>
|
|
701
|
+
<p class="welcome-tagline">One setup. 10+ platforms. All your AI clients.</p>
|
|
702
|
+
<p class="subtitle" style="margin-bottom: 0;">
|
|
703
|
+
GitHub, Salesforce, M365, Slack, Asana, and more — configured in under 2 minutes.
|
|
704
|
+
</p>
|
|
705
|
+
</div>
|
|
706
|
+
<button class="btn btn-accent btn-block" onclick="goToStep('signin')">Get Started</button>
|
|
707
|
+
<p class="welcome-footer">Powered by Sonance</p>
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
|
|
711
|
+
<!-- Step 2: Sign In -->
|
|
712
|
+
<div id="step-signin" class="step">
|
|
713
|
+
<div class="card">
|
|
714
|
+
<h2>Sign In</h2>
|
|
715
|
+
<p class="subtitle">Authenticate with your company SSO</p>
|
|
716
|
+
<div id="signin-content"></div>
|
|
717
|
+
</div>
|
|
718
|
+
</div>
|
|
719
|
+
|
|
720
|
+
<!-- Step 3: Select MCPs -->
|
|
721
|
+
<div id="step-mcps" class="step">
|
|
722
|
+
<div class="card">
|
|
723
|
+
<h2>Select Integrations</h2>
|
|
724
|
+
<p class="subtitle">Choose which services to connect. You can enable more later.</p>
|
|
725
|
+
<div id="mcp-list-container"></div>
|
|
726
|
+
<div class="btn-row">
|
|
727
|
+
<button class="btn btn-secondary" onclick="goToStep('signin')">Back</button>
|
|
728
|
+
<button class="btn btn-primary" onclick="saveMcps()">Continue</button>
|
|
729
|
+
</div>
|
|
730
|
+
</div>
|
|
731
|
+
</div>
|
|
732
|
+
|
|
733
|
+
<!-- Step 4: Configure Clients -->
|
|
734
|
+
<div id="step-clients" class="step">
|
|
735
|
+
<div class="card">
|
|
736
|
+
<h2>Configure AI Clients</h2>
|
|
737
|
+
<p class="subtitle">Select which AI tools to set up with Cortex</p>
|
|
738
|
+
<div id="client-list" class="client-list"></div>
|
|
739
|
+
<div id="client-result" class="hidden"></div>
|
|
740
|
+
<div class="btn-row">
|
|
741
|
+
<button class="btn btn-secondary" onclick="goToStep('mcps')">Back</button>
|
|
742
|
+
<button id="btn-configure" class="btn btn-primary" onclick="configureClients()">Configure</button>
|
|
743
|
+
</div>
|
|
744
|
+
</div>
|
|
745
|
+
</div>
|
|
746
|
+
|
|
747
|
+
<!-- Step 5: Connect Accounts -->
|
|
748
|
+
<div id="step-connect" class="step">
|
|
749
|
+
<div class="card">
|
|
750
|
+
<h2>Connect Accounts</h2>
|
|
751
|
+
<p class="subtitle">Link personal accounts for services that require them (optional)</p>
|
|
752
|
+
<div id="conn-list" class="conn-list"></div>
|
|
753
|
+
<div class="btn-row">
|
|
754
|
+
<button class="btn btn-secondary" onclick="goToStep('clients')">Back</button>
|
|
755
|
+
<button class="btn btn-primary" onclick="goToStep('done')">Continue</button>
|
|
756
|
+
</div>
|
|
757
|
+
</div>
|
|
758
|
+
</div>
|
|
759
|
+
|
|
760
|
+
<!-- Step 6: Done -->
|
|
761
|
+
<div id="step-done" class="step">
|
|
762
|
+
<div class="card text-center">
|
|
763
|
+
<div class="success-circle"><span class="success-check">✓</span></div>
|
|
764
|
+
<h2>Setup Complete</h2>
|
|
765
|
+
<p class="subtitle">Your AI clients are now connected to Cortex</p>
|
|
766
|
+
<div id="summary" style="text-align: left; margin: 24px 0;"></div>
|
|
767
|
+
<div class="next-steps">
|
|
768
|
+
<h3>What's Next</h3>
|
|
769
|
+
<div class="next-step-item">1. Restart your AI clients to load the new tools</div>
|
|
770
|
+
<div class="next-step-item">2. Try: “List my GitHub repos” or “Show my Asana tasks”</div>
|
|
771
|
+
<div class="next-step-item">3. Run <code>cortex-mcp connect <provider></code> to add more accounts later</div>
|
|
772
|
+
</div>
|
|
773
|
+
<div style="margin-top: 24px;">
|
|
774
|
+
<button class="btn btn-accent btn-block" onclick="finishSetup()">Close</button>
|
|
775
|
+
</div>
|
|
776
|
+
<p class="text-sm text-muted mt-4">Config saved to ~/.cortex-mcp/config.json</p>
|
|
777
|
+
</div>
|
|
778
|
+
</div>
|
|
779
|
+
|
|
780
|
+
</div>
|
|
781
|
+
|
|
782
|
+
<script>
|
|
783
|
+
// \u2500\u2500 State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
784
|
+
var state = {
|
|
785
|
+
credentials: null,
|
|
786
|
+
availableMcps: [],
|
|
787
|
+
selectedMcps: [],
|
|
788
|
+
detectedClients: [],
|
|
789
|
+
selectedClients: [],
|
|
790
|
+
configuredClients: [],
|
|
791
|
+
connections: [],
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
// \u2500\u2500 Step Index Map \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
795
|
+
var STEP_ORDER = ['welcome', 'signin', 'mcps', 'clients', 'connect', 'done'];
|
|
796
|
+
var currentStepIndex = 0;
|
|
797
|
+
|
|
798
|
+
// \u2500\u2500 MCP Icons (inline SVG) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
799
|
+
var MCP_ICONS = {
|
|
800
|
+
github: '<svg viewBox="0 0 16 16"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>',
|
|
801
|
+
supabase: '<svg viewBox="0 0 16 16"><path d="M9.3 14.7c-.2.3-.7.1-.7-.3V9.3h4.6c.5 0 .8-.6.5-1L8.7 1.3c-.2-.3-.7-.1-.7.3v5.1H3.4c-.5 0-.8.6-.5 1l5 7z"/></svg>',
|
|
802
|
+
vercel: '<svg viewBox="0 0 16 16"><path d="M8 1l7 13H1z"/></svg>',
|
|
803
|
+
m365: '<svg viewBox="0 0 16 16"><rect x="1" y="1" width="6" height="6" rx="1"/><rect x="9" y="1" width="6" height="6" rx="1"/><rect x="1" y="9" width="6" height="6" rx="1"/><rect x="9" y="9" width="6" height="6" rx="1"/></svg>',
|
|
804
|
+
salesforce: '<svg viewBox="0 0 16 16"><path d="M6.7 3.3a3 3 0 014.8.5 2.5 2.5 0 013 2.4 2.5 2.5 0 01-1.5 2.3 2.8 2.8 0 01-3.5 2.2A3 3 0 016 12.5a3 3 0 01-3.3-1.8A2.5 2.5 0 011 8.5a2.5 2.5 0 011.8-2.4 2.8 2.8 0 013.9-2.8z"/></svg>',
|
|
805
|
+
slack: '<svg viewBox="0 0 16 16"><path d="M3.5 10a1.5 1.5 0 11-3 0 1.5 1.5 0 011.5-1.5H3.5V10zm.75 0a1.5 1.5 0 113 0v3.5a1.5 1.5 0 11-3 0V10zM6 3.5a1.5 1.5 0 110-3 1.5 1.5 0 011.5 1.5V3.5H6zm0 .75a1.5 1.5 0 110 3H2.5a1.5 1.5 0 110-3H6zM12.5 6a1.5 1.5 0 113 0 1.5 1.5 0 01-1.5 1.5H12.5V6zm-.75 0a1.5 1.5 0 11-3 0V2.5a1.5 1.5 0 113 0V6zM10 12.5a1.5 1.5 0 110 3 1.5 1.5 0 01-1.5-1.5v-1.5H10zm0-.75a1.5 1.5 0 110-3h3.5a1.5 1.5 0 110 3H10z"/></svg>',
|
|
806
|
+
asana: '<svg viewBox="0 0 16 16"><circle cx="8" cy="4" r="2.5"/><circle cx="3.5" cy="11" r="2.5"/><circle cx="12.5" cy="11" r="2.5"/></svg>',
|
|
807
|
+
monday: '<svg viewBox="0 0 16 16"><circle cx="4" cy="10" r="2" /><circle cx="8" cy="6" r="2" /><circle cx="12" cy="10" r="2" /></svg>',
|
|
808
|
+
powerbi: '<svg viewBox="0 0 16 16"><rect x="2" y="8" width="3" height="6" rx="1"/><rect x="6.5" y="5" width="3" height="9" rx="1"/><rect x="11" y="2" width="3" height="12" rx="1"/></svg>',
|
|
809
|
+
bestbuy: '<svg viewBox="0 0 16 16"><path d="M2 3h12a1 1 0 011 1v8a1 1 0 01-1 1H2a1 1 0 01-1-1V4a1 1 0 011-1zm3 3v4h2V6H5zm4 0v4h2V6H9z"/></svg>',
|
|
810
|
+
mailchimp: '<svg viewBox="0 0 16 16"><path d="M8 2a6 6 0 100 12A6 6 0 008 2zM6.5 10.5a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm3 0a1.5 1.5 0 110-3 1.5 1.5 0 010 3z"/></svg>',
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
// \u2500\u2500 Client Icons (inline SVG) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
814
|
+
var CLIENT_ICONS = {
|
|
815
|
+
'claude-desktop': '<svg viewBox="0 0 16 16"><path d="M10.5 2H5.5a3 3 0 00-3 3v6a3 3 0 003 3h5a3 3 0 003-3V5a3 3 0 00-3-3zM8 11.5a.75.75 0 110-1.5.75.75 0 010 1.5zm2-3.5H6a.5.5 0 010-1h4a.5.5 0 010 1z"/></svg>',
|
|
816
|
+
'claude-code': '<svg viewBox="0 0 16 16"><path d="M5.7 11.3a.5.5 0 01-.7-.7L7.3 8 5 5.7a.5.5 0 01.7-.7l3 3a.5.5 0 010 .7l-3 3zM9 11h3a.5.5 0 010 1H9a.5.5 0 010-1z"/><rect x="1" y="2" width="14" height="12" rx="2" fill="none" stroke="currentColor" stroke-width="1"/></svg>',
|
|
817
|
+
cursor: '<svg viewBox="0 0 16 16"><path d="M3 2l10 6-4 1.5L7.5 14z"/></svg>',
|
|
818
|
+
vscode: '<svg viewBox="0 0 16 16"><path d="M11.5 1L5 7l-3-2.5L1 5l3.5 3L1 11l1 .5L5 9l6.5 6 2.5-1V2z"/></svg>',
|
|
819
|
+
antigravity: '<svg viewBox="0 0 16 16"><path d="M8 1l2 5h5l-4 3 1.5 5L8 11l-4.5 3L5 9 1 6h5z"/></svg>',
|
|
820
|
+
codex: '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="3" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M8 1v3M8 12v3M1 8h3M12 8h3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>',
|
|
821
|
+
stdio: '<svg viewBox="0 0 16 16"><path d="M2 3h12a1 1 0 011 1v8a1 1 0 01-1 1H2a1 1 0 01-1-1V4a1 1 0 011-1zm1 2v6h10V5H3z"/></svg>',
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
// \u2500\u2500 Init \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
825
|
+
async function init() {
|
|
826
|
+
try {
|
|
827
|
+
var resp = await fetch('/api/state');
|
|
828
|
+
var data = await resp.json();
|
|
829
|
+
state.credentials = data.credentials;
|
|
830
|
+
state.availableMcps = data.availableMcps || [];
|
|
831
|
+
state.selectedMcps = ['github', 'supabase', 'vercel'];
|
|
832
|
+
state.detectedClients = data.detectedClients || [];
|
|
833
|
+
} catch (err) {
|
|
834
|
+
console.error('Failed to load state:', err);
|
|
835
|
+
}
|
|
836
|
+
updateHeaderDots();
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
init();
|
|
840
|
+
|
|
841
|
+
// \u2500\u2500 Header Dots \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
842
|
+
function updateHeaderDots() {
|
|
843
|
+
var el = document.getElementById('header-steps');
|
|
844
|
+
var html = '';
|
|
845
|
+
for (var i = 0; i < STEP_ORDER.length; i++) {
|
|
846
|
+
var cls = 'header-dot';
|
|
847
|
+
if (i < currentStepIndex) cls += ' done';
|
|
848
|
+
else if (i === currentStepIndex) cls += ' current';
|
|
849
|
+
html += '<span class="' + cls + '"></span>';
|
|
850
|
+
}
|
|
851
|
+
el.innerHTML = html;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// \u2500\u2500 Navigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
855
|
+
function goToStep(step) {
|
|
856
|
+
var newIndex = STEP_ORDER.indexOf(step);
|
|
857
|
+
var direction = newIndex >= currentStepIndex ? 'slide-forward' : 'slide-backward';
|
|
858
|
+
|
|
859
|
+
var active = document.querySelector('.step.active');
|
|
860
|
+
if (active) {
|
|
861
|
+
active.classList.remove('active', 'slide-forward', 'slide-backward');
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
var nextEl = document.getElementById('step-' + step);
|
|
865
|
+
nextEl.classList.add('active', direction);
|
|
866
|
+
currentStepIndex = newIndex;
|
|
867
|
+
updateHeaderDots();
|
|
868
|
+
|
|
869
|
+
if (step === 'signin') renderSignin();
|
|
870
|
+
if (step === 'mcps') renderMcps();
|
|
871
|
+
if (step === 'clients') renderClients();
|
|
872
|
+
if (step === 'connect') renderConnect();
|
|
873
|
+
if (step === 'done') renderDone();
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// \u2500\u2500 Blue Orb Mouse Follow \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
877
|
+
(function() {
|
|
878
|
+
var primary = document.getElementById('orb-primary');
|
|
879
|
+
var secondary = document.getElementById('orb-secondary');
|
|
880
|
+
document.addEventListener('mousemove', function(e) {
|
|
881
|
+
var dx = (e.clientX - window.innerWidth / 2) * 0.15;
|
|
882
|
+
var dy = (e.clientY - window.innerHeight / 2) * 0.15;
|
|
883
|
+
primary.style.transform = 'translate(calc(-50% + ' + dx + 'px), calc(-50% + ' + dy + 'px))';
|
|
884
|
+
secondary.style.transform = 'translate(calc(-50% + ' + (-dx * 0.6) + 'px), calc(-50% + ' + (-dy * 0.6) + 'px))';
|
|
885
|
+
});
|
|
886
|
+
})();
|
|
887
|
+
|
|
888
|
+
// \u2500\u2500 Step 2: Sign In \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
889
|
+
function renderSignin() {
|
|
890
|
+
var el = document.getElementById('signin-content');
|
|
891
|
+
|
|
892
|
+
if (state.credentials) {
|
|
893
|
+
var name = state.credentials.name || state.credentials.email;
|
|
894
|
+
el.innerHTML =
|
|
895
|
+
'<div class="status status-success">' +
|
|
896
|
+
'<span>✓</span>' +
|
|
897
|
+
'<span>Signed in as <strong>' + escapeHtml(name) + '</strong></span>' +
|
|
898
|
+
'</div>' +
|
|
899
|
+
'<div class="btn-row">' +
|
|
900
|
+
'<button class="btn btn-primary btn-block" onclick="goToStep(\\'mcps\\')">Continue</button>' +
|
|
901
|
+
'</div>';
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
el.innerHTML =
|
|
906
|
+
'<button id="btn-signin" class="btn btn-accent btn-block" onclick="startLogin()">Sign in with SSO</button>' +
|
|
907
|
+
'<div id="signin-status" class="hidden"></div>';
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
async function startLogin() {
|
|
911
|
+
var btn = document.getElementById('btn-signin');
|
|
912
|
+
btn.disabled = true;
|
|
913
|
+
btn.innerHTML = '<span class="spinner"></span> Starting...';
|
|
914
|
+
|
|
915
|
+
var statusEl = document.getElementById('signin-status');
|
|
916
|
+
|
|
917
|
+
try {
|
|
918
|
+
var resp = await fetch('/api/auth/login', { method: 'POST' });
|
|
919
|
+
var data = await resp.json();
|
|
920
|
+
|
|
921
|
+
if (data.error) {
|
|
922
|
+
statusEl.className = 'status status-error';
|
|
923
|
+
statusEl.textContent = data.error;
|
|
924
|
+
btn.disabled = false;
|
|
925
|
+
btn.textContent = 'Sign in with SSO';
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
window.open(data.auth_url, '_blank');
|
|
930
|
+
|
|
931
|
+
// Show auth phase indicators
|
|
932
|
+
statusEl.className = '';
|
|
933
|
+
statusEl.innerHTML =
|
|
934
|
+
'<div class="auth-phases">' +
|
|
935
|
+
'<div class="auth-phase done" id="phase-browser"><span class="phase-icon">✓</span> Browser opened</div>' +
|
|
936
|
+
'<div class="auth-phase active" id="phase-sso"><span class="spinner"></span> Waiting for SSO...</div>' +
|
|
937
|
+
'<div class="auth-phase pending" id="phase-verify"><span class="phase-icon">•</span> Verifying identity</div>' +
|
|
938
|
+
'</div>';
|
|
939
|
+
|
|
940
|
+
btn.className = 'btn btn-secondary btn-block';
|
|
941
|
+
btn.disabled = false;
|
|
942
|
+
btn.textContent = 'Open SSO page again';
|
|
943
|
+
btn.onclick = function() { window.open(data.auth_url, '_blank'); };
|
|
944
|
+
|
|
945
|
+
var result = await pollUntilDone(
|
|
946
|
+
'/api/auth/poll?session=' + encodeURIComponent(data.session_id),
|
|
947
|
+
3000,
|
|
948
|
+
data.expires_in * 1000
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
// Update phases to show completion
|
|
952
|
+
var ssoPhase = document.getElementById('phase-sso');
|
|
953
|
+
if (ssoPhase) {
|
|
954
|
+
ssoPhase.className = 'auth-phase done';
|
|
955
|
+
ssoPhase.innerHTML = '<span class="phase-icon">✓</span> SSO completed';
|
|
956
|
+
}
|
|
957
|
+
var verifyPhase = document.getElementById('phase-verify');
|
|
958
|
+
if (verifyPhase) {
|
|
959
|
+
verifyPhase.className = 'auth-phase done';
|
|
960
|
+
verifyPhase.innerHTML = '<span class="phase-icon">✓</span> Identity verified';
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
state.credentials = {
|
|
964
|
+
email: result.employee_email || result.email || '',
|
|
965
|
+
name: result.employee_name || result.name || '',
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
// Brief pause so user sees all green checks
|
|
969
|
+
await new Promise(function(r) { setTimeout(r, 600); });
|
|
970
|
+
renderSignin();
|
|
971
|
+
} catch (err) {
|
|
972
|
+
statusEl.className = 'status status-error';
|
|
973
|
+
statusEl.textContent = err.message === 'Timeout'
|
|
974
|
+
? 'Authentication timed out. Please try again.'
|
|
975
|
+
: 'Authentication failed: ' + err.message;
|
|
976
|
+
btn.disabled = false;
|
|
977
|
+
btn.textContent = 'Try Again';
|
|
978
|
+
btn.onclick = startLogin;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// \u2500\u2500 Step 3: Select MCPs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
983
|
+
var REQUIRED_MCPS = ['github', 'supabase', 'vercel'];
|
|
984
|
+
|
|
985
|
+
var MCP_TOOLTIPS = {
|
|
986
|
+
asana: 'Manage projects, tasks, and team workflows',
|
|
987
|
+
github: 'Cloud storage and version control for any projects with code',
|
|
988
|
+
vercel: 'Deploy and host websites and web apps',
|
|
989
|
+
supabase: 'Your project database \\u2014 store and query data',
|
|
990
|
+
m365: 'Access your Outlook email, calendar, OneDrive files, and Teams',
|
|
991
|
+
mailchimp: 'Create and manage email marketing campaigns and audiences',
|
|
992
|
+
salesforce: 'Access your CRM \\u2014 contacts, leads, opportunities, and reports',
|
|
993
|
+
monday: 'Track work with boards, timelines, and team updates',
|
|
994
|
+
slack: 'Send messages, search conversations, and manage channels',
|
|
995
|
+
powerbi: 'Build dashboards and run data queries on your reports',
|
|
996
|
+
bestbuy: 'Search products, compare prices, and find store locations',
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
function renderMcps() {
|
|
1000
|
+
var container = document.getElementById('mcp-list-container');
|
|
1001
|
+
|
|
1002
|
+
var requiredMcps = state.availableMcps.filter(function(m) { return REQUIRED_MCPS.indexOf(m.name) !== -1; });
|
|
1003
|
+
var optionalMcps = state.availableMcps.filter(function(m) { return REQUIRED_MCPS.indexOf(m.name) === -1; });
|
|
1004
|
+
|
|
1005
|
+
var html = '';
|
|
1006
|
+
|
|
1007
|
+
// Core section
|
|
1008
|
+
html += '<div class="mcp-section">';
|
|
1009
|
+
html += '<div class="mcp-section-title">Core (always enabled)</div>';
|
|
1010
|
+
html += '<div class="mcp-list">';
|
|
1011
|
+
html += requiredMcps.map(function(mcp) { return renderMcpItem(mcp, true); }).join('');
|
|
1012
|
+
html += '</div></div>';
|
|
1013
|
+
|
|
1014
|
+
// Optional section
|
|
1015
|
+
if (optionalMcps.length > 0) {
|
|
1016
|
+
html += '<div class="mcp-section">';
|
|
1017
|
+
html += '<div class="mcp-section-title">Optional integrations</div>';
|
|
1018
|
+
html += '<div class="mcp-list">';
|
|
1019
|
+
html += optionalMcps.map(function(mcp) { return renderMcpItem(mcp, false); }).join('');
|
|
1020
|
+
html += '</div></div>';
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
container.innerHTML = html;
|
|
1024
|
+
|
|
1025
|
+
// Attach click handlers to optional items
|
|
1026
|
+
container.querySelectorAll('.mcp-item:not(.required)').forEach(function(item) {
|
|
1027
|
+
item.addEventListener('click', function(e) {
|
|
1028
|
+
e.preventDefault();
|
|
1029
|
+
var cb = item.querySelector('input[type="checkbox"]');
|
|
1030
|
+
cb.checked = !cb.checked;
|
|
1031
|
+
item.classList.toggle('checked', cb.checked);
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function renderMcpItem(mcp, isRequired) {
|
|
1037
|
+
var checked = isRequired || state.selectedMcps.indexOf(mcp.name) !== -1;
|
|
1038
|
+
var icon = MCP_ICONS[mcp.name] || '';
|
|
1039
|
+
return (
|
|
1040
|
+
'<label class="mcp-item' + (checked ? ' checked' : '') + (isRequired ? ' required' : '') + '" data-mcp="' + mcp.name + '">' +
|
|
1041
|
+
(MCP_TOOLTIPS[mcp.name] ? '<span class="mcp-tooltip">' + MCP_TOOLTIPS[mcp.name] + '</span>' : '') +
|
|
1042
|
+
'<input type="checkbox" value="' + mcp.name + '"' + (checked ? ' checked' : '') + '>' +
|
|
1043
|
+
(icon ? '<div class="mcp-icon">' + icon + '</div>' : '') +
|
|
1044
|
+
'<div class="mcp-check"><span class="mcp-check-icon">✓</span></div>' +
|
|
1045
|
+
'<div class="mcp-info">' +
|
|
1046
|
+
'<span class="mcp-name">' + escapeHtml(mcp.displayName) +
|
|
1047
|
+
'<span class="mcp-badge ' + mcp.authMode + '">' + mcp.authMode + '</span>' +
|
|
1048
|
+
'</span>' +
|
|
1049
|
+
'<span class="mcp-desc">' + escapeHtml(mcp.description) + '</span>' +
|
|
1050
|
+
'</div>' +
|
|
1051
|
+
(isRequired ? '<span class="mcp-required-label">Required</span>' : '') +
|
|
1052
|
+
'</label>'
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function saveMcps() {
|
|
1057
|
+
var checkboxes = document.querySelectorAll('.mcp-item input[type="checkbox"]');
|
|
1058
|
+
var selected = Array.from(checkboxes).filter(function(cb) { return cb.checked; }).map(function(cb) { return cb.value; });
|
|
1059
|
+
REQUIRED_MCPS.forEach(function(name) { if (selected.indexOf(name) === -1) selected.push(name); });
|
|
1060
|
+
state.selectedMcps = selected;
|
|
1061
|
+
goToStep('clients');
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// \u2500\u2500 Step 4: Configure Clients \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1065
|
+
function renderClients() {
|
|
1066
|
+
var el = document.getElementById('client-list');
|
|
1067
|
+
document.getElementById('client-result').className = 'hidden';
|
|
1068
|
+
document.getElementById('btn-configure').disabled = false;
|
|
1069
|
+
document.getElementById('btn-configure').textContent = 'Configure';
|
|
1070
|
+
|
|
1071
|
+
state.selectedClients = [];
|
|
1072
|
+
|
|
1073
|
+
el.innerHTML = state.detectedClients.map(function(client) {
|
|
1074
|
+
var detected = client.detected;
|
|
1075
|
+
var icon = CLIENT_ICONS[client.type] || '';
|
|
1076
|
+
return (
|
|
1077
|
+
'<label class="client-item' + (detected ? '' : ' disabled') + '" data-client="' + client.type + '">' +
|
|
1078
|
+
(icon ? '<div class="client-icon">' + icon + '</div>' : '') +
|
|
1079
|
+
'<div class="mcp-check"><span class="mcp-check-icon">✓</span></div>' +
|
|
1080
|
+
'<span class="client-name">' + escapeHtml(client.name) + '</span>' +
|
|
1081
|
+
'<span class="client-status">' + (detected ? 'Detected' : 'Not found') + '</span>' +
|
|
1082
|
+
'</label>'
|
|
1083
|
+
);
|
|
1084
|
+
}).join('');
|
|
1085
|
+
|
|
1086
|
+
el.querySelectorAll('.client-item:not(.disabled)').forEach(function(item) {
|
|
1087
|
+
item.addEventListener('click', function(e) {
|
|
1088
|
+
e.preventDefault();
|
|
1089
|
+
item.classList.toggle('checked');
|
|
1090
|
+
state.selectedClients = Array.from(el.querySelectorAll('.client-item.checked'))
|
|
1091
|
+
.map(function(i) { return i.dataset.client; });
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
async function configureClients() {
|
|
1097
|
+
if (state.selectedClients.length === 0) {
|
|
1098
|
+
goToStep('connect');
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
var btn = document.getElementById('btn-configure');
|
|
1103
|
+
btn.disabled = true;
|
|
1104
|
+
btn.innerHTML = '<span class="spinner"></span> Configuring...';
|
|
1105
|
+
|
|
1106
|
+
// Show per-client spinners
|
|
1107
|
+
state.selectedClients.forEach(function(clientType) {
|
|
1108
|
+
var item = document.querySelector('[data-client="' + clientType + '"]');
|
|
1109
|
+
if (item) {
|
|
1110
|
+
var statusSpan = item.querySelector('.client-status');
|
|
1111
|
+
statusSpan.innerHTML = '<span class="spinner"></span>';
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
try {
|
|
1116
|
+
var resp = await fetch('/api/clients/configure', {
|
|
1117
|
+
method: 'POST',
|
|
1118
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1119
|
+
body: JSON.stringify({ clients: state.selectedClients, mcps: state.selectedMcps }),
|
|
1120
|
+
});
|
|
1121
|
+
var data = await resp.json();
|
|
1122
|
+
|
|
1123
|
+
state.configuredClients = data.results.filter(function(r) { return r.success; }).map(function(r) { return r.client; });
|
|
1124
|
+
|
|
1125
|
+
// Update per-client status inline
|
|
1126
|
+
data.results.forEach(function(r) {
|
|
1127
|
+
var item = document.querySelector('[data-client="' + r.client + '"]');
|
|
1128
|
+
if (item) {
|
|
1129
|
+
var statusSpan = item.querySelector('.client-status');
|
|
1130
|
+
if (r.success) {
|
|
1131
|
+
statusSpan.innerHTML = '<span style="color: #4CAF50;">✓ Done</span>';
|
|
1132
|
+
} else {
|
|
1133
|
+
statusSpan.innerHTML = '<span style="color: #EF5350;">✗ Failed</span>';
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
var resultEl = document.getElementById('client-result');
|
|
1139
|
+
resultEl.className = '';
|
|
1140
|
+
resultEl.innerHTML = data.results.map(function(r) {
|
|
1141
|
+
var html = '<div class="status ' + (r.success ? 'status-success' : 'status-error') + '">' +
|
|
1142
|
+
'<span>' + (r.success ? '✓' : '✗') + '</span>' +
|
|
1143
|
+
'<span>' + escapeHtml(r.client) + ': ' + (r.success ? 'Configured' : r.error || 'Failed') + '</span>' +
|
|
1144
|
+
'</div>';
|
|
1145
|
+
// Show copyable snippet for stdio/OpenClaw
|
|
1146
|
+
if (r.client === 'stdio' && r.success && r.message) {
|
|
1147
|
+
var snippet = r.message.replace(/^Add this to your client config:\\n\\n/, '');
|
|
1148
|
+
html += '<div class="stdio-snippet">' +
|
|
1149
|
+
'<button class="copy-btn" onclick="copySnippet(this)">Copy</button>' +
|
|
1150
|
+
'<pre>' + escapeHtml(snippet) + '</pre>' +
|
|
1151
|
+
'</div>';
|
|
1152
|
+
}
|
|
1153
|
+
return html;
|
|
1154
|
+
}).join('');
|
|
1155
|
+
|
|
1156
|
+
btn.textContent = 'Continue';
|
|
1157
|
+
btn.disabled = false;
|
|
1158
|
+
btn.onclick = function() { goToStep('connect'); };
|
|
1159
|
+
} catch (err) {
|
|
1160
|
+
btn.disabled = false;
|
|
1161
|
+
btn.textContent = 'Retry';
|
|
1162
|
+
var resultEl = document.getElementById('client-result');
|
|
1163
|
+
resultEl.className = '';
|
|
1164
|
+
resultEl.innerHTML = '<div class="status status-error"><span>✗</span><span>' + escapeHtml(err.message) + '</span></div>';
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// \u2500\u2500 Step 5: Connect Accounts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1169
|
+
async function renderConnect() {
|
|
1170
|
+
var el = document.getElementById('conn-list');
|
|
1171
|
+
el.innerHTML = '<div class="status status-info"><span class="spinner"></span> Loading connections...</div>';
|
|
1172
|
+
|
|
1173
|
+
var personalMcps = state.availableMcps.filter(function(m) {
|
|
1174
|
+
return m.authMode === 'personal' && state.selectedMcps.indexOf(m.name) !== -1;
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
if (personalMcps.length === 0) {
|
|
1178
|
+
el.innerHTML = '<div class="status status-info">No personal account connections needed.</div>';
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
try {
|
|
1183
|
+
var resp = await fetch('/api/connections');
|
|
1184
|
+
var data = await resp.json();
|
|
1185
|
+
state.connections = data.connections || [];
|
|
1186
|
+
} catch (e) {
|
|
1187
|
+
state.connections = [];
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
el.innerHTML = personalMcps.map(function(mcp) {
|
|
1191
|
+
var conn = state.connections.find(function(c) { return c.mcp_name === mcp.name; });
|
|
1192
|
+
var isConnected = conn && !conn.is_company_default && conn.account_email;
|
|
1193
|
+
|
|
1194
|
+
return (
|
|
1195
|
+
'<div class="conn-item' + (isConnected ? ' connected' : '') + '" id="conn-' + mcp.name + '">' +
|
|
1196
|
+
'<span class="conn-name">' + escapeHtml(mcp.displayName) + '</span>' +
|
|
1197
|
+
(isConnected
|
|
1198
|
+
? '<span class="conn-status connected">✓ ' + escapeHtml(conn.account_email) + '</span>'
|
|
1199
|
+
: '<span class="conn-status">Not connected</span>' +
|
|
1200
|
+
'<button class="btn btn-sm btn-secondary" onclick="connectProvider(\\'' + mcp.name + '\\', \\'' + escapeHtml(mcp.displayName) + '\\')">Connect</button>'
|
|
1201
|
+
) +
|
|
1202
|
+
'</div>'
|
|
1203
|
+
);
|
|
1204
|
+
}).join('');
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
async function connectProvider(name, displayName) {
|
|
1208
|
+
var item = document.getElementById('conn-' + name);
|
|
1209
|
+
var btn = item.querySelector('button');
|
|
1210
|
+
var statusSpan = item.querySelector('.conn-status');
|
|
1211
|
+
|
|
1212
|
+
btn.disabled = true;
|
|
1213
|
+
btn.innerHTML = '<span class="spinner"></span>';
|
|
1214
|
+
statusSpan.textContent = 'Connecting...';
|
|
1215
|
+
|
|
1216
|
+
try {
|
|
1217
|
+
var resp = await fetch('/api/oauth/connect', {
|
|
1218
|
+
method: 'POST',
|
|
1219
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1220
|
+
body: JSON.stringify({ provider: name }),
|
|
1221
|
+
});
|
|
1222
|
+
var data = await resp.json();
|
|
1223
|
+
|
|
1224
|
+
if (data.error) {
|
|
1225
|
+
statusSpan.textContent = data.error;
|
|
1226
|
+
statusSpan.className = 'conn-status';
|
|
1227
|
+
btn.disabled = false;
|
|
1228
|
+
btn.textContent = 'Retry';
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
window.open(data.authorization_url, '_blank');
|
|
1233
|
+
statusSpan.innerHTML = '<span class="spinner"></span> Waiting \\u2014 check the new tab';
|
|
1234
|
+
btn.textContent = 'Open again';
|
|
1235
|
+
btn.disabled = false;
|
|
1236
|
+
btn.onclick = function() { window.open(data.authorization_url, '_blank'); };
|
|
1237
|
+
|
|
1238
|
+
var result = await pollUntilDone(
|
|
1239
|
+
'/api/oauth/poll?session=' + encodeURIComponent(data.session_id),
|
|
1240
|
+
3000,
|
|
1241
|
+
data.expires_in * 1000
|
|
1242
|
+
);
|
|
1243
|
+
|
|
1244
|
+
var email = result.account_email || 'connected';
|
|
1245
|
+
item.classList.add('connected');
|
|
1246
|
+
statusSpan.className = 'conn-status connected';
|
|
1247
|
+
statusSpan.textContent = '\\u2713 ' + email;
|
|
1248
|
+
btn.remove();
|
|
1249
|
+
} catch (err) {
|
|
1250
|
+
statusSpan.textContent = err.message === 'Timeout' ? 'Timed out' : err.message;
|
|
1251
|
+
statusSpan.className = 'conn-status';
|
|
1252
|
+
btn.disabled = false;
|
|
1253
|
+
btn.textContent = 'Retry';
|
|
1254
|
+
btn.onclick = function() { connectProvider(name, displayName); };
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// \u2500\u2500 Step 6: Done \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1259
|
+
function renderDone() {
|
|
1260
|
+
var el = document.getElementById('summary');
|
|
1261
|
+
var html = '';
|
|
1262
|
+
|
|
1263
|
+
html += '<div class="summary-section">';
|
|
1264
|
+
html += '<h3>Integrations (' + state.selectedMcps.length + ')</h3>';
|
|
1265
|
+
state.selectedMcps.forEach(function(name) {
|
|
1266
|
+
var mcp = state.availableMcps.find(function(m) { return m.name === name; });
|
|
1267
|
+
html += '<div class="summary-item"><span class="check-icon">✓</span>' + escapeHtml(mcp ? mcp.displayName : name) + '</div>';
|
|
1268
|
+
});
|
|
1269
|
+
html += '</div>';
|
|
1270
|
+
|
|
1271
|
+
if (state.configuredClients.length > 0) {
|
|
1272
|
+
html += '<div class="summary-section">';
|
|
1273
|
+
html += '<h3>Configured Clients</h3>';
|
|
1274
|
+
state.configuredClients.forEach(function(c) {
|
|
1275
|
+
var client = state.detectedClients.find(function(d) { return d.type === c; });
|
|
1276
|
+
html += '<div class="summary-item"><span class="check-icon">✓</span>' + escapeHtml(client ? client.name : c) + '</div>';
|
|
1277
|
+
});
|
|
1278
|
+
html += '</div>';
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
if (state.credentials) {
|
|
1282
|
+
html += '<div class="summary-section">';
|
|
1283
|
+
html += '<h3>Signed In</h3>';
|
|
1284
|
+
html += '<div class="summary-item"><span class="check-icon">✓</span>' + escapeHtml(state.credentials.name || state.credentials.email) + '</div>';
|
|
1285
|
+
html += '</div>';
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
el.innerHTML = html;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
async function finishSetup() {
|
|
1292
|
+
try {
|
|
1293
|
+
await fetch('/api/complete', {
|
|
1294
|
+
method: 'POST',
|
|
1295
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1296
|
+
body: JSON.stringify({
|
|
1297
|
+
mcps: state.selectedMcps,
|
|
1298
|
+
configuredClients: state.configuredClients,
|
|
1299
|
+
}),
|
|
1300
|
+
});
|
|
1301
|
+
} catch (e) {
|
|
1302
|
+
// Server may already be closing
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
document.getElementById('step-done').querySelector('.card').innerHTML =
|
|
1306
|
+
'<div class="text-center">' +
|
|
1307
|
+
'<div class="success-circle"><span class="success-check">✓</span></div>' +
|
|
1308
|
+
'<h2>All Done</h2>' +
|
|
1309
|
+
'<p class="subtitle mt-2">You can close this tab. Restart your AI clients to see the new tools.</p>' +
|
|
1310
|
+
'</div>';
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1314
|
+
function pollUntilDone(url, intervalMs, timeoutMs) {
|
|
1315
|
+
return new Promise(function(resolve, reject) {
|
|
1316
|
+
var deadline = Date.now() + timeoutMs;
|
|
1317
|
+
var timer = setInterval(async function() {
|
|
1318
|
+
if (Date.now() > deadline) {
|
|
1319
|
+
clearInterval(timer);
|
|
1320
|
+
reject(new Error('Timeout'));
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
try {
|
|
1324
|
+
var resp = await fetch(url);
|
|
1325
|
+
var data = await resp.json();
|
|
1326
|
+
if (data.status === 'completed') {
|
|
1327
|
+
clearInterval(timer);
|
|
1328
|
+
resolve(data);
|
|
1329
|
+
} else if (data.status === 'error' || data.status === 'expired') {
|
|
1330
|
+
clearInterval(timer);
|
|
1331
|
+
reject(new Error(data.error_message || data.status));
|
|
1332
|
+
}
|
|
1333
|
+
} catch (e) {
|
|
1334
|
+
// Network error -- keep polling
|
|
1335
|
+
}
|
|
1336
|
+
}, intervalMs);
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function copySnippet(btn) {
|
|
1341
|
+
var pre = btn.parentElement.querySelector('pre');
|
|
1342
|
+
navigator.clipboard.writeText(pre.textContent).then(function() {
|
|
1343
|
+
btn.textContent = 'Copied!';
|
|
1344
|
+
setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function escapeHtml(str) {
|
|
1349
|
+
var div = document.createElement('div');
|
|
1350
|
+
div.textContent = str || '';
|
|
1351
|
+
return div.innerHTML;
|
|
1352
|
+
}
|
|
1353
|
+
</script>
|
|
1354
|
+
</body>
|
|
1355
|
+
</html>`;
|
|
64
1356
|
}
|
|
65
1357
|
|
|
66
1358
|
// src/config/clients.ts
|
|
67
|
-
import { existsSync
|
|
1359
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
68
1360
|
import { dirname } from "path";
|
|
69
|
-
import { mkdirSync
|
|
1361
|
+
import { mkdirSync } from "fs";
|
|
70
1362
|
import { execSync } from "child_process";
|
|
1363
|
+
import { homedir } from "os";
|
|
1364
|
+
import { join } from "path";
|
|
71
1365
|
function detectClients() {
|
|
72
1366
|
const clients = [];
|
|
73
1367
|
const desktopPath = getClaudeDesktopConfigPath();
|
|
74
1368
|
const desktopDir = dirname(desktopPath);
|
|
75
1369
|
clients.push({
|
|
76
1370
|
type: "claude-desktop",
|
|
77
|
-
name: "Claude Desktop",
|
|
1371
|
+
name: "Claude (Desktop)",
|
|
78
1372
|
configPath: desktopPath,
|
|
79
|
-
detected:
|
|
1373
|
+
detected: existsSync(desktopDir)
|
|
80
1374
|
});
|
|
81
1375
|
let claudeCodeDetected = false;
|
|
82
1376
|
try {
|
|
@@ -96,15 +1390,43 @@ function detectClients() {
|
|
|
96
1390
|
type: "cursor",
|
|
97
1391
|
name: "Cursor",
|
|
98
1392
|
configPath: cursorPath,
|
|
99
|
-
detected:
|
|
1393
|
+
detected: existsSync(cursorDir)
|
|
1394
|
+
});
|
|
1395
|
+
const home = homedir();
|
|
1396
|
+
const vscodePath = getVSCodeMcpConfigPath();
|
|
1397
|
+
clients.push({
|
|
1398
|
+
type: "vscode",
|
|
1399
|
+
name: "VS Code",
|
|
1400
|
+
configPath: vscodePath,
|
|
1401
|
+
detected: existsSync(join(home, ".vscode"))
|
|
1402
|
+
});
|
|
1403
|
+
const antigravityPath = getAntigravityConfigPath();
|
|
1404
|
+
clients.push({
|
|
1405
|
+
type: "antigravity",
|
|
1406
|
+
name: "Antigravity",
|
|
1407
|
+
configPath: antigravityPath,
|
|
1408
|
+
detected: existsSync(join(home, ".antigravity"))
|
|
100
1409
|
});
|
|
101
1410
|
const codexPath = getCodexConfigPath();
|
|
102
|
-
|
|
1411
|
+
let codexDetected = existsSync(join(home, ".codex"));
|
|
1412
|
+
if (!codexDetected) {
|
|
1413
|
+
try {
|
|
1414
|
+
execSync("which codex", { stdio: "pipe" });
|
|
1415
|
+
codexDetected = true;
|
|
1416
|
+
} catch {
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
103
1419
|
clients.push({
|
|
104
1420
|
type: "codex",
|
|
105
|
-
name: "Codex",
|
|
1421
|
+
name: "Codex (OpenAI)",
|
|
106
1422
|
configPath: codexPath,
|
|
107
|
-
detected:
|
|
1423
|
+
detected: codexDetected
|
|
1424
|
+
});
|
|
1425
|
+
clients.push({
|
|
1426
|
+
type: "stdio",
|
|
1427
|
+
name: "OpenClaw (stdio)",
|
|
1428
|
+
configPath: null,
|
|
1429
|
+
detected: true
|
|
108
1430
|
});
|
|
109
1431
|
return clients;
|
|
110
1432
|
}
|
|
@@ -122,13 +1444,13 @@ function buildHttpEntries(serverUrl, apiKey, mcps) {
|
|
|
122
1444
|
function configureClaudeDesktop(_serverUrl, apiKey, _mcps) {
|
|
123
1445
|
const configPath = getClaudeDesktopConfigPath();
|
|
124
1446
|
const dir = dirname(configPath);
|
|
125
|
-
if (!
|
|
126
|
-
|
|
1447
|
+
if (!existsSync(dir)) {
|
|
1448
|
+
mkdirSync(dir, { recursive: true });
|
|
127
1449
|
}
|
|
128
1450
|
let config = {};
|
|
129
|
-
if (
|
|
1451
|
+
if (existsSync(configPath)) {
|
|
130
1452
|
try {
|
|
131
|
-
config = JSON.parse(
|
|
1453
|
+
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
132
1454
|
} catch {
|
|
133
1455
|
}
|
|
134
1456
|
}
|
|
@@ -141,11 +1463,15 @@ function configureClaudeDesktop(_serverUrl, apiKey, _mcps) {
|
|
|
141
1463
|
delete servers[key];
|
|
142
1464
|
}
|
|
143
1465
|
}
|
|
144
|
-
|
|
1466
|
+
const isWindows = getPlatform() === "windows";
|
|
1467
|
+
servers["cortex"] = isWindows ? {
|
|
1468
|
+
command: "cmd",
|
|
1469
|
+
args: ["/c", "npx", "-y", "@danainnovations/cortex-mcp@latest", "serve"]
|
|
1470
|
+
} : {
|
|
145
1471
|
command: "npx",
|
|
146
1472
|
args: ["-y", "@danainnovations/cortex-mcp@latest", "serve"]
|
|
147
1473
|
};
|
|
148
|
-
|
|
1474
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
149
1475
|
}
|
|
150
1476
|
function configureClaudeCode(serverUrl, apiKey, mcps) {
|
|
151
1477
|
for (const mcp of AVAILABLE_MCPS) {
|
|
@@ -164,13 +1490,13 @@ function configureClaudeCode(serverUrl, apiKey, mcps) {
|
|
|
164
1490
|
function configureCursor(serverUrl, apiKey, mcps) {
|
|
165
1491
|
const configPath = getCursorConfigPath();
|
|
166
1492
|
const dir = dirname(configPath);
|
|
167
|
-
if (!
|
|
168
|
-
|
|
1493
|
+
if (!existsSync(dir)) {
|
|
1494
|
+
mkdirSync(dir, { recursive: true });
|
|
169
1495
|
}
|
|
170
1496
|
let config = {};
|
|
171
|
-
if (
|
|
1497
|
+
if (existsSync(configPath)) {
|
|
172
1498
|
try {
|
|
173
|
-
config = JSON.parse(
|
|
1499
|
+
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
174
1500
|
} catch {
|
|
175
1501
|
}
|
|
176
1502
|
}
|
|
@@ -185,24 +1511,109 @@ function configureCursor(serverUrl, apiKey, mcps) {
|
|
|
185
1511
|
}
|
|
186
1512
|
const entries = buildHttpEntries(serverUrl, apiKey, mcps);
|
|
187
1513
|
Object.assign(servers, entries);
|
|
188
|
-
|
|
1514
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
189
1515
|
}
|
|
190
|
-
function
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
1516
|
+
function configureVSCode(serverUrl, apiKey, mcps) {
|
|
1517
|
+
const configPath = getVSCodeMcpConfigPath();
|
|
1518
|
+
const dir = dirname(configPath);
|
|
1519
|
+
if (!existsSync(dir)) {
|
|
1520
|
+
mkdirSync(dir, { recursive: true });
|
|
1521
|
+
}
|
|
1522
|
+
let config = {};
|
|
1523
|
+
if (existsSync(configPath)) {
|
|
1524
|
+
try {
|
|
1525
|
+
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
1526
|
+
} catch {
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
if (!config.mcpServers || typeof config.mcpServers !== "object") {
|
|
1530
|
+
config.mcpServers = {};
|
|
1531
|
+
}
|
|
1532
|
+
const servers = config.mcpServers;
|
|
1533
|
+
for (const key of Object.keys(servers)) {
|
|
1534
|
+
if (key.startsWith("cortex-")) {
|
|
1535
|
+
delete servers[key];
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
const entries = buildHttpEntries(serverUrl, apiKey, mcps);
|
|
1539
|
+
Object.assign(servers, entries);
|
|
1540
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1541
|
+
}
|
|
1542
|
+
function configureAntigravity(serverUrl, apiKey, mcps) {
|
|
1543
|
+
const configPath = getAntigravityConfigPath();
|
|
1544
|
+
const dir = dirname(configPath);
|
|
1545
|
+
if (!existsSync(dir)) {
|
|
1546
|
+
mkdirSync(dir, { recursive: true });
|
|
1547
|
+
}
|
|
1548
|
+
let config = {};
|
|
1549
|
+
if (existsSync(configPath)) {
|
|
1550
|
+
try {
|
|
1551
|
+
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
1552
|
+
} catch {
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
if (!config.mcpServers || typeof config.mcpServers !== "object") {
|
|
1556
|
+
config.mcpServers = {};
|
|
1557
|
+
}
|
|
1558
|
+
const servers = config.mcpServers;
|
|
1559
|
+
for (const key of Object.keys(servers)) {
|
|
1560
|
+
if (key.startsWith("cortex-")) {
|
|
1561
|
+
delete servers[key];
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
const entries = buildHttpEntries(serverUrl, apiKey, mcps);
|
|
1565
|
+
Object.assign(servers, entries);
|
|
1566
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1567
|
+
}
|
|
1568
|
+
function configureCodex(serverUrl, apiKey, mcps) {
|
|
1569
|
+
const configPath = getCodexConfigPath();
|
|
1570
|
+
const dir = dirname(configPath);
|
|
1571
|
+
if (!existsSync(dir)) {
|
|
1572
|
+
mkdirSync(dir, { recursive: true });
|
|
1573
|
+
}
|
|
1574
|
+
let existingContent = "";
|
|
1575
|
+
if (existsSync(configPath)) {
|
|
1576
|
+
try {
|
|
1577
|
+
existingContent = readFileSync(configPath, "utf-8");
|
|
1578
|
+
} catch {
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
const cleaned = existingContent.replace(
|
|
1582
|
+
/\[mcp_servers\.cortex-[^\]]*\][^[]*(?=\[|$)/g,
|
|
1583
|
+
""
|
|
1584
|
+
).trim();
|
|
1585
|
+
const tomlEntries = [];
|
|
1586
|
+
for (const mcp of AVAILABLE_MCPS) {
|
|
1587
|
+
if (!mcps.includes(mcp.name)) continue;
|
|
1588
|
+
tomlEntries.push(
|
|
1589
|
+
`[mcp_servers.${mcp.serverName}]
|
|
1590
|
+
url = "${serverUrl}/mcp/${mcp.name}"
|
|
1591
|
+
http_headers = { "x-api-key" = "${apiKey}" }`
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
const newContent = (cleaned ? cleaned + "\n\n" : "") + tomlEntries.join("\n\n") + "\n";
|
|
1595
|
+
writeFileSync(configPath, newContent);
|
|
1596
|
+
}
|
|
1597
|
+
function generateStdioSnippet(_apiKey) {
|
|
1598
|
+
const isWindows = getPlatform() === "windows";
|
|
1599
|
+
const config = {
|
|
1600
|
+
mcpServers: {
|
|
1601
|
+
cortex: isWindows ? {
|
|
1602
|
+
command: "cmd",
|
|
1603
|
+
args: ["/c", "npx", "-y", "@danainnovations/cortex-mcp@latest", "serve"]
|
|
1604
|
+
} : {
|
|
1605
|
+
command: "npx",
|
|
1606
|
+
args: ["-y", "@danainnovations/cortex-mcp@latest", "serve"]
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
198
1609
|
};
|
|
199
1610
|
return JSON.stringify(config, null, 2);
|
|
200
1611
|
}
|
|
201
1612
|
function resetClaudeDesktop() {
|
|
202
1613
|
const configPath = getClaudeDesktopConfigPath();
|
|
203
|
-
if (!
|
|
1614
|
+
if (!existsSync(configPath)) return false;
|
|
204
1615
|
try {
|
|
205
|
-
const config = JSON.parse(
|
|
1616
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
206
1617
|
if (!config.mcpServers) return false;
|
|
207
1618
|
const servers = config.mcpServers;
|
|
208
1619
|
let removed = false;
|
|
@@ -213,7 +1624,7 @@ function resetClaudeDesktop() {
|
|
|
213
1624
|
}
|
|
214
1625
|
}
|
|
215
1626
|
if (removed) {
|
|
216
|
-
|
|
1627
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
217
1628
|
}
|
|
218
1629
|
return removed;
|
|
219
1630
|
} catch {
|
|
@@ -222,9 +1633,9 @@ function resetClaudeDesktop() {
|
|
|
222
1633
|
}
|
|
223
1634
|
function resetCursor() {
|
|
224
1635
|
const configPath = getCursorConfigPath();
|
|
225
|
-
if (!
|
|
1636
|
+
if (!existsSync(configPath)) return false;
|
|
226
1637
|
try {
|
|
227
|
-
const config = JSON.parse(
|
|
1638
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
228
1639
|
if (!config.mcpServers) return false;
|
|
229
1640
|
const servers = config.mcpServers;
|
|
230
1641
|
let removed = false;
|
|
@@ -235,7 +1646,7 @@ function resetCursor() {
|
|
|
235
1646
|
}
|
|
236
1647
|
}
|
|
237
1648
|
if (removed) {
|
|
238
|
-
|
|
1649
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
239
1650
|
}
|
|
240
1651
|
return removed;
|
|
241
1652
|
} catch {
|
|
@@ -253,56 +1664,17 @@ function resetClaudeCode() {
|
|
|
253
1664
|
}
|
|
254
1665
|
return removed;
|
|
255
1666
|
}
|
|
256
|
-
function removeCortexTomlSections(content) {
|
|
257
|
-
const lines = content.split("\n");
|
|
258
|
-
const result = [];
|
|
259
|
-
let skipping = false;
|
|
260
|
-
for (const line of lines) {
|
|
261
|
-
const trimmed = line.trim();
|
|
262
|
-
if (trimmed.startsWith("[")) {
|
|
263
|
-
skipping = /^\[mcp_servers\.cortex[_-]/.test(trimmed) || trimmed === "[mcp_servers.cortex]";
|
|
264
|
-
}
|
|
265
|
-
if (!skipping) {
|
|
266
|
-
result.push(line);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return result.join("\n");
|
|
270
|
-
}
|
|
271
|
-
function configureCodex(serverUrl, apiKey, mcps) {
|
|
272
|
-
const configPath = getCodexConfigPath();
|
|
273
|
-
const dir = dirname(configPath);
|
|
274
|
-
if (!existsSync2(dir)) {
|
|
275
|
-
mkdirSync2(dir, { recursive: true });
|
|
276
|
-
}
|
|
277
|
-
let content = "";
|
|
278
|
-
if (existsSync2(configPath)) {
|
|
279
|
-
try {
|
|
280
|
-
content = readFileSync2(configPath, "utf-8");
|
|
281
|
-
} catch {
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
content = removeCortexTomlSections(content);
|
|
285
|
-
const sections = [];
|
|
286
|
-
for (const mcp of AVAILABLE_MCPS) {
|
|
287
|
-
if (!mcps.includes(mcp.name)) continue;
|
|
288
|
-
sections.push(`[mcp_servers.${mcp.serverName}]`);
|
|
289
|
-
sections.push(`url = "${serverUrl}/mcp/${mcp.name}"`);
|
|
290
|
-
sections.push(`[mcp_servers.${mcp.serverName}.http_headers]`);
|
|
291
|
-
sections.push(`"x-api-key" = "${apiKey}"`);
|
|
292
|
-
sections.push("");
|
|
293
|
-
}
|
|
294
|
-
const trimmed = content.trimEnd();
|
|
295
|
-
const newContent = trimmed ? trimmed + "\n\n" + sections.join("\n") + "\n" : sections.join("\n") + "\n";
|
|
296
|
-
writeFileSync2(configPath, newContent);
|
|
297
|
-
}
|
|
298
1667
|
function resetCodex() {
|
|
299
1668
|
const configPath = getCodexConfigPath();
|
|
300
|
-
if (!
|
|
1669
|
+
if (!existsSync(configPath)) return false;
|
|
301
1670
|
try {
|
|
302
|
-
const content =
|
|
303
|
-
const cleaned =
|
|
304
|
-
|
|
305
|
-
|
|
1671
|
+
const content = readFileSync(configPath, "utf-8");
|
|
1672
|
+
const cleaned = content.replace(
|
|
1673
|
+
/\[mcp_servers\.cortex-[^\]]*\][^[]*(?=\[|$)/g,
|
|
1674
|
+
""
|
|
1675
|
+
).trim();
|
|
1676
|
+
if (cleaned !== content.trim()) {
|
|
1677
|
+
writeFileSync(configPath, cleaned ? cleaned + "\n" : "");
|
|
306
1678
|
return true;
|
|
307
1679
|
}
|
|
308
1680
|
return false;
|
|
@@ -321,6 +1693,12 @@ function configureClient(clientType, serverUrl, apiKey, mcps) {
|
|
|
321
1693
|
case "cursor":
|
|
322
1694
|
configureCursor(serverUrl, apiKey, mcps);
|
|
323
1695
|
return "Cursor configured";
|
|
1696
|
+
case "vscode":
|
|
1697
|
+
configureVSCode(serverUrl, apiKey, mcps);
|
|
1698
|
+
return "VS Code configured";
|
|
1699
|
+
case "antigravity":
|
|
1700
|
+
configureAntigravity(serverUrl, apiKey, mcps);
|
|
1701
|
+
return "Antigravity configured";
|
|
324
1702
|
case "codex":
|
|
325
1703
|
configureCodex(serverUrl, apiKey, mcps);
|
|
326
1704
|
return "Codex configured";
|
|
@@ -329,11 +1707,50 @@ function configureClient(clientType, serverUrl, apiKey, mcps) {
|
|
|
329
1707
|
}
|
|
330
1708
|
}
|
|
331
1709
|
|
|
1710
|
+
// src/config/storage.ts
|
|
1711
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1712
|
+
import { join as join2 } from "path";
|
|
1713
|
+
function getConfigDir() {
|
|
1714
|
+
return join2(getHomeDir(), CONFIG_DIR_NAME);
|
|
1715
|
+
}
|
|
1716
|
+
function getConfigPath() {
|
|
1717
|
+
return join2(getConfigDir(), CONFIG_FILE_NAME);
|
|
1718
|
+
}
|
|
1719
|
+
function readConfig() {
|
|
1720
|
+
const path = getConfigPath();
|
|
1721
|
+
if (!existsSync2(path)) return null;
|
|
1722
|
+
try {
|
|
1723
|
+
const raw = readFileSync2(path, "utf-8");
|
|
1724
|
+
return JSON.parse(raw);
|
|
1725
|
+
} catch {
|
|
1726
|
+
return null;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
function writeConfig(config) {
|
|
1730
|
+
const dir = getConfigDir();
|
|
1731
|
+
if (!existsSync2(dir)) {
|
|
1732
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
1733
|
+
}
|
|
1734
|
+
const path = getConfigPath();
|
|
1735
|
+
writeFileSync2(path, JSON.stringify(config, null, 2) + "\n", {
|
|
1736
|
+
mode: 384
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
function createConfig(apiKey, mcps) {
|
|
1740
|
+
return {
|
|
1741
|
+
version: 1,
|
|
1742
|
+
server: DEFAULT_SERVER_URL,
|
|
1743
|
+
apiKey,
|
|
1744
|
+
mcps,
|
|
1745
|
+
configuredClients: []
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
|
|
332
1749
|
// src/auth/credentials.ts
|
|
333
1750
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
334
|
-
import { join as
|
|
1751
|
+
import { join as join3 } from "path";
|
|
335
1752
|
function getCredentialsPath() {
|
|
336
|
-
return
|
|
1753
|
+
return join3(getHomeDir(), CONFIG_DIR_NAME, CREDENTIALS_FILE_NAME);
|
|
337
1754
|
}
|
|
338
1755
|
function readCredentials() {
|
|
339
1756
|
const path = getCredentialsPath();
|
|
@@ -346,7 +1763,7 @@ function readCredentials() {
|
|
|
346
1763
|
}
|
|
347
1764
|
}
|
|
348
1765
|
function writeCredentials(creds) {
|
|
349
|
-
const dir =
|
|
1766
|
+
const dir = join3(getHomeDir(), CONFIG_DIR_NAME);
|
|
350
1767
|
if (!existsSync3(dir)) {
|
|
351
1768
|
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
352
1769
|
}
|
|
@@ -371,691 +1788,339 @@ function getEffectiveApiKey() {
|
|
|
371
1788
|
return DEFAULT_API_KEY;
|
|
372
1789
|
}
|
|
373
1790
|
|
|
374
|
-
// src/
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
function sleep(ms) {
|
|
382
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1791
|
+
// src/wizard/routes.ts
|
|
1792
|
+
var wizardState = null;
|
|
1793
|
+
function getState() {
|
|
1794
|
+
if (!wizardState) {
|
|
1795
|
+
wizardState = { apiKey: getEffectiveApiKey() };
|
|
1796
|
+
}
|
|
1797
|
+
return wizardState;
|
|
383
1798
|
}
|
|
384
|
-
function
|
|
1799
|
+
function parseBody(req) {
|
|
385
1800
|
return new Promise((resolve) => {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
1801
|
+
const chunks = [];
|
|
1802
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
1803
|
+
req.on("end", () => {
|
|
1804
|
+
try {
|
|
1805
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString()));
|
|
1806
|
+
} catch {
|
|
1807
|
+
resolve({});
|
|
1808
|
+
}
|
|
391
1809
|
});
|
|
1810
|
+
req.on("error", () => resolve({}));
|
|
392
1811
|
});
|
|
393
1812
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
1813
|
+
function json(res, data, status = 200) {
|
|
1814
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1815
|
+
res.end(JSON.stringify(data));
|
|
1816
|
+
}
|
|
1817
|
+
async function handleApiRoute(path, searchParams, req, res, options, onComplete) {
|
|
1818
|
+
const method = req.method || "GET";
|
|
1819
|
+
if (path === "/api/state" && method === "GET") {
|
|
1820
|
+
const creds = readCredentials();
|
|
1821
|
+
const config = readConfig();
|
|
1822
|
+
const clients = detectClients();
|
|
1823
|
+
json(res, {
|
|
1824
|
+
credentials: creds ? { email: creds.email, name: creds.name } : null,
|
|
1825
|
+
config,
|
|
1826
|
+
apiKey: getState().apiKey,
|
|
1827
|
+
defaultMcps: [...DEFAULT_MCPS],
|
|
1828
|
+
availableMcps: AVAILABLE_MCPS.map((m) => ({
|
|
1829
|
+
name: m.name,
|
|
1830
|
+
displayName: m.displayName,
|
|
1831
|
+
description: m.description,
|
|
1832
|
+
authMode: m.authMode
|
|
1833
|
+
})),
|
|
1834
|
+
detectedClients: clients
|
|
1835
|
+
});
|
|
1836
|
+
return true;
|
|
1837
|
+
}
|
|
1838
|
+
if (path === "/api/auth/login" && method === "POST") {
|
|
1839
|
+
try {
|
|
1840
|
+
const resp = await fetch(
|
|
1841
|
+
`${options.serverUrl}/api/v1/auth/employee/initiate`,
|
|
1842
|
+
{
|
|
1843
|
+
method: "POST",
|
|
1844
|
+
headers: { "Content-Type": "application/json" }
|
|
406
1845
|
}
|
|
1846
|
+
);
|
|
1847
|
+
if (!resp.ok) {
|
|
1848
|
+
json(res, { error: `Server returned HTTP ${resp.status}` }, 502);
|
|
1849
|
+
return true;
|
|
407
1850
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
1851
|
+
const data = await resp.json();
|
|
1852
|
+
json(res, data);
|
|
1853
|
+
} catch (err) {
|
|
1854
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1855
|
+
json(res, { error: `Could not reach Cortex server: ${msg}` }, 502);
|
|
1856
|
+
}
|
|
1857
|
+
return true;
|
|
1858
|
+
}
|
|
1859
|
+
if (path === "/api/auth/poll" && method === "GET") {
|
|
1860
|
+
const sessionId = searchParams.get("session");
|
|
1861
|
+
if (!sessionId) {
|
|
1862
|
+
json(res, { error: "Missing session parameter" }, 400);
|
|
1863
|
+
return true;
|
|
1864
|
+
}
|
|
1865
|
+
try {
|
|
1866
|
+
const resp = await fetch(
|
|
1867
|
+
`${options.serverUrl}/api/v1/auth/employee/poll/${sessionId}`
|
|
1868
|
+
);
|
|
1869
|
+
const result = await resp.json();
|
|
1870
|
+
if (result.status === "completed" && result.api_key) {
|
|
1871
|
+
const email = result.employee_email || result.user_info?.email || "";
|
|
1872
|
+
const name = result.employee_name || result.user_info?.name || "";
|
|
1873
|
+
writeCredentials({
|
|
1874
|
+
apiKey: result.api_key,
|
|
1875
|
+
email,
|
|
1876
|
+
name,
|
|
1877
|
+
authenticatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1878
|
+
});
|
|
1879
|
+
getState().apiKey = result.api_key;
|
|
1880
|
+
}
|
|
1881
|
+
json(res, result);
|
|
1882
|
+
} catch {
|
|
1883
|
+
json(res, { status: "pending" });
|
|
1884
|
+
}
|
|
1885
|
+
return true;
|
|
1886
|
+
}
|
|
1887
|
+
if (path === "/api/clients/configure" && method === "POST") {
|
|
1888
|
+
const body = await parseBody(req);
|
|
1889
|
+
const clients = body.clients || [];
|
|
1890
|
+
const mcps = body.mcps || [...DEFAULT_MCPS];
|
|
1891
|
+
const apiKey = getState().apiKey;
|
|
1892
|
+
const results = [];
|
|
1893
|
+
for (const clientType of clients) {
|
|
1894
|
+
try {
|
|
1895
|
+
const msg = configureClient(
|
|
1896
|
+
clientType,
|
|
1897
|
+
options.serverUrl,
|
|
1898
|
+
apiKey,
|
|
1899
|
+
mcps
|
|
1900
|
+
);
|
|
1901
|
+
results.push({ client: clientType, success: true, message: msg });
|
|
1902
|
+
} catch (err) {
|
|
1903
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1904
|
+
results.push({ client: clientType, success: false, error: msg });
|
|
413
1905
|
}
|
|
414
|
-
const err = await resp.json().catch(() => ({}));
|
|
415
|
-
log(` Error: ${err.detail || `HTTP ${resp.status}`}`);
|
|
416
|
-
return false;
|
|
417
1906
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
authUrl = data.authorization_url;
|
|
421
|
-
expiresIn = data.expires_in;
|
|
422
|
-
} catch (err) {
|
|
423
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
424
|
-
log(` Error: Could not reach Cortex server at ${serverUrl}`);
|
|
425
|
-
log(` ${msg}`);
|
|
426
|
-
return false;
|
|
1907
|
+
json(res, { results });
|
|
1908
|
+
return true;
|
|
427
1909
|
}
|
|
428
|
-
if (
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
1910
|
+
if (path === "/api/connections" && method === "GET") {
|
|
1911
|
+
const apiKey = getState().apiKey;
|
|
1912
|
+
try {
|
|
1913
|
+
const resp = await fetch(
|
|
1914
|
+
`${options.serverUrl}/api/v1/oauth/connections`,
|
|
1915
|
+
{ headers: { "x-api-key": apiKey } }
|
|
1916
|
+
);
|
|
1917
|
+
if (resp.ok) {
|
|
1918
|
+
const data = await resp.json();
|
|
1919
|
+
json(res, data);
|
|
1920
|
+
} else {
|
|
1921
|
+
json(res, { connections: [] });
|
|
1922
|
+
}
|
|
1923
|
+
} catch {
|
|
1924
|
+
json(res, { connections: [] });
|
|
1925
|
+
}
|
|
1926
|
+
return true;
|
|
437
1927
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
await sleep(3e3);
|
|
1928
|
+
if (path === "/api/oauth/connect" && method === "POST") {
|
|
1929
|
+
const body = await parseBody(req);
|
|
1930
|
+
const provider = body.provider;
|
|
1931
|
+
const apiKey = getState().apiKey;
|
|
1932
|
+
if (!provider) {
|
|
1933
|
+
json(res, { error: "Missing provider" }, 400);
|
|
1934
|
+
return true;
|
|
1935
|
+
}
|
|
447
1936
|
try {
|
|
448
1937
|
const resp = await fetch(
|
|
449
|
-
`${serverUrl}/api/v1/oauth/connect
|
|
1938
|
+
`${options.serverUrl}/api/v1/oauth/connect/${provider}/initiate`,
|
|
450
1939
|
{
|
|
1940
|
+
method: "POST",
|
|
451
1941
|
headers: {
|
|
1942
|
+
"Content-Type": "application/json",
|
|
452
1943
|
"x-api-key": apiKey
|
|
453
1944
|
}
|
|
454
1945
|
}
|
|
455
1946
|
);
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
1947
|
+
if (!resp.ok) {
|
|
1948
|
+
if (resp.status === 401) {
|
|
1949
|
+
json(res, { error: "Session expired. Please re-login." }, 401);
|
|
1950
|
+
return true;
|
|
1951
|
+
}
|
|
1952
|
+
const err = await resp.json().catch(() => ({}));
|
|
1953
|
+
json(res, { error: err.detail || `HTTP ${resp.status}` }, resp.status);
|
|
463
1954
|
return true;
|
|
464
1955
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
if (result.status === "error") {
|
|
471
|
-
process.stderr.write("\n");
|
|
472
|
-
log(
|
|
473
|
-
` Connection failed: ${result.error_message || "unknown error"}`
|
|
474
|
-
);
|
|
475
|
-
return false;
|
|
476
|
-
}
|
|
477
|
-
process.stderr.write(".");
|
|
478
|
-
} catch {
|
|
479
|
-
process.stderr.write("!");
|
|
1956
|
+
const data = await resp.json();
|
|
1957
|
+
json(res, data);
|
|
1958
|
+
} catch (err) {
|
|
1959
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1960
|
+
json(res, { error: `Could not reach Cortex server: ${msg}` }, 502);
|
|
480
1961
|
}
|
|
1962
|
+
return true;
|
|
481
1963
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
log(` Unknown provider: ${provider}`);
|
|
501
|
-
log(` Available: ${AVAILABLE_MCPS.map((m) => m.name).join(", ")}`);
|
|
502
|
-
log("");
|
|
503
|
-
process.exit(1);
|
|
1964
|
+
if (path === "/api/oauth/poll" && method === "GET") {
|
|
1965
|
+
const sessionId = searchParams.get("session");
|
|
1966
|
+
if (!sessionId) {
|
|
1967
|
+
json(res, { error: "Missing session parameter" }, 400);
|
|
1968
|
+
return true;
|
|
1969
|
+
}
|
|
1970
|
+
const apiKey = getState().apiKey;
|
|
1971
|
+
try {
|
|
1972
|
+
const resp = await fetch(
|
|
1973
|
+
`${options.serverUrl}/api/v1/oauth/connect/poll/${sessionId}`,
|
|
1974
|
+
{ headers: { "x-api-key": apiKey } }
|
|
1975
|
+
);
|
|
1976
|
+
const result = await resp.json();
|
|
1977
|
+
json(res, result);
|
|
1978
|
+
} catch {
|
|
1979
|
+
json(res, { status: "pending" });
|
|
1980
|
+
}
|
|
1981
|
+
return true;
|
|
504
1982
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
serverUrl,
|
|
516
|
-
"installUrl" in mcp ? mcp.installUrl : void 0
|
|
517
|
-
);
|
|
518
|
-
if (!success) {
|
|
519
|
-
process.exit(1);
|
|
1983
|
+
if (path === "/api/complete" && method === "POST") {
|
|
1984
|
+
const body = await parseBody(req);
|
|
1985
|
+
const mcps = body.mcps || [...DEFAULT_MCPS];
|
|
1986
|
+
const configuredClients = body.configuredClients || [];
|
|
1987
|
+
const config = createConfig(getState().apiKey, mcps);
|
|
1988
|
+
config.configuredClients = configuredClients;
|
|
1989
|
+
writeConfig(config);
|
|
1990
|
+
json(res, { success: true });
|
|
1991
|
+
setTimeout(onComplete, 100);
|
|
1992
|
+
return true;
|
|
520
1993
|
}
|
|
1994
|
+
return false;
|
|
521
1995
|
}
|
|
522
1996
|
|
|
523
|
-
// src/
|
|
524
|
-
function
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
async function loginWithSSO(serverUrl, emailHint) {
|
|
531
|
-
let sessionId;
|
|
532
|
-
let authUrl;
|
|
533
|
-
let expiresIn;
|
|
534
|
-
try {
|
|
535
|
-
const body = {};
|
|
536
|
-
if (emailHint) {
|
|
537
|
-
body.email_hint = emailHint;
|
|
538
|
-
}
|
|
539
|
-
const resp = await fetch(`${serverUrl}/api/v1/auth/employee/initiate`, {
|
|
540
|
-
method: "POST",
|
|
541
|
-
headers: { "Content-Type": "application/json" },
|
|
542
|
-
body: JSON.stringify(body)
|
|
1997
|
+
// src/wizard/server.ts
|
|
1998
|
+
function startWizardServer(options) {
|
|
1999
|
+
return new Promise((resolve, reject) => {
|
|
2000
|
+
let completionResolve;
|
|
2001
|
+
const completionPromise = new Promise((r) => {
|
|
2002
|
+
completionResolve = r;
|
|
543
2003
|
});
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const resp = await fetch(
|
|
569
|
-
`${serverUrl}/api/v1/auth/employee/poll/${sessionId}`
|
|
570
|
-
);
|
|
571
|
-
const result = await resp.json();
|
|
572
|
-
if (result.status === "completed") {
|
|
573
|
-
process.stderr.write("\n\n");
|
|
574
|
-
const email = result.employee_email || result.user_info?.email || "unknown";
|
|
575
|
-
const name = result.employee_name || result.user_info?.name || "";
|
|
576
|
-
const apiKey = result.api_key || "";
|
|
577
|
-
return { status: "success", apiKey, email, name: name || void 0 };
|
|
578
|
-
}
|
|
579
|
-
if (result.status === "expired") {
|
|
580
|
-
process.stderr.write("\n");
|
|
581
|
-
log2(" Authentication session expired. Please try again.");
|
|
582
|
-
return { status: "failed" };
|
|
583
|
-
}
|
|
584
|
-
if (result.status === "error") {
|
|
585
|
-
process.stderr.write("\n");
|
|
586
|
-
if (result.error_message === "not_verified_employee" && !emailHint) {
|
|
587
|
-
return {
|
|
588
|
-
status: "needs_hint",
|
|
589
|
-
ssoEmail: result.sso_email || "unknown"
|
|
590
|
-
};
|
|
2004
|
+
const onComplete = () => {
|
|
2005
|
+
completionResolve();
|
|
2006
|
+
};
|
|
2007
|
+
const server = http.createServer(async (req, res) => {
|
|
2008
|
+
const url = new URL(req.url || "/", `http://localhost`);
|
|
2009
|
+
const path = url.pathname;
|
|
2010
|
+
if (path.startsWith("/api/")) {
|
|
2011
|
+
try {
|
|
2012
|
+
const handled = await handleApiRoute(
|
|
2013
|
+
path,
|
|
2014
|
+
url.searchParams,
|
|
2015
|
+
req,
|
|
2016
|
+
res,
|
|
2017
|
+
options,
|
|
2018
|
+
onComplete
|
|
2019
|
+
);
|
|
2020
|
+
if (!handled) {
|
|
2021
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2022
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
2023
|
+
}
|
|
2024
|
+
} catch (err) {
|
|
2025
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2026
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2027
|
+
res.end(JSON.stringify({ error: msg }));
|
|
591
2028
|
}
|
|
592
|
-
|
|
593
|
-
` Authentication failed: ${result.error_message || "unknown error"}`
|
|
594
|
-
);
|
|
595
|
-
return { status: "failed" };
|
|
2029
|
+
return;
|
|
596
2030
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
process.stderr.write("!");
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
process.stderr.write("\n");
|
|
603
|
-
log2(" Timeout waiting for authentication. Please try again.");
|
|
604
|
-
return { status: "failed" };
|
|
605
|
-
}
|
|
606
|
-
async function showConnectionsAndAutoConnect(apiKey, serverUrl) {
|
|
607
|
-
let existingConnections = [];
|
|
608
|
-
try {
|
|
609
|
-
const resp = await fetch(`${serverUrl}/api/v1/oauth/connections`, {
|
|
610
|
-
headers: { "x-api-key": apiKey }
|
|
2031
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
2032
|
+
res.end(getWizardHtml());
|
|
611
2033
|
});
|
|
612
|
-
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
} catch {
|
|
624
|
-
log2(" Warning: Could not fetch current MCP connections due to a network error.");
|
|
625
|
-
}
|
|
626
|
-
log2("");
|
|
627
|
-
log2(" MCP Connections");
|
|
628
|
-
log2(" " + "\u2500".repeat(45));
|
|
629
|
-
const personalMcps = [];
|
|
630
|
-
for (const mcp of AVAILABLE_MCPS) {
|
|
631
|
-
const conn = existingConnections.find((c) => c.mcp_name === mcp.name);
|
|
632
|
-
if (mcp.authMode === "company") {
|
|
633
|
-
log2(` ${mcp.displayName.padEnd(15)} company default`);
|
|
634
|
-
} else {
|
|
635
|
-
if (conn && !conn.is_company_default && conn.account_email) {
|
|
636
|
-
log2(` ${mcp.displayName.padEnd(15)} ${conn.account_email}`);
|
|
637
|
-
} else {
|
|
638
|
-
log2(
|
|
639
|
-
` ${mcp.displayName.padEnd(15)} not connected (personal account required)`
|
|
640
|
-
);
|
|
641
|
-
personalMcps.push({ name: mcp.name, displayName: mcp.displayName });
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
log2("");
|
|
646
|
-
if (personalMcps.length === 0) {
|
|
647
|
-
log2(" All accounts connected!");
|
|
648
|
-
log2("");
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
log2(" The following MCPs require a personal account to access your data.");
|
|
652
|
-
log2(" Skip any you don't have an account for \u2014 you can always connect later.\n");
|
|
653
|
-
const rl2 = readline.createInterface({
|
|
654
|
-
input: process.stdin,
|
|
655
|
-
output: process.stderr
|
|
656
|
-
});
|
|
657
|
-
const ask2 = (question) => new Promise((resolve) => {
|
|
658
|
-
rl2.question(question, (answer) => resolve(answer.trim().toLowerCase()));
|
|
2034
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2035
|
+
const addr = server.address();
|
|
2036
|
+
resolve({
|
|
2037
|
+
port: addr.port,
|
|
2038
|
+
close: () => new Promise((r) => {
|
|
2039
|
+
server.close(() => r());
|
|
2040
|
+
}),
|
|
2041
|
+
waitForCompletion: () => completionPromise
|
|
2042
|
+
});
|
|
2043
|
+
});
|
|
2044
|
+
server.on("error", reject);
|
|
659
2045
|
});
|
|
660
|
-
for (const mcp of personalMcps) {
|
|
661
|
-
const answer = await ask2(
|
|
662
|
-
` Connect ${mcp.displayName}? (Y/n/s to skip all): `
|
|
663
|
-
);
|
|
664
|
-
if (answer === "s") {
|
|
665
|
-
log2(" Skipping remaining account connections.");
|
|
666
|
-
log2(" You can connect anytime with: cortex-mcp connect <provider>\n");
|
|
667
|
-
break;
|
|
668
|
-
}
|
|
669
|
-
if (answer === "n") {
|
|
670
|
-
log2(` Skipped. Connect later with: cortex-mcp connect ${mcp.name}`);
|
|
671
|
-
log2("");
|
|
672
|
-
continue;
|
|
673
|
-
}
|
|
674
|
-
log2("");
|
|
675
|
-
const success = await connectProvider(
|
|
676
|
-
mcp.name,
|
|
677
|
-
mcp.displayName,
|
|
678
|
-
apiKey,
|
|
679
|
-
serverUrl,
|
|
680
|
-
"installUrl" in mcp ? mcp.installUrl : void 0
|
|
681
|
-
);
|
|
682
|
-
if (!success) {
|
|
683
|
-
log2(
|
|
684
|
-
` You can connect later with: cortex-mcp connect ${mcp.name}`
|
|
685
|
-
);
|
|
686
|
-
}
|
|
687
|
-
log2("");
|
|
688
|
-
}
|
|
689
|
-
rl2.close();
|
|
690
2046
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
rl2.question(
|
|
704
|
-
" Enter your company email address: ",
|
|
705
|
-
(a) => resolve(a.trim())
|
|
706
|
-
);
|
|
2047
|
+
|
|
2048
|
+
// src/cli/setup.ts
|
|
2049
|
+
function log(msg) {
|
|
2050
|
+
process.stderr.write(msg + "\n");
|
|
2051
|
+
}
|
|
2052
|
+
async function runSetup() {
|
|
2053
|
+
log("");
|
|
2054
|
+
log(" Cortex MCP Setup");
|
|
2055
|
+
log(" Starting setup wizard...");
|
|
2056
|
+
log("");
|
|
2057
|
+
const { port, close, waitForCompletion } = await startWizardServer({
|
|
2058
|
+
serverUrl: DEFAULT_SERVER_URL
|
|
707
2059
|
});
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
2060
|
+
const url = `http://localhost:${port}`;
|
|
2061
|
+
log(` Setup wizard running at ${url}`);
|
|
2062
|
+
log("");
|
|
2063
|
+
await openBrowser(url);
|
|
2064
|
+
log(" If the browser didn't open, visit:");
|
|
2065
|
+
log(` ${url}`);
|
|
2066
|
+
log("");
|
|
2067
|
+
log(" Waiting for setup to complete (press Ctrl+C to cancel)...");
|
|
2068
|
+
const cleanup = async () => {
|
|
2069
|
+
log("\n Setup cancelled.");
|
|
2070
|
+
await close();
|
|
2071
|
+
process.exit(0);
|
|
2072
|
+
};
|
|
2073
|
+
process.on("SIGINT", cleanup);
|
|
2074
|
+
process.on("SIGTERM", cleanup);
|
|
2075
|
+
await waitForCompletion();
|
|
2076
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
2077
|
+
await close();
|
|
2078
|
+
log("");
|
|
2079
|
+
log(" Setup complete! Restart your AI clients to see the new tools.");
|
|
2080
|
+
log("");
|
|
714
2081
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
2082
|
+
|
|
2083
|
+
// src/cli/configure.ts
|
|
2084
|
+
var VALID_CLIENTS = {
|
|
2085
|
+
"claude-desktop": "claude-desktop",
|
|
2086
|
+
"claude-code": "claude-code",
|
|
2087
|
+
cursor: "cursor",
|
|
2088
|
+
codex: "codex",
|
|
2089
|
+
stdio: "stdio"
|
|
2090
|
+
};
|
|
2091
|
+
async function runConfigure(options) {
|
|
2092
|
+
const key = options.key || getEffectiveApiKey();
|
|
2093
|
+
const { client } = options;
|
|
2094
|
+
const clientType = VALID_CLIENTS[client];
|
|
2095
|
+
if (!clientType) {
|
|
2096
|
+
console.error(
|
|
2097
|
+
`Unknown client: ${client}. Valid options: ${Object.keys(VALID_CLIENTS).join(", ")}`
|
|
2098
|
+
);
|
|
2099
|
+
process.exit(1);
|
|
723
2100
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
if (result.status === "needs_hint") {
|
|
732
|
-
let hintEmail;
|
|
733
|
-
if (emailHint) {
|
|
734
|
-
hintEmail = emailHint;
|
|
735
|
-
} else {
|
|
736
|
-
hintEmail = await promptForEmailHint(result.ssoEmail);
|
|
737
|
-
}
|
|
738
|
-
if (!hintEmail) {
|
|
739
|
-
process.exit(1);
|
|
740
|
-
}
|
|
741
|
-
log2("");
|
|
742
|
-
log2(" Retrying with your company email. You'll need to complete SSO again.");
|
|
743
|
-
log2("");
|
|
744
|
-
result = await loginWithSSO(serverUrl, hintEmail);
|
|
745
|
-
if (result.status === "needs_hint") {
|
|
746
|
-
log2("");
|
|
747
|
-
log2(" The provided email was also not found in the employee directory.");
|
|
748
|
-
log2(" Please verify your email and contact IT if the issue persists.");
|
|
2101
|
+
let selectedMcps;
|
|
2102
|
+
if (options.mcps) {
|
|
2103
|
+
selectedMcps = options.mcps.split(",").map((s) => s.trim()).filter((s) => MCP_NAMES.includes(s));
|
|
2104
|
+
if (selectedMcps.length === 0) {
|
|
2105
|
+
console.error(
|
|
2106
|
+
`No valid MCPs. Available: ${MCP_NAMES.join(", ")}`
|
|
2107
|
+
);
|
|
749
2108
|
process.exit(1);
|
|
750
2109
|
}
|
|
2110
|
+
} else {
|
|
2111
|
+
selectedMcps = [...DEFAULT_MCPS];
|
|
751
2112
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
log2(" Personal API key saved.");
|
|
764
|
-
const existingConfig = readConfig();
|
|
765
|
-
if (existingConfig) {
|
|
766
|
-
existingConfig.apiKey = apiKey;
|
|
767
|
-
if (existingConfig.configuredClients?.length > 0) {
|
|
768
|
-
log2("");
|
|
769
|
-
log2(" Updating configured clients:");
|
|
770
|
-
for (const clientType of existingConfig.configuredClients) {
|
|
771
|
-
try {
|
|
772
|
-
configureClient(
|
|
773
|
-
clientType,
|
|
774
|
-
existingConfig.server || DEFAULT_SERVER_URL,
|
|
775
|
-
apiKey,
|
|
776
|
-
existingConfig.mcps
|
|
777
|
-
);
|
|
778
|
-
log2(` ${clientType}: updated`);
|
|
779
|
-
} catch {
|
|
780
|
-
log2(` ${clientType}: failed to update`);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
writeConfig(existingConfig);
|
|
785
|
-
}
|
|
786
|
-
await showConnectionsAndAutoConnect(apiKey, serverUrl);
|
|
787
|
-
log2(" Done! Restart your AI clients to apply.");
|
|
788
|
-
log2("");
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// src/cli/setup.ts
|
|
792
|
-
var rl = readline2.createInterface({
|
|
793
|
-
input: process.stdin,
|
|
794
|
-
output: process.stderr
|
|
795
|
-
});
|
|
796
|
-
function ask(question) {
|
|
797
|
-
return new Promise((resolve) => {
|
|
798
|
-
rl.question(question, (answer) => resolve(answer.trim()));
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
|
-
function log3(msg) {
|
|
802
|
-
process.stderr.write(msg + "\n");
|
|
803
|
-
}
|
|
804
|
-
async function validateApiKey(apiKey, serverUrl) {
|
|
805
|
-
try {
|
|
806
|
-
const resp = await fetch(`${serverUrl}/api/v1/oauth/connections`, {
|
|
807
|
-
headers: { "x-api-key": apiKey }
|
|
808
|
-
});
|
|
809
|
-
if (resp.ok) {
|
|
810
|
-
return "valid";
|
|
811
|
-
}
|
|
812
|
-
if (resp.status === 401 || resp.status === 403) {
|
|
813
|
-
return "invalid";
|
|
814
|
-
}
|
|
815
|
-
return "unreachable";
|
|
816
|
-
} catch {
|
|
817
|
-
return "unreachable";
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
async function runSetup(emailHint) {
|
|
821
|
-
let apiKey = getEffectiveApiKey();
|
|
822
|
-
let creds = readCredentials();
|
|
823
|
-
log3("");
|
|
824
|
-
log3(" Cortex MCP Setup");
|
|
825
|
-
log3(" Connect your AI tools to Asana, GitHub, Monday.com, Salesforce, M365, Vercel & Supabase");
|
|
826
|
-
log3("");
|
|
827
|
-
log3(" Step 1: Select MCPs");
|
|
828
|
-
log3(" Available MCPs (all enabled by default):\n");
|
|
829
|
-
for (const mcp of AVAILABLE_MCPS) {
|
|
830
|
-
log3(` - ${mcp.displayName.padEnd(15)} ${mcp.description}`);
|
|
831
|
-
}
|
|
832
|
-
log3("");
|
|
833
|
-
const mcpInput = await ask(
|
|
834
|
-
` MCPs to enable (comma-separated, or press Enter for all): `
|
|
835
|
-
);
|
|
836
|
-
let selectedMcps;
|
|
837
|
-
if (!mcpInput) {
|
|
838
|
-
selectedMcps = [...DEFAULT_MCPS];
|
|
839
|
-
log3(" All MCPs enabled.\n");
|
|
840
|
-
} else {
|
|
841
|
-
selectedMcps = mcpInput.split(",").map((s) => s.trim().toLowerCase()).filter(
|
|
842
|
-
(s) => AVAILABLE_MCPS.some(
|
|
843
|
-
(m) => m.name === s || m.displayName.toLowerCase() === s
|
|
844
|
-
)
|
|
845
|
-
);
|
|
846
|
-
selectedMcps = selectedMcps.map((s) => {
|
|
847
|
-
const found = AVAILABLE_MCPS.find(
|
|
848
|
-
(m) => m.displayName.toLowerCase() === s
|
|
849
|
-
);
|
|
850
|
-
return found ? found.name : s;
|
|
851
|
-
});
|
|
852
|
-
if (selectedMcps.length === 0) {
|
|
853
|
-
selectedMcps = [...DEFAULT_MCPS];
|
|
854
|
-
log3(" No valid MCPs recognized. Enabling all.\n");
|
|
855
|
-
} else {
|
|
856
|
-
log3(` Enabled: ${selectedMcps.join(", ")}
|
|
857
|
-
`);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
log3(" Step 2: Configure AI Clients\n");
|
|
861
|
-
const clients = detectClients();
|
|
862
|
-
const configuredClients = [];
|
|
863
|
-
for (const client of clients) {
|
|
864
|
-
if (!client.detected) {
|
|
865
|
-
log3(` ${client.name}: not detected, skipping`);
|
|
866
|
-
continue;
|
|
867
|
-
}
|
|
868
|
-
const answer = await ask(` Configure ${client.name}? (Y/n): `);
|
|
869
|
-
if (answer.toLowerCase() === "n") {
|
|
870
|
-
log3(` Skipping ${client.name}`);
|
|
871
|
-
continue;
|
|
872
|
-
}
|
|
873
|
-
try {
|
|
874
|
-
if (client.type === "claude-desktop") {
|
|
875
|
-
configureClaudeDesktop(DEFAULT_SERVER_URL, apiKey, selectedMcps);
|
|
876
|
-
log3(` ${client.name}: configured`);
|
|
877
|
-
configuredClients.push(client.type);
|
|
878
|
-
} else if (client.type === "claude-code") {
|
|
879
|
-
configureClaudeCode(DEFAULT_SERVER_URL, apiKey, selectedMcps);
|
|
880
|
-
log3(` ${client.name}: configured`);
|
|
881
|
-
configuredClients.push(client.type);
|
|
882
|
-
} else if (client.type === "cursor") {
|
|
883
|
-
configureCursor(DEFAULT_SERVER_URL, apiKey, selectedMcps);
|
|
884
|
-
log3(` ${client.name}: configured`);
|
|
885
|
-
configuredClients.push(client.type);
|
|
886
|
-
} else if (client.type === "codex") {
|
|
887
|
-
configureCodex(DEFAULT_SERVER_URL, apiKey, selectedMcps);
|
|
888
|
-
log3(` ${client.name}: configured`);
|
|
889
|
-
configuredClients.push(client.type);
|
|
890
|
-
}
|
|
891
|
-
} catch (err) {
|
|
892
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
893
|
-
log3(` ${client.name}: failed \u2014 ${msg}`);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
log3("");
|
|
897
|
-
const stdioAnswer = await ask(
|
|
898
|
-
" Do you also need a config snippet for OpenClaw or other stdio clients? (y/N): "
|
|
899
|
-
);
|
|
900
|
-
if (stdioAnswer.toLowerCase() === "y") {
|
|
901
|
-
log3("\n Add this to your client's MCP config:\n");
|
|
902
|
-
log3(generateStdioSnippet(apiKey));
|
|
903
|
-
log3("");
|
|
904
|
-
configuredClients.push("stdio");
|
|
905
|
-
}
|
|
906
|
-
const config = createConfig(apiKey, selectedMcps);
|
|
907
|
-
config.configuredClients = configuredClients;
|
|
908
|
-
writeConfig(config);
|
|
909
|
-
log3("");
|
|
910
|
-
log3(" Step 3: Sign In\n");
|
|
911
|
-
let shouldLogin = !creds;
|
|
912
|
-
if (creds) {
|
|
913
|
-
log3(` Already signed in as ${creds.name || creds.email}`);
|
|
914
|
-
const validation = await validateApiKey(creds.apiKey, DEFAULT_SERVER_URL);
|
|
915
|
-
if (validation === "invalid") {
|
|
916
|
-
log3(" Saved session is no longer valid. Re-authenticating...");
|
|
917
|
-
deleteCredentials();
|
|
918
|
-
creds = null;
|
|
919
|
-
shouldLogin = true;
|
|
920
|
-
} else {
|
|
921
|
-
apiKey = creds.apiKey;
|
|
922
|
-
if (validation === "unreachable") {
|
|
923
|
-
log3(" Warning: Could not validate saved session. Continuing with existing credentials.");
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
if (shouldLogin) {
|
|
928
|
-
log3(" Sign in with your company Okta SSO account.");
|
|
929
|
-
log3("");
|
|
930
|
-
rl.close();
|
|
931
|
-
let result = await loginWithSSO(DEFAULT_SERVER_URL, emailHint);
|
|
932
|
-
if (result.status === "needs_hint") {
|
|
933
|
-
let hintEmail;
|
|
934
|
-
if (emailHint) {
|
|
935
|
-
hintEmail = emailHint;
|
|
936
|
-
} else {
|
|
937
|
-
log3("");
|
|
938
|
-
log3(
|
|
939
|
-
` Your SSO email (${result.ssoEmail}) was not found in the employee directory.`
|
|
940
|
-
);
|
|
941
|
-
log3(
|
|
942
|
-
" This can happen if your Okta account uses a different email than your company directory."
|
|
943
|
-
);
|
|
944
|
-
log3("");
|
|
945
|
-
const hintRl = readline2.createInterface({
|
|
946
|
-
input: process.stdin,
|
|
947
|
-
output: process.stderr
|
|
948
|
-
});
|
|
949
|
-
hintEmail = await new Promise((resolve) => {
|
|
950
|
-
hintRl.question(
|
|
951
|
-
" Enter your company email address: ",
|
|
952
|
-
(a) => resolve(a.trim())
|
|
953
|
-
);
|
|
954
|
-
});
|
|
955
|
-
hintRl.close();
|
|
956
|
-
if (!hintEmail || !hintEmail.includes("@")) {
|
|
957
|
-
log3(" Invalid email. Run 'cortex-mcp login --email you@company.com' to try again.");
|
|
958
|
-
process.exit(1);
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
log3("");
|
|
962
|
-
log3(" Retrying with your company email. You'll need to complete SSO again.");
|
|
963
|
-
log3("");
|
|
964
|
-
result = await loginWithSSO(DEFAULT_SERVER_URL, hintEmail);
|
|
965
|
-
if (result.status === "needs_hint") {
|
|
966
|
-
log3("");
|
|
967
|
-
log3(" The provided email was also not found in the employee directory.");
|
|
968
|
-
log3(" Please verify your email and contact IT if the issue persists.");
|
|
969
|
-
process.exit(1);
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
if (result.status !== "success") {
|
|
973
|
-
log3(" Login failed. Run 'cortex-mcp login' to try again.");
|
|
974
|
-
process.exit(1);
|
|
975
|
-
}
|
|
976
|
-
const { apiKey: newKey, email, name } = result;
|
|
977
|
-
apiKey = newKey;
|
|
978
|
-
writeCredentials({
|
|
979
|
-
apiKey: newKey,
|
|
980
|
-
email,
|
|
981
|
-
name,
|
|
982
|
-
authenticatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
983
|
-
});
|
|
984
|
-
log3(` Authenticated as ${name || email}${name ? ` (${email})` : ""}`);
|
|
985
|
-
log3(" Personal API key saved.");
|
|
986
|
-
if (configuredClients.length > 0) {
|
|
987
|
-
log3("");
|
|
988
|
-
log3(" Updating clients with personal key:");
|
|
989
|
-
for (const clientType of configuredClients) {
|
|
990
|
-
try {
|
|
991
|
-
if (clientType === "claude-desktop") {
|
|
992
|
-
configureClaudeDesktop(DEFAULT_SERVER_URL, newKey, selectedMcps);
|
|
993
|
-
} else if (clientType === "claude-code") {
|
|
994
|
-
configureClaudeCode(DEFAULT_SERVER_URL, newKey, selectedMcps);
|
|
995
|
-
} else if (clientType === "cursor") {
|
|
996
|
-
configureCursor(DEFAULT_SERVER_URL, newKey, selectedMcps);
|
|
997
|
-
} else if (clientType === "codex") {
|
|
998
|
-
configureCodex(DEFAULT_SERVER_URL, newKey, selectedMcps);
|
|
999
|
-
}
|
|
1000
|
-
log3(` ${clientType}: updated`);
|
|
1001
|
-
} catch {
|
|
1002
|
-
log3(` ${clientType}: failed to update`);
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
config.apiKey = newKey;
|
|
1007
|
-
writeConfig(config);
|
|
1008
|
-
creds = readCredentials();
|
|
1009
|
-
}
|
|
1010
|
-
log3("");
|
|
1011
|
-
log3(" Step 4: Connect Accounts (optional)\n");
|
|
1012
|
-
await showConnectionsAndAutoConnect(apiKey, DEFAULT_SERVER_URL);
|
|
1013
|
-
log3(" Setup complete! Restart your AI clients to see the new tools.");
|
|
1014
|
-
log3(` Config saved to ~/.cortex-mcp/config.json`);
|
|
1015
|
-
log3("");
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
// src/cli/configure.ts
|
|
1019
|
-
var VALID_CLIENTS = {
|
|
1020
|
-
"claude-desktop": "claude-desktop",
|
|
1021
|
-
"claude-code": "claude-code",
|
|
1022
|
-
cursor: "cursor",
|
|
1023
|
-
codex: "codex",
|
|
1024
|
-
stdio: "stdio"
|
|
1025
|
-
};
|
|
1026
|
-
async function runConfigure(options) {
|
|
1027
|
-
const key = options.key || getEffectiveApiKey();
|
|
1028
|
-
const { client } = options;
|
|
1029
|
-
const clientType = VALID_CLIENTS[client];
|
|
1030
|
-
if (!clientType) {
|
|
1031
|
-
console.error(
|
|
1032
|
-
`Unknown client: ${client}. Valid options: ${Object.keys(VALID_CLIENTS).join(", ")}`
|
|
1033
|
-
);
|
|
1034
|
-
process.exit(1);
|
|
1035
|
-
}
|
|
1036
|
-
let selectedMcps;
|
|
1037
|
-
if (options.mcps) {
|
|
1038
|
-
selectedMcps = options.mcps.split(",").map((s) => s.trim()).filter((s) => MCP_NAMES.includes(s));
|
|
1039
|
-
if (selectedMcps.length === 0) {
|
|
1040
|
-
console.error(
|
|
1041
|
-
`No valid MCPs. Available: ${MCP_NAMES.join(", ")}`
|
|
1042
|
-
);
|
|
1043
|
-
process.exit(1);
|
|
1044
|
-
}
|
|
1045
|
-
} else {
|
|
1046
|
-
selectedMcps = [...DEFAULT_MCPS];
|
|
1047
|
-
}
|
|
1048
|
-
try {
|
|
1049
|
-
const result = configureClient(
|
|
1050
|
-
clientType,
|
|
1051
|
-
DEFAULT_SERVER_URL,
|
|
1052
|
-
key,
|
|
1053
|
-
selectedMcps
|
|
1054
|
-
);
|
|
1055
|
-
console.log(result);
|
|
1056
|
-
} catch (err) {
|
|
1057
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1058
|
-
console.error(`Failed to configure ${client}: ${msg}`);
|
|
2113
|
+
try {
|
|
2114
|
+
const result = configureClient(
|
|
2115
|
+
clientType,
|
|
2116
|
+
DEFAULT_SERVER_URL,
|
|
2117
|
+
key,
|
|
2118
|
+
selectedMcps
|
|
2119
|
+
);
|
|
2120
|
+
console.log(result);
|
|
2121
|
+
} catch (err) {
|
|
2122
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2123
|
+
console.error(`Failed to configure ${client}: ${msg}`);
|
|
1059
2124
|
process.exit(1);
|
|
1060
2125
|
}
|
|
1061
2126
|
const config = createConfig(key, selectedMcps);
|
|
@@ -1233,275 +2298,692 @@ function overrideUploadToolSchema(tool) {
|
|
|
1233
2298
|
}
|
|
1234
2299
|
}
|
|
1235
2300
|
}
|
|
1236
|
-
};
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
return tool;
|
|
2304
|
+
}
|
|
2305
|
+
async function handleLocalFileUpload(cortex, toolName, args) {
|
|
2306
|
+
const filePath = args.file_path;
|
|
2307
|
+
let fileSize;
|
|
2308
|
+
try {
|
|
2309
|
+
fileSize = statSync(filePath).size;
|
|
2310
|
+
} catch {
|
|
2311
|
+
return {
|
|
2312
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2313
|
+
success: false,
|
|
2314
|
+
error: `File not found: ${filePath}`
|
|
2315
|
+
}) }],
|
|
2316
|
+
isError: true
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
if (fileSize <= INLINE_UPLOAD_MAX) {
|
|
2320
|
+
const fileBuffer2 = readFileSync4(filePath);
|
|
2321
|
+
const base64Content = fileBuffer2.toString("base64");
|
|
2322
|
+
const forwardArgs = { ...args, content: base64Content };
|
|
2323
|
+
delete forwardArgs.file_path;
|
|
2324
|
+
console.error(
|
|
2325
|
+
`[cortex-mcp] ${toolName}: reading local file (${(fileSize / 1024).toFixed(1)}KB), forwarding as base64`
|
|
2326
|
+
);
|
|
2327
|
+
const response = await cortex.callTool(toolName, forwardArgs);
|
|
2328
|
+
if (response.error) {
|
|
2329
|
+
return {
|
|
2330
|
+
content: [{ type: "text", text: response.error.message }],
|
|
2331
|
+
isError: true
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
return response.result;
|
|
2335
|
+
}
|
|
2336
|
+
console.error(
|
|
2337
|
+
`[cortex-mcp] ${toolName}: large file (${(fileSize / 1024 / 1024).toFixed(1)}MB), using upload session`
|
|
2338
|
+
);
|
|
2339
|
+
const prefix = toolName.includes("__") ? toolName.split("__")[0] + "__" : "";
|
|
2340
|
+
const sessionResponse = await cortex.callTool(`${prefix}create_upload_session`, {
|
|
2341
|
+
path: args.path
|
|
2342
|
+
});
|
|
2343
|
+
if (sessionResponse.error) {
|
|
2344
|
+
return {
|
|
2345
|
+
content: [{ type: "text", text: sessionResponse.error.message }],
|
|
2346
|
+
isError: true
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
const sessionResult = sessionResponse.result;
|
|
2350
|
+
let sessionData;
|
|
2351
|
+
try {
|
|
2352
|
+
sessionData = JSON.parse(sessionResult.content[0].text);
|
|
2353
|
+
} catch {
|
|
2354
|
+
return {
|
|
2355
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2356
|
+
success: false,
|
|
2357
|
+
error: "Failed to parse upload session response from backend"
|
|
2358
|
+
}) }],
|
|
2359
|
+
isError: true
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
const uploadUrl = sessionData.uploadUrl;
|
|
2363
|
+
if (!uploadUrl) {
|
|
2364
|
+
return {
|
|
2365
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2366
|
+
success: false,
|
|
2367
|
+
error: "Backend did not return an uploadUrl"
|
|
2368
|
+
}) }],
|
|
2369
|
+
isError: true
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
const fileBuffer = readFileSync4(filePath);
|
|
2373
|
+
const chunkSize = 2.5 * 1024 * 1024;
|
|
2374
|
+
const total = fileBuffer.length;
|
|
2375
|
+
let driveItem = {};
|
|
2376
|
+
for (let start = 0; start < total; start += chunkSize) {
|
|
2377
|
+
const end = Math.min(start + chunkSize, total);
|
|
2378
|
+
const chunk = fileBuffer.subarray(start, end);
|
|
2379
|
+
const chunkBase64 = Buffer.from(chunk).toString("base64");
|
|
2380
|
+
console.error(
|
|
2381
|
+
`[cortex-mcp] Uploading chunk ${start}-${end - 1}/${total} via backend relay`
|
|
2382
|
+
);
|
|
2383
|
+
const chunkResponse = await cortex.callTool(`${prefix}upload_file_chunk`, {
|
|
2384
|
+
upload_url: uploadUrl,
|
|
2385
|
+
chunk: chunkBase64,
|
|
2386
|
+
range_start: start,
|
|
2387
|
+
range_end: end - 1,
|
|
2388
|
+
total_size: total
|
|
2389
|
+
});
|
|
2390
|
+
if (chunkResponse.error) {
|
|
2391
|
+
return {
|
|
2392
|
+
content: [{ type: "text", text: chunkResponse.error.message }],
|
|
2393
|
+
isError: true
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
const chunkResult = chunkResponse.result;
|
|
2397
|
+
let chunkData;
|
|
2398
|
+
try {
|
|
2399
|
+
chunkData = JSON.parse(chunkResult.content[0].text);
|
|
2400
|
+
} catch {
|
|
2401
|
+
return {
|
|
2402
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2403
|
+
success: false,
|
|
2404
|
+
error: "Failed to parse chunk upload response from backend"
|
|
2405
|
+
}) }],
|
|
2406
|
+
isError: true
|
|
2407
|
+
};
|
|
2408
|
+
}
|
|
2409
|
+
if (!chunkData.success) {
|
|
2410
|
+
return {
|
|
2411
|
+
content: [{ type: "text", text: JSON.stringify(chunkData) }],
|
|
2412
|
+
isError: true
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
if (chunkData.status === 200 || chunkData.status === 201) {
|
|
2416
|
+
driveItem = chunkData.data ?? {};
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
return {
|
|
2420
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2421
|
+
success: true,
|
|
2422
|
+
file: driveItem,
|
|
2423
|
+
message: `Uploaded '${args.path}' (${(fileSize / 1024 / 1024).toFixed(1)}MB) via upload session`
|
|
2424
|
+
}) }],
|
|
2425
|
+
isError: false
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
async function startStdioServer(options) {
|
|
2429
|
+
const { serverUrl, apiKey, endpoint = "cortex" } = options;
|
|
2430
|
+
const cortex = new CortexHttpClient(serverUrl, apiKey, endpoint);
|
|
2431
|
+
const server = new Server(
|
|
2432
|
+
{ name: "cortex-mcp", version: "1.0.0" },
|
|
2433
|
+
{ capabilities: { tools: { listChanged: false } } }
|
|
2434
|
+
);
|
|
2435
|
+
let initialized = false;
|
|
2436
|
+
async function ensureInitialized() {
|
|
2437
|
+
if (initialized) return;
|
|
2438
|
+
try {
|
|
2439
|
+
console.error("[cortex-mcp] Initializing backend session...");
|
|
2440
|
+
await cortex.initialize();
|
|
2441
|
+
console.error("[cortex-mcp] Backend session established.");
|
|
2442
|
+
} catch (err) {
|
|
2443
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2444
|
+
console.error(`[cortex-mcp] Backend initialization failed: ${msg}`);
|
|
2445
|
+
}
|
|
2446
|
+
initialized = true;
|
|
2447
|
+
}
|
|
2448
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2449
|
+
await ensureInitialized();
|
|
2450
|
+
const response = await cortex.listTools();
|
|
2451
|
+
if (response.error) {
|
|
2452
|
+
throw new Error(response.error.message);
|
|
2453
|
+
}
|
|
2454
|
+
const result = response.result;
|
|
2455
|
+
const tools = (result.tools || []).map(overrideUploadToolSchema);
|
|
2456
|
+
return { tools };
|
|
2457
|
+
});
|
|
2458
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2459
|
+
await ensureInitialized();
|
|
2460
|
+
const { name, arguments: args } = request.params;
|
|
2461
|
+
const typedArgs = args ?? {};
|
|
2462
|
+
const baseName = name.includes("__") ? name.split("__").pop() : name;
|
|
2463
|
+
if (UPLOAD_TOOLS.has(baseName) && typedArgs.file_path) {
|
|
2464
|
+
return handleLocalFileUpload(cortex, name, typedArgs);
|
|
2465
|
+
}
|
|
2466
|
+
const response = await cortex.callTool(name, typedArgs);
|
|
2467
|
+
if (response.error) {
|
|
2468
|
+
return {
|
|
2469
|
+
content: [{ type: "text", text: response.error.message }],
|
|
2470
|
+
isError: true
|
|
2471
|
+
};
|
|
2472
|
+
}
|
|
2473
|
+
const result = response.result;
|
|
2474
|
+
return result;
|
|
2475
|
+
});
|
|
2476
|
+
console.error("[cortex-mcp] Starting stdio server...");
|
|
2477
|
+
const transport = new StdioServerTransport();
|
|
2478
|
+
await server.connect(transport);
|
|
2479
|
+
console.error("[cortex-mcp] Stdio server connected.");
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
// src/cli/serve.ts
|
|
2483
|
+
async function runServe() {
|
|
2484
|
+
const config = readConfig();
|
|
2485
|
+
const serverUrl = config?.server || DEFAULT_SERVER_URL;
|
|
2486
|
+
const apiKey = getEffectiveApiKey();
|
|
2487
|
+
if (apiKey === DEFAULT_API_KEY) {
|
|
2488
|
+
process.stderr.write(
|
|
2489
|
+
"Warning: Using shared API key. Personal MCPs (M365, Slack, etc.) will not have access to your account.\nRun: npx @danainnovations/cortex-mcp@latest login\n\n"
|
|
2490
|
+
);
|
|
2491
|
+
}
|
|
2492
|
+
await startStdioServer({ serverUrl, apiKey });
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
// src/cli/status.ts
|
|
2496
|
+
function runStatus() {
|
|
2497
|
+
const config = readConfig();
|
|
2498
|
+
if (!config) {
|
|
2499
|
+
console.log("No configuration found. Run: npx @danainnovations/cortex-mcp setup");
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
const maskedKey = config.apiKey.slice(0, 8) + "****" + config.apiKey.slice(-4);
|
|
2503
|
+
const creds = readCredentials();
|
|
2504
|
+
console.log("");
|
|
2505
|
+
console.log(" Cortex MCP Status");
|
|
2506
|
+
console.log(" -----------------");
|
|
2507
|
+
if (creds) {
|
|
2508
|
+
console.log(` User: ${creds.name || creds.email}`);
|
|
2509
|
+
console.log(` Email: ${creds.email}`);
|
|
2510
|
+
} else {
|
|
2511
|
+
console.log(" User: not logged in (using shared key)");
|
|
2512
|
+
}
|
|
2513
|
+
console.log(` Config: ${getConfigPath()}`);
|
|
2514
|
+
console.log(` Server: ${config.server}`);
|
|
2515
|
+
console.log(` API Key: ${maskedKey}`);
|
|
2516
|
+
console.log("");
|
|
2517
|
+
console.log(" Enabled MCPs:");
|
|
2518
|
+
for (const mcpName of config.mcps) {
|
|
2519
|
+
const mcp = AVAILABLE_MCPS.find((m) => m.name === mcpName);
|
|
2520
|
+
const display = mcp ? mcp.displayName : mcpName;
|
|
2521
|
+
console.log(` ${display.padEnd(15)} ${config.server}/mcp/${mcpName}`);
|
|
2522
|
+
}
|
|
2523
|
+
console.log("");
|
|
2524
|
+
console.log(" Configured Clients:");
|
|
2525
|
+
if (config.configuredClients.length === 0) {
|
|
2526
|
+
console.log(" (none)");
|
|
2527
|
+
} else {
|
|
2528
|
+
for (const client of config.configuredClients) {
|
|
2529
|
+
console.log(` ${client}`);
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
console.log("");
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
// src/cli/reset.ts
|
|
2536
|
+
import { existsSync as existsSync4, unlinkSync as unlinkSync2, rmdirSync } from "fs";
|
|
2537
|
+
function runReset() {
|
|
2538
|
+
console.log("");
|
|
2539
|
+
console.log(" Resetting Cortex MCP configuration...");
|
|
2540
|
+
const desktopReset = resetClaudeDesktop();
|
|
2541
|
+
console.log(
|
|
2542
|
+
` Claude Desktop: ${desktopReset ? "entries removed" : "no entries found"}`
|
|
2543
|
+
);
|
|
2544
|
+
const codeReset = resetClaudeCode();
|
|
2545
|
+
console.log(
|
|
2546
|
+
` Claude Code: ${codeReset ? "entries removed" : "no entries found"}`
|
|
2547
|
+
);
|
|
2548
|
+
const cursorReset = resetCursor();
|
|
2549
|
+
console.log(
|
|
2550
|
+
` Cursor: ${cursorReset ? "entries removed" : "no entries found"}`
|
|
2551
|
+
);
|
|
2552
|
+
const codexReset = resetCodex();
|
|
2553
|
+
console.log(
|
|
2554
|
+
` Codex: ${codexReset ? "entries removed" : "no entries found"}`
|
|
2555
|
+
);
|
|
2556
|
+
const configPath = getConfigPath();
|
|
2557
|
+
if (existsSync4(configPath)) {
|
|
2558
|
+
unlinkSync2(configPath);
|
|
2559
|
+
console.log(` Config file: removed`);
|
|
2560
|
+
}
|
|
2561
|
+
const configDir = getConfigDir();
|
|
2562
|
+
try {
|
|
2563
|
+
rmdirSync(configDir);
|
|
2564
|
+
} catch {
|
|
2565
|
+
}
|
|
2566
|
+
console.log("");
|
|
2567
|
+
console.log(" Done. Restart your AI clients to apply changes.");
|
|
2568
|
+
console.log(" Note: Login credentials are preserved. Run 'cortex-mcp logout' to sign out.");
|
|
2569
|
+
console.log("");
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
// src/cli/login.ts
|
|
2573
|
+
import * as readline from "readline";
|
|
2574
|
+
|
|
2575
|
+
// src/cli/connect.ts
|
|
2576
|
+
function log2(msg) {
|
|
2577
|
+
process.stderr.write(msg + "\n");
|
|
2578
|
+
}
|
|
2579
|
+
function sleep(ms) {
|
|
2580
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2581
|
+
}
|
|
2582
|
+
function waitForEnter() {
|
|
2583
|
+
return new Promise((resolve) => {
|
|
2584
|
+
process.stdin.setRawMode?.(false);
|
|
2585
|
+
process.stdin.resume();
|
|
2586
|
+
process.stdin.once("data", () => {
|
|
2587
|
+
process.stdin.pause();
|
|
2588
|
+
resolve();
|
|
2589
|
+
});
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
async function connectProvider(providerName, displayName, apiKey, serverUrl, installUrl) {
|
|
2593
|
+
let sessionId;
|
|
2594
|
+
let authUrl;
|
|
2595
|
+
let expiresIn;
|
|
2596
|
+
try {
|
|
2597
|
+
const resp = await fetch(
|
|
2598
|
+
`${serverUrl}/api/v1/oauth/connect/${providerName}/initiate`,
|
|
2599
|
+
{
|
|
2600
|
+
method: "POST",
|
|
2601
|
+
headers: {
|
|
2602
|
+
"Content-Type": "application/json",
|
|
2603
|
+
"x-api-key": apiKey
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
);
|
|
2607
|
+
if (!resp.ok) {
|
|
2608
|
+
if (resp.status === 401) {
|
|
2609
|
+
log2(" Error: Your session has expired. Run 'cortex-mcp login' to re-authenticate.");
|
|
2610
|
+
return false;
|
|
2611
|
+
}
|
|
2612
|
+
const err = await resp.json().catch(() => ({}));
|
|
2613
|
+
log2(` Error: ${err.detail || `HTTP ${resp.status}`}`);
|
|
2614
|
+
return false;
|
|
2615
|
+
}
|
|
2616
|
+
const data = await resp.json();
|
|
2617
|
+
sessionId = data.session_id;
|
|
2618
|
+
authUrl = data.authorization_url;
|
|
2619
|
+
expiresIn = data.expires_in;
|
|
2620
|
+
} catch (err) {
|
|
2621
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2622
|
+
log2(` Error: Could not reach Cortex server at ${serverUrl}`);
|
|
2623
|
+
log2(` ${msg}`);
|
|
2624
|
+
return false;
|
|
2625
|
+
}
|
|
2626
|
+
if (installUrl) {
|
|
2627
|
+
log2(` First, install the ${displayName} app on your account...`);
|
|
2628
|
+
await openBrowser(installUrl);
|
|
2629
|
+
log2(" If the browser didn't open, visit:");
|
|
2630
|
+
log2(` ${installUrl}`);
|
|
2631
|
+
log2("");
|
|
2632
|
+
log2(" Press Enter once the app is installed...");
|
|
2633
|
+
await waitForEnter();
|
|
2634
|
+
log2("");
|
|
2635
|
+
}
|
|
2636
|
+
log2(` Opening browser for ${displayName} authorization...`);
|
|
2637
|
+
await openBrowser(authUrl);
|
|
2638
|
+
log2(" If the browser didn't open, visit:");
|
|
2639
|
+
log2(` ${authUrl}`);
|
|
2640
|
+
log2("");
|
|
2641
|
+
const deadline = Date.now() + expiresIn * 1e3;
|
|
2642
|
+
process.stderr.write(" Waiting for authorization");
|
|
2643
|
+
while (Date.now() < deadline) {
|
|
2644
|
+
await sleep(3e3);
|
|
2645
|
+
try {
|
|
2646
|
+
const resp = await fetch(
|
|
2647
|
+
`${serverUrl}/api/v1/oauth/connect/poll/${sessionId}`,
|
|
2648
|
+
{
|
|
2649
|
+
headers: {
|
|
2650
|
+
"x-api-key": apiKey
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
);
|
|
2654
|
+
const result = await resp.json();
|
|
2655
|
+
if (result.status === "completed") {
|
|
2656
|
+
process.stderr.write("\n\n");
|
|
2657
|
+
const who = result.account_email || "unknown";
|
|
2658
|
+
log2(` Connected as ${who}`);
|
|
2659
|
+
log2(` ${displayName} tools will now use your personal account.`);
|
|
2660
|
+
log2("");
|
|
2661
|
+
return true;
|
|
2662
|
+
}
|
|
2663
|
+
if (result.status === "expired") {
|
|
2664
|
+
process.stderr.write("\n");
|
|
2665
|
+
log2(" Authorization session expired. Please try again.");
|
|
2666
|
+
return false;
|
|
2667
|
+
}
|
|
2668
|
+
if (result.status === "error") {
|
|
2669
|
+
process.stderr.write("\n");
|
|
2670
|
+
log2(
|
|
2671
|
+
` Connection failed: ${result.error_message || "unknown error"}`
|
|
2672
|
+
);
|
|
2673
|
+
return false;
|
|
2674
|
+
}
|
|
2675
|
+
process.stderr.write(".");
|
|
2676
|
+
} catch {
|
|
2677
|
+
process.stderr.write("!");
|
|
2678
|
+
}
|
|
1237
2679
|
}
|
|
1238
|
-
|
|
2680
|
+
process.stderr.write("\n");
|
|
2681
|
+
log2(" Timeout waiting for authorization. Please try again.");
|
|
2682
|
+
return false;
|
|
1239
2683
|
}
|
|
1240
|
-
async function
|
|
1241
|
-
const
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
success: false,
|
|
1249
|
-
error: `File not found: ${filePath}`
|
|
1250
|
-
}) }],
|
|
1251
|
-
isError: true
|
|
1252
|
-
};
|
|
2684
|
+
async function runConnect(provider) {
|
|
2685
|
+
const creds = readCredentials();
|
|
2686
|
+
if (!creds) {
|
|
2687
|
+
log2("");
|
|
2688
|
+
log2(" You must be logged in first.");
|
|
2689
|
+
log2(" Run: cortex-mcp login");
|
|
2690
|
+
log2("");
|
|
2691
|
+
process.exit(1);
|
|
1253
2692
|
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
);
|
|
1262
|
-
|
|
1263
|
-
if (response.error) {
|
|
1264
|
-
return {
|
|
1265
|
-
content: [{ type: "text", text: response.error.message }],
|
|
1266
|
-
isError: true
|
|
1267
|
-
};
|
|
1268
|
-
}
|
|
1269
|
-
return response.result;
|
|
2693
|
+
const mcp = AVAILABLE_MCPS.find(
|
|
2694
|
+
(m) => m.name === provider || m.displayName.toLowerCase() === provider.toLowerCase()
|
|
2695
|
+
);
|
|
2696
|
+
if (!mcp) {
|
|
2697
|
+
log2("");
|
|
2698
|
+
log2(` Unknown provider: ${provider}`);
|
|
2699
|
+
log2(` Available: ${AVAILABLE_MCPS.map((m) => m.name).join(", ")}`);
|
|
2700
|
+
log2("");
|
|
2701
|
+
process.exit(1);
|
|
1270
2702
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
2703
|
+
const config = readConfig();
|
|
2704
|
+
const serverUrl = config?.server || DEFAULT_SERVER_URL;
|
|
2705
|
+
log2("");
|
|
2706
|
+
log2(` Cortex MCP \u2014 Connect ${mcp.displayName}`);
|
|
2707
|
+
log2(` Link your personal ${mcp.displayName} account`);
|
|
2708
|
+
log2("");
|
|
2709
|
+
const success = await connectProvider(
|
|
2710
|
+
mcp.name,
|
|
2711
|
+
mcp.displayName,
|
|
2712
|
+
creds.apiKey,
|
|
2713
|
+
serverUrl,
|
|
2714
|
+
"installUrl" in mcp ? mcp.installUrl : void 0
|
|
1273
2715
|
);
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
path: args.path
|
|
1277
|
-
});
|
|
1278
|
-
if (sessionResponse.error) {
|
|
1279
|
-
return {
|
|
1280
|
-
content: [{ type: "text", text: sessionResponse.error.message }],
|
|
1281
|
-
isError: true
|
|
1282
|
-
};
|
|
2716
|
+
if (!success) {
|
|
2717
|
+
process.exit(1);
|
|
1283
2718
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
// src/cli/login.ts
|
|
2722
|
+
function log3(msg) {
|
|
2723
|
+
process.stderr.write(msg + "\n");
|
|
2724
|
+
}
|
|
2725
|
+
function sleep2(ms) {
|
|
2726
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2727
|
+
}
|
|
2728
|
+
async function loginWithSSO(serverUrl, emailHint) {
|
|
2729
|
+
let sessionId;
|
|
2730
|
+
let authUrl;
|
|
2731
|
+
let expiresIn;
|
|
1286
2732
|
try {
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
};
|
|
1296
|
-
}
|
|
1297
|
-
const uploadUrl = sessionData.uploadUrl;
|
|
1298
|
-
if (!uploadUrl) {
|
|
1299
|
-
return {
|
|
1300
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
1301
|
-
success: false,
|
|
1302
|
-
error: "Backend did not return an uploadUrl"
|
|
1303
|
-
}) }],
|
|
1304
|
-
isError: true
|
|
1305
|
-
};
|
|
1306
|
-
}
|
|
1307
|
-
const fileBuffer = readFileSync4(filePath);
|
|
1308
|
-
const chunkSize = 2.5 * 1024 * 1024;
|
|
1309
|
-
const total = fileBuffer.length;
|
|
1310
|
-
let driveItem = {};
|
|
1311
|
-
for (let start = 0; start < total; start += chunkSize) {
|
|
1312
|
-
const end = Math.min(start + chunkSize, total);
|
|
1313
|
-
const chunk = fileBuffer.subarray(start, end);
|
|
1314
|
-
const chunkBase64 = Buffer.from(chunk).toString("base64");
|
|
1315
|
-
console.error(
|
|
1316
|
-
`[cortex-mcp] Uploading chunk ${start}-${end - 1}/${total} via backend relay`
|
|
1317
|
-
);
|
|
1318
|
-
const chunkResponse = await cortex.callTool(`${prefix}upload_file_chunk`, {
|
|
1319
|
-
upload_url: uploadUrl,
|
|
1320
|
-
chunk: chunkBase64,
|
|
1321
|
-
range_start: start,
|
|
1322
|
-
range_end: end - 1,
|
|
1323
|
-
total_size: total
|
|
2733
|
+
const body = {};
|
|
2734
|
+
if (emailHint) {
|
|
2735
|
+
body.email_hint = emailHint;
|
|
2736
|
+
}
|
|
2737
|
+
const resp = await fetch(`${serverUrl}/api/v1/auth/employee/initiate`, {
|
|
2738
|
+
method: "POST",
|
|
2739
|
+
headers: { "Content-Type": "application/json" },
|
|
2740
|
+
body: JSON.stringify(body)
|
|
1324
2741
|
});
|
|
1325
|
-
if (
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
isError: true
|
|
1329
|
-
};
|
|
2742
|
+
if (!resp.ok) {
|
|
2743
|
+
log3(` Error: Failed to start authentication (HTTP ${resp.status})`);
|
|
2744
|
+
return { status: "failed" };
|
|
1330
2745
|
}
|
|
1331
|
-
const
|
|
1332
|
-
|
|
2746
|
+
const data = await resp.json();
|
|
2747
|
+
sessionId = data.session_id;
|
|
2748
|
+
authUrl = data.auth_url;
|
|
2749
|
+
expiresIn = data.expires_in;
|
|
2750
|
+
} catch (err) {
|
|
2751
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2752
|
+
log3(` Error: Could not reach Cortex server at ${serverUrl}`);
|
|
2753
|
+
log3(` ${msg}`);
|
|
2754
|
+
return { status: "failed" };
|
|
2755
|
+
}
|
|
2756
|
+
log3(" Opening browser for Okta SSO login...");
|
|
2757
|
+
await openBrowser(authUrl);
|
|
2758
|
+
log3(" If the browser didn't open, visit:");
|
|
2759
|
+
log3(` ${authUrl}`);
|
|
2760
|
+
log3("");
|
|
2761
|
+
const deadline = Date.now() + expiresIn * 1e3;
|
|
2762
|
+
process.stderr.write(" Waiting for authentication");
|
|
2763
|
+
while (Date.now() < deadline) {
|
|
2764
|
+
await sleep2(3e3);
|
|
1333
2765
|
try {
|
|
1334
|
-
|
|
2766
|
+
const resp = await fetch(
|
|
2767
|
+
`${serverUrl}/api/v1/auth/employee/poll/${sessionId}`
|
|
2768
|
+
);
|
|
2769
|
+
const result = await resp.json();
|
|
2770
|
+
if (result.status === "completed") {
|
|
2771
|
+
process.stderr.write("\n\n");
|
|
2772
|
+
const email = result.employee_email || result.user_info?.email || "unknown";
|
|
2773
|
+
const name = result.employee_name || result.user_info?.name || "";
|
|
2774
|
+
const apiKey = result.api_key || "";
|
|
2775
|
+
return { status: "success", apiKey, email, name: name || void 0 };
|
|
2776
|
+
}
|
|
2777
|
+
if (result.status === "expired") {
|
|
2778
|
+
process.stderr.write("\n");
|
|
2779
|
+
log3(" Authentication session expired. Please try again.");
|
|
2780
|
+
return { status: "failed" };
|
|
2781
|
+
}
|
|
2782
|
+
if (result.status === "error") {
|
|
2783
|
+
process.stderr.write("\n");
|
|
2784
|
+
if (result.error_message === "not_verified_employee" && !emailHint) {
|
|
2785
|
+
return {
|
|
2786
|
+
status: "needs_hint",
|
|
2787
|
+
ssoEmail: result.sso_email || "unknown"
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
log3(
|
|
2791
|
+
` Authentication failed: ${result.error_message || "unknown error"}`
|
|
2792
|
+
);
|
|
2793
|
+
return { status: "failed" };
|
|
2794
|
+
}
|
|
2795
|
+
process.stderr.write(".");
|
|
1335
2796
|
} catch {
|
|
1336
|
-
|
|
1337
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
1338
|
-
success: false,
|
|
1339
|
-
error: "Failed to parse chunk upload response from backend"
|
|
1340
|
-
}) }],
|
|
1341
|
-
isError: true
|
|
1342
|
-
};
|
|
1343
|
-
}
|
|
1344
|
-
if (!chunkData.success) {
|
|
1345
|
-
return {
|
|
1346
|
-
content: [{ type: "text", text: JSON.stringify(chunkData) }],
|
|
1347
|
-
isError: true
|
|
1348
|
-
};
|
|
2797
|
+
process.stderr.write("!");
|
|
1349
2798
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
2799
|
+
}
|
|
2800
|
+
process.stderr.write("\n");
|
|
2801
|
+
log3(" Timeout waiting for authentication. Please try again.");
|
|
2802
|
+
return { status: "failed" };
|
|
2803
|
+
}
|
|
2804
|
+
async function showConnectionsAndAutoConnect(apiKey, serverUrl) {
|
|
2805
|
+
let existingConnections = [];
|
|
2806
|
+
try {
|
|
2807
|
+
const resp = await fetch(`${serverUrl}/api/v1/oauth/connections`, {
|
|
2808
|
+
headers: { "x-api-key": apiKey }
|
|
2809
|
+
});
|
|
2810
|
+
if (resp.ok) {
|
|
2811
|
+
const data = await resp.json();
|
|
2812
|
+
existingConnections = data.connections || [];
|
|
2813
|
+
} else {
|
|
2814
|
+
log3(
|
|
2815
|
+
` Warning: Could not fetch current MCP connections (HTTP ${resp.status}).`
|
|
2816
|
+
);
|
|
2817
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
2818
|
+
log3(" Warning: Authentication may be expired. Run 'cortex-mcp login' to refresh.");
|
|
2819
|
+
}
|
|
1352
2820
|
}
|
|
2821
|
+
} catch {
|
|
2822
|
+
log3(" Warning: Could not fetch current MCP connections due to a network error.");
|
|
1353
2823
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
{
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
async function ensureInitialized() {
|
|
1372
|
-
if (initialized) return;
|
|
1373
|
-
try {
|
|
1374
|
-
console.error("[cortex-mcp] Initializing backend session...");
|
|
1375
|
-
await cortex.initialize();
|
|
1376
|
-
console.error("[cortex-mcp] Backend session established.");
|
|
1377
|
-
} catch (err) {
|
|
1378
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1379
|
-
console.error(`[cortex-mcp] Backend initialization failed: ${msg}`);
|
|
2824
|
+
log3("");
|
|
2825
|
+
log3(" MCP Connections");
|
|
2826
|
+
log3(" " + "\u2500".repeat(45));
|
|
2827
|
+
const personalMcps = [];
|
|
2828
|
+
for (const mcp of AVAILABLE_MCPS) {
|
|
2829
|
+
const conn = existingConnections.find((c) => c.mcp_name === mcp.name);
|
|
2830
|
+
if (mcp.authMode === "company") {
|
|
2831
|
+
log3(` ${mcp.displayName.padEnd(15)} company default`);
|
|
2832
|
+
} else {
|
|
2833
|
+
if (conn && !conn.is_company_default && conn.account_email) {
|
|
2834
|
+
log3(` ${mcp.displayName.padEnd(15)} ${conn.account_email}`);
|
|
2835
|
+
} else {
|
|
2836
|
+
log3(
|
|
2837
|
+
` ${mcp.displayName.padEnd(15)} not connected (personal account required)`
|
|
2838
|
+
);
|
|
2839
|
+
personalMcps.push({ name: mcp.name, displayName: mcp.displayName });
|
|
2840
|
+
}
|
|
1380
2841
|
}
|
|
1381
|
-
initialized = true;
|
|
1382
2842
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
2843
|
+
log3("");
|
|
2844
|
+
if (personalMcps.length === 0) {
|
|
2845
|
+
log3(" All accounts connected!");
|
|
2846
|
+
log3("");
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
log3(" The following MCPs require a personal account to access your data.");
|
|
2850
|
+
log3(" Skip any you don't have an account for \u2014 you can always connect later.\n");
|
|
2851
|
+
const rl = readline.createInterface({
|
|
2852
|
+
input: process.stdin,
|
|
2853
|
+
output: process.stderr
|
|
1392
2854
|
});
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
2855
|
+
const ask = (question) => new Promise((resolve) => {
|
|
2856
|
+
rl.question(question, (answer) => resolve(answer.trim().toLowerCase()));
|
|
2857
|
+
});
|
|
2858
|
+
for (const mcp of personalMcps) {
|
|
2859
|
+
const answer = await ask(
|
|
2860
|
+
` Connect ${mcp.displayName}? (Y/n/s to skip all): `
|
|
2861
|
+
);
|
|
2862
|
+
if (answer === "s") {
|
|
2863
|
+
log3(" Skipping remaining account connections.");
|
|
2864
|
+
log3(" You can connect anytime with: cortex-mcp connect <provider>\n");
|
|
2865
|
+
break;
|
|
1400
2866
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
isError: true
|
|
1406
|
-
};
|
|
2867
|
+
if (answer === "n") {
|
|
2868
|
+
log3(` Skipped. Connect later with: cortex-mcp connect ${mcp.name}`);
|
|
2869
|
+
log3("");
|
|
2870
|
+
continue;
|
|
1407
2871
|
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
2872
|
+
log3("");
|
|
2873
|
+
const success = await connectProvider(
|
|
2874
|
+
mcp.name,
|
|
2875
|
+
mcp.displayName,
|
|
2876
|
+
apiKey,
|
|
2877
|
+
serverUrl,
|
|
2878
|
+
"installUrl" in mcp ? mcp.installUrl : void 0
|
|
2879
|
+
);
|
|
2880
|
+
if (!success) {
|
|
2881
|
+
log3(
|
|
2882
|
+
` You can connect later with: cortex-mcp connect ${mcp.name}`
|
|
2883
|
+
);
|
|
2884
|
+
}
|
|
2885
|
+
log3("");
|
|
2886
|
+
}
|
|
2887
|
+
rl.close();
|
|
1415
2888
|
}
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
2889
|
+
async function promptForEmailHint(ssoEmail) {
|
|
2890
|
+
log3("");
|
|
2891
|
+
log3(` Your SSO email (${ssoEmail}) was not found in the employee directory.`);
|
|
2892
|
+
log3(
|
|
2893
|
+
" This can happen if your Okta account uses a different email than your company directory."
|
|
2894
|
+
);
|
|
2895
|
+
log3("");
|
|
2896
|
+
const rl = readline.createInterface({
|
|
2897
|
+
input: process.stdin,
|
|
2898
|
+
output: process.stderr
|
|
2899
|
+
});
|
|
2900
|
+
const answer = await new Promise((resolve) => {
|
|
2901
|
+
rl.question(
|
|
2902
|
+
" Enter your company email address: ",
|
|
2903
|
+
(a) => resolve(a.trim())
|
|
1425
2904
|
);
|
|
2905
|
+
});
|
|
2906
|
+
rl.close();
|
|
2907
|
+
if (!answer || !answer.includes("@")) {
|
|
2908
|
+
log3(" Invalid email. Run 'cortex-mcp login --email you@company.com' to try again.");
|
|
2909
|
+
return null;
|
|
1426
2910
|
}
|
|
1427
|
-
|
|
2911
|
+
return answer;
|
|
1428
2912
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
2913
|
+
async function runLogin(emailHint) {
|
|
2914
|
+
const existing = readCredentials();
|
|
2915
|
+
if (existing) {
|
|
2916
|
+
log3("");
|
|
2917
|
+
log3(` Already logged in as ${existing.name || existing.email}`);
|
|
2918
|
+
log3(" Run 'cortex-mcp logout' first to switch accounts.");
|
|
2919
|
+
log3("");
|
|
1435
2920
|
return;
|
|
1436
2921
|
}
|
|
1437
|
-
const
|
|
1438
|
-
const
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
for (const client of config.configuredClients) {
|
|
1464
|
-
console.log(` ${client}`);
|
|
2922
|
+
const config = readConfig();
|
|
2923
|
+
const serverUrl = config?.server || DEFAULT_SERVER_URL;
|
|
2924
|
+
log3("");
|
|
2925
|
+
log3(" Cortex MCP Login");
|
|
2926
|
+
log3(" Sign in with your company Okta SSO account");
|
|
2927
|
+
log3("");
|
|
2928
|
+
let result = await loginWithSSO(serverUrl, emailHint);
|
|
2929
|
+
if (result.status === "needs_hint") {
|
|
2930
|
+
let hintEmail;
|
|
2931
|
+
if (emailHint) {
|
|
2932
|
+
hintEmail = emailHint;
|
|
2933
|
+
} else {
|
|
2934
|
+
hintEmail = await promptForEmailHint(result.ssoEmail);
|
|
2935
|
+
}
|
|
2936
|
+
if (!hintEmail) {
|
|
2937
|
+
process.exit(1);
|
|
2938
|
+
}
|
|
2939
|
+
log3("");
|
|
2940
|
+
log3(" Retrying with your company email. You'll need to complete SSO again.");
|
|
2941
|
+
log3("");
|
|
2942
|
+
result = await loginWithSSO(serverUrl, hintEmail);
|
|
2943
|
+
if (result.status === "needs_hint") {
|
|
2944
|
+
log3("");
|
|
2945
|
+
log3(" The provided email was also not found in the employee directory.");
|
|
2946
|
+
log3(" Please verify your email and contact IT if the issue persists.");
|
|
2947
|
+
process.exit(1);
|
|
1465
2948
|
}
|
|
1466
2949
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
// src/cli/reset.ts
|
|
1471
|
-
import { existsSync as existsSync4, unlinkSync as unlinkSync2, rmdirSync } from "fs";
|
|
1472
|
-
function runReset() {
|
|
1473
|
-
console.log("");
|
|
1474
|
-
console.log(" Resetting Cortex MCP configuration...");
|
|
1475
|
-
const desktopReset = resetClaudeDesktop();
|
|
1476
|
-
console.log(
|
|
1477
|
-
` Claude Desktop: ${desktopReset ? "entries removed" : "no entries found"}`
|
|
1478
|
-
);
|
|
1479
|
-
const codeReset = resetClaudeCode();
|
|
1480
|
-
console.log(
|
|
1481
|
-
` Claude Code: ${codeReset ? "entries removed" : "no entries found"}`
|
|
1482
|
-
);
|
|
1483
|
-
const cursorReset = resetCursor();
|
|
1484
|
-
console.log(
|
|
1485
|
-
` Cursor: ${cursorReset ? "entries removed" : "no entries found"}`
|
|
1486
|
-
);
|
|
1487
|
-
const codexReset = resetCodex();
|
|
1488
|
-
console.log(
|
|
1489
|
-
` Codex: ${codexReset ? "entries removed" : "no entries found"}`
|
|
1490
|
-
);
|
|
1491
|
-
const configPath = getConfigPath();
|
|
1492
|
-
if (existsSync4(configPath)) {
|
|
1493
|
-
unlinkSync2(configPath);
|
|
1494
|
-
console.log(` Config file: removed`);
|
|
2950
|
+
if (result.status !== "success") {
|
|
2951
|
+
process.exit(1);
|
|
1495
2952
|
}
|
|
1496
|
-
const
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
2953
|
+
const { apiKey, email, name } = result;
|
|
2954
|
+
writeCredentials({
|
|
2955
|
+
apiKey,
|
|
2956
|
+
email,
|
|
2957
|
+
name,
|
|
2958
|
+
authenticatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2959
|
+
});
|
|
2960
|
+
log3(` Authenticated as ${name || email}${name ? ` (${email})` : ""}`);
|
|
2961
|
+
log3(" Personal API key saved.");
|
|
2962
|
+
const existingConfig = readConfig();
|
|
2963
|
+
if (existingConfig) {
|
|
2964
|
+
existingConfig.apiKey = apiKey;
|
|
2965
|
+
if (existingConfig.configuredClients?.length > 0) {
|
|
2966
|
+
log3("");
|
|
2967
|
+
log3(" Updating configured clients:");
|
|
2968
|
+
for (const clientType of existingConfig.configuredClients) {
|
|
2969
|
+
try {
|
|
2970
|
+
configureClient(
|
|
2971
|
+
clientType,
|
|
2972
|
+
existingConfig.server || DEFAULT_SERVER_URL,
|
|
2973
|
+
apiKey,
|
|
2974
|
+
existingConfig.mcps
|
|
2975
|
+
);
|
|
2976
|
+
log3(` ${clientType}: updated`);
|
|
2977
|
+
} catch {
|
|
2978
|
+
log3(` ${clientType}: failed to update`);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
writeConfig(existingConfig);
|
|
1500
2983
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
console.log("");
|
|
2984
|
+
await showConnectionsAndAutoConnect(apiKey, serverUrl);
|
|
2985
|
+
log3(" Done! Restart your AI clients to apply.");
|
|
2986
|
+
log3("");
|
|
1505
2987
|
}
|
|
1506
2988
|
|
|
1507
2989
|
// src/cli/logout.ts
|
|
@@ -1676,12 +3158,9 @@ var program = new Command();
|
|
|
1676
3158
|
program.name("cortex-mcp").description(
|
|
1677
3159
|
"Connect your AI tools to Asana, GitHub, Microsoft 365, Monday.com, Salesforce, Vercel & Supabase via Cortex \u2014 supports Claude, Cursor, Codex & more"
|
|
1678
3160
|
).version("1.0.0");
|
|
1679
|
-
program.command("setup").description("Interactive setup wizard \u2014 configure MCPs and AI clients").
|
|
1680
|
-
"--email <email>",
|
|
1681
|
-
"Company email (use if your Okta email differs from your directory email)"
|
|
1682
|
-
).action(async (options) => {
|
|
3161
|
+
program.command("setup").description("Interactive setup wizard \u2014 configure MCPs and AI clients").action(async () => {
|
|
1683
3162
|
try {
|
|
1684
|
-
await runSetup(
|
|
3163
|
+
await runSetup();
|
|
1685
3164
|
} catch (err) {
|
|
1686
3165
|
if (err.code === "ERR_USE_AFTER_CLOSE") {
|
|
1687
3166
|
process.exit(0);
|
|
@@ -1758,9 +3237,9 @@ program.command("disconnect <provider>").description("Remove your personal OAuth
|
|
|
1758
3237
|
}
|
|
1759
3238
|
});
|
|
1760
3239
|
program.command("connect-mobile").description("Connect Cortex to Claude on mobile \u2014 opens setup page in browser").action(async () => {
|
|
1761
|
-
const { DEFAULT_SERVER_URL:
|
|
1762
|
-
const { openBrowser: openBrowser2 } = await import("./browser-
|
|
1763
|
-
const url = `${
|
|
3240
|
+
const { DEFAULT_SERVER_URL: DEFAULT_SERVER_URL3 } = await import("./constants-2D6W3HYY.js");
|
|
3241
|
+
const { openBrowser: openBrowser2 } = await import("./browser-WFTDXNKP.js");
|
|
3242
|
+
const url = `${DEFAULT_SERVER_URL3}/connect`;
|
|
1764
3243
|
console.log("\nOpening Cortex connect page...");
|
|
1765
3244
|
console.log(` ${url}
|
|
1766
3245
|
`);
|