@stigmer/react 0.0.84 → 0.0.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/demo/fixtures.d.ts +4 -0
  2. package/demo/fixtures.d.ts.map +1 -1
  3. package/demo/fixtures.js +4 -0
  4. package/demo/fixtures.js.map +1 -1
  5. package/index.d.ts +4 -2
  6. package/index.d.ts.map +1 -1
  7. package/index.js +3 -1
  8. package/index.js.map +1 -1
  9. package/library/ResourceListView.js +1 -1
  10. package/library/ResourceListView.js.map +1 -1
  11. package/mcp-server/McpServerConnectDialog.d.ts +51 -0
  12. package/mcp-server/McpServerConnectDialog.d.ts.map +1 -0
  13. package/mcp-server/McpServerConnectDialog.js +164 -0
  14. package/mcp-server/McpServerConnectDialog.js.map +1 -0
  15. package/mcp-server/McpServerDetailView.js +2 -2
  16. package/mcp-server/McpServerDetailView.js.map +1 -1
  17. package/mcp-server/McpServerPicker.d.ts.map +1 -1
  18. package/mcp-server/McpServerPicker.js +7 -1
  19. package/mcp-server/McpServerPicker.js.map +1 -1
  20. package/mcp-server/index.d.ts +2 -0
  21. package/mcp-server/index.d.ts.map +1 -1
  22. package/mcp-server/index.js +1 -0
  23. package/mcp-server/index.js.map +1 -1
  24. package/oauth-app/CreateOAuthAppForm.d.ts +41 -0
  25. package/oauth-app/CreateOAuthAppForm.d.ts.map +1 -0
  26. package/oauth-app/CreateOAuthAppForm.js +140 -0
  27. package/oauth-app/CreateOAuthAppForm.js.map +1 -0
  28. package/oauth-app/OAuthAppDetailPanel.d.ts +43 -0
  29. package/oauth-app/OAuthAppDetailPanel.d.ts.map +1 -0
  30. package/oauth-app/OAuthAppDetailPanel.js +202 -0
  31. package/oauth-app/OAuthAppDetailPanel.js.map +1 -0
  32. package/oauth-app/OAuthAppListPanel.d.ts +43 -0
  33. package/oauth-app/OAuthAppListPanel.d.ts.map +1 -0
  34. package/oauth-app/OAuthAppListPanel.js +79 -0
  35. package/oauth-app/OAuthAppListPanel.js.map +1 -0
  36. package/oauth-app/index.d.ts +15 -0
  37. package/oauth-app/index.d.ts.map +1 -0
  38. package/oauth-app/index.js +8 -0
  39. package/oauth-app/index.js.map +1 -0
  40. package/oauth-app/useCreateOAuthApp.d.ts +39 -0
  41. package/oauth-app/useCreateOAuthApp.d.ts.map +1 -0
  42. package/oauth-app/useCreateOAuthApp.js +50 -0
  43. package/oauth-app/useCreateOAuthApp.js.map +1 -0
  44. package/oauth-app/useDeleteOAuthApp.d.ts +31 -0
  45. package/oauth-app/useDeleteOAuthApp.d.ts.map +1 -0
  46. package/oauth-app/useDeleteOAuthApp.js +43 -0
  47. package/oauth-app/useDeleteOAuthApp.js.map +1 -0
  48. package/oauth-app/useOAuthAppList.d.ts +32 -0
  49. package/oauth-app/useOAuthAppList.d.ts.map +1 -0
  50. package/oauth-app/useOAuthAppList.js +61 -0
  51. package/oauth-app/useOAuthAppList.js.map +1 -0
  52. package/oauth-app/useUpdateOAuthApp.d.ts +38 -0
  53. package/oauth-app/useUpdateOAuthApp.d.ts.map +1 -0
  54. package/oauth-app/useUpdateOAuthApp.js +49 -0
  55. package/oauth-app/useUpdateOAuthApp.js.map +1 -0
  56. package/package.json +4 -4
  57. package/src/demo/fixtures.ts +8 -0
  58. package/src/index.ts +22 -0
  59. package/src/library/ResourceListView.tsx +8 -8
  60. package/src/mcp-server/McpServerConnectDialog.tsx +527 -0
  61. package/src/mcp-server/McpServerDetailView.tsx +2 -1
  62. package/src/mcp-server/McpServerPicker.tsx +8 -1
  63. package/src/mcp-server/index.ts +3 -0
  64. package/src/oauth-app/CreateOAuthAppForm.tsx +449 -0
  65. package/src/oauth-app/OAuthAppDetailPanel.tsx +671 -0
  66. package/src/oauth-app/OAuthAppListPanel.tsx +237 -0
  67. package/src/oauth-app/index.ts +14 -0
  68. package/src/oauth-app/useCreateOAuthApp.ts +70 -0
  69. package/src/oauth-app/useDeleteOAuthApp.ts +62 -0
  70. package/src/oauth-app/useOAuthAppList.ts +84 -0
  71. package/src/oauth-app/useUpdateOAuthApp.ts +69 -0
  72. package/styles.css +1 -1
@@ -0,0 +1,527 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { cn } from "@stigmer/theme";
5
+ import type { EnvVarInput } from "@stigmer/sdk";
6
+ import { useMcpServer } from "./useMcpServer";
7
+ import { useMcpServerCredentials } from "./useMcpServerCredentials";
8
+ import { useMcpServerOAuthConnect } from "./useMcpServerOAuthConnect";
9
+ import type { OAuthConnectPhase } from "./useMcpServerOAuthConnect";
10
+ import { useMcpServerConnect } from "./useMcpServerConnect";
11
+ import { useDisconnectOAuth } from "./useDisconnectOAuth";
12
+ import { EnvVarForm } from "../environment/EnvVarForm";
13
+ import { ErrorMessage } from "../error/ErrorMessage";
14
+
15
+ /** Props for {@link McpServerConnectDialog}. */
16
+ export interface McpServerConnectDialogProps {
17
+ /** Organization slug that owns the MCP server. */
18
+ readonly org: string;
19
+ /** MCP server slug. */
20
+ readonly slug: string;
21
+ /**
22
+ * The authenticated user's active organization slug.
23
+ * Used for credential storage — tokens are stored in the user's
24
+ * personal environment within this org.
25
+ * Falls back to `org` when omitted.
26
+ */
27
+ readonly activeOrg?: string;
28
+ /** Whether the dialog is open. */
29
+ readonly open: boolean;
30
+ /** Called when the dialog should close (backdrop click, cancel, or success). */
31
+ readonly onClose: () => void;
32
+ /** Called after a successful connect with the server name. */
33
+ readonly onConnected?: (serverName: string) => void;
34
+ /** Additional CSS classes for the dialog element. */
35
+ readonly className?: string;
36
+ }
37
+
38
+ type DialogPhase = "credentials" | "connecting" | "success" | "error";
39
+
40
+ /**
41
+ * Modal dialog for connecting to an MCP server.
42
+ *
43
+ * Fetches the server, determines whether credentials or OAuth are
44
+ * needed, and walks the user through the connect flow — all without
45
+ * navigating away from the current page.
46
+ *
47
+ * Uses the native `<dialog>` element for accessibility (focus trap,
48
+ * Escape to close, backdrop click).
49
+ *
50
+ * Built on existing hooks: {@link useMcpServer},
51
+ * {@link useMcpServerCredentials}, {@link useMcpServerOAuthConnect},
52
+ * and {@link useMcpServerConnect}.
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * const [connectTarget, setConnectTarget] = useState<{ org: string; slug: string } | null>(null);
57
+ *
58
+ * <McpServerConnectDialog
59
+ * org={connectTarget?.org ?? ""}
60
+ * slug={connectTarget?.slug ?? ""}
61
+ * open={connectTarget !== null}
62
+ * onClose={() => setConnectTarget(null)}
63
+ * onConnected={(name) => toast(`Connected to ${name}`)}
64
+ * />
65
+ * ```
66
+ */
67
+ export function McpServerConnectDialog({
68
+ org,
69
+ slug,
70
+ activeOrg,
71
+ open,
72
+ onClose,
73
+ onConnected,
74
+ className,
75
+ }: McpServerConnectDialogProps) {
76
+ const dialogRef = useRef<HTMLDialogElement>(null);
77
+ const resolvedOrg = activeOrg || org;
78
+
79
+ useEffect(() => {
80
+ const dialog = dialogRef.current;
81
+ if (!dialog) return;
82
+
83
+ if (open && !dialog.open) {
84
+ dialog.showModal();
85
+ } else if (!open && dialog.open) {
86
+ dialog.close();
87
+ }
88
+ }, [open]);
89
+
90
+ const handleDialogClose = useCallback(() => {
91
+ onClose();
92
+ }, [onClose]);
93
+
94
+ const handleBackdropClick = useCallback(
95
+ (e: React.MouseEvent<HTMLDialogElement>) => {
96
+ if (e.target === dialogRef.current) {
97
+ onClose();
98
+ }
99
+ },
100
+ [onClose],
101
+ );
102
+
103
+ if (!open) return null;
104
+
105
+ return (
106
+ <dialog
107
+ ref={dialogRef}
108
+ onClose={handleDialogClose}
109
+ onClick={handleBackdropClick}
110
+ className={cn(
111
+ "m-auto max-h-[85vh] w-full max-w-md overflow-visible rounded-lg border border-border bg-card p-0 text-foreground shadow-lg",
112
+ "backdrop:bg-black/50",
113
+ className,
114
+ )}
115
+ >
116
+ <div
117
+ className="flex max-h-[85vh] flex-col overflow-y-auto p-6"
118
+ onClick={(e) => e.stopPropagation()}
119
+ >
120
+ <ConnectDialogContent
121
+ org={org}
122
+ slug={slug}
123
+ activeOrg={resolvedOrg}
124
+ onClose={onClose}
125
+ onConnected={onConnected}
126
+ />
127
+ </div>
128
+ </dialog>
129
+ );
130
+ }
131
+
132
+ function ConnectDialogContent({
133
+ org,
134
+ slug,
135
+ activeOrg,
136
+ onClose,
137
+ onConnected,
138
+ }: {
139
+ readonly org: string;
140
+ readonly slug: string;
141
+ readonly activeOrg: string;
142
+ readonly onClose: () => void;
143
+ readonly onConnected?: (serverName: string) => void;
144
+ }) {
145
+ const { mcpServer, isLoading: isServerLoading, error: serverError } = useMcpServer(org, slug);
146
+ const creds = useMcpServerCredentials(activeOrg, mcpServer);
147
+ const { connect, isConnecting, error: connectError, clearError: clearConnectError } = useMcpServerConnect();
148
+ const oauth = useMcpServerOAuthConnect();
149
+ const disconnect = useDisconnectOAuth();
150
+
151
+ const [phase, setPhase] = useState<DialogPhase>("credentials");
152
+
153
+ const serverName = mcpServer?.metadata?.name ?? slug;
154
+ const serverId = mcpServer?.metadata?.id ?? "";
155
+ const declaredEnvKeys = Object.keys(mcpServer?.spec?.env ?? {});
156
+
157
+ const handleConnect = useCallback(async () => {
158
+ if (!serverId) return;
159
+
160
+ setPhase("connecting");
161
+ try {
162
+ await connect(serverId, activeOrg, undefined, declaredEnvKeys);
163
+ setPhase("success");
164
+ onConnected?.(serverName);
165
+ } catch {
166
+ setPhase("error");
167
+ }
168
+ }, [serverId, activeOrg, declaredEnvKeys, connect, onConnected, serverName]);
169
+
170
+ const handleCredentialSubmit = useCallback(
171
+ async (values: Record<string, EnvVarInput>) => {
172
+ await creds.saveCredentials(values);
173
+ creds.refetch();
174
+ },
175
+ [creds],
176
+ );
177
+
178
+ const handleOAuthSignIn = useCallback(() => {
179
+ if (!serverId) return;
180
+ oauth.startOAuth(serverId, activeOrg, declaredEnvKeys).then(
181
+ () => {
182
+ creds.refetch();
183
+ setPhase("success");
184
+ onConnected?.(serverName);
185
+ },
186
+ () => {
187
+ setPhase("error");
188
+ },
189
+ );
190
+ }, [serverId, activeOrg, declaredEnvKeys, oauth, creds, onConnected, serverName]);
191
+
192
+ // Auto-trigger connect when credentials become ready (manual-only servers)
193
+ useEffect(() => {
194
+ if (
195
+ phase === "credentials" &&
196
+ creds.isReady &&
197
+ !creds.isLoading &&
198
+ mcpServer &&
199
+ creds.authMode === "manual"
200
+ ) {
201
+ // Don't auto-connect; let user click the button
202
+ }
203
+ }, [phase, creds.isReady, creds.isLoading, mcpServer, creds.authMode]);
204
+
205
+ if (isServerLoading || creds.isLoading) {
206
+ return (
207
+ <>
208
+ <DialogHeader title="Connect MCP Server" onClose={onClose} />
209
+ <div className="flex flex-col items-center gap-3 py-8">
210
+ <LoadingSpinner />
211
+ <p className="text-sm text-muted-foreground">Loading server details...</p>
212
+ </div>
213
+ </>
214
+ );
215
+ }
216
+
217
+ if (serverError) {
218
+ return (
219
+ <>
220
+ <DialogHeader title="Connect MCP Server" onClose={onClose} />
221
+ <div className="py-4">
222
+ <ErrorMessage error={serverError} />
223
+ </div>
224
+ </>
225
+ );
226
+ }
227
+
228
+ if (!mcpServer) {
229
+ return (
230
+ <>
231
+ <DialogHeader title="Connect MCP Server" onClose={onClose} />
232
+ <p className="py-4 text-sm text-muted-foreground">MCP server not found.</p>
233
+ </>
234
+ );
235
+ }
236
+
237
+ if (phase === "success") {
238
+ return (
239
+ <>
240
+ <DialogHeader title="Connected" onClose={onClose} />
241
+ <div className="flex flex-col items-center gap-3 py-6">
242
+ <SuccessIcon />
243
+ <p className="text-sm font-medium text-foreground">
244
+ Successfully connected to {serverName}
245
+ </p>
246
+ <p className="text-xs text-muted-foreground">
247
+ Tools and capabilities have been discovered.
248
+ </p>
249
+ <button
250
+ type="button"
251
+ onClick={onClose}
252
+ className={cn(
253
+ "mt-2 inline-flex items-center rounded-md px-4 py-2 text-sm font-medium",
254
+ "bg-primary text-primary-foreground",
255
+ "hover:bg-primary/90",
256
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
257
+ )}
258
+ >
259
+ Done
260
+ </button>
261
+ </div>
262
+ </>
263
+ );
264
+ }
265
+
266
+ const isConnectingPhase = phase === "connecting" || isConnecting || oauth.isInProgress;
267
+ const activeError = connectError ?? oauth.error;
268
+
269
+ return (
270
+ <>
271
+ <DialogHeader title={serverName} onClose={onClose} />
272
+
273
+ {mcpServer.spec?.description && (
274
+ <p className="mb-4 text-xs leading-relaxed text-muted-foreground">
275
+ {mcpServer.spec.description}
276
+ </p>
277
+ )}
278
+
279
+ {activeError && (
280
+ <div className="mb-4">
281
+ <ErrorMessage
282
+ error={activeError}
283
+ retry={() => {
284
+ clearConnectError();
285
+ oauth.clearError();
286
+ setPhase("credentials");
287
+ }}
288
+ />
289
+ </div>
290
+ )}
291
+
292
+ {creds.authMode === "oauth" && !creds.manualOverride && (
293
+ <OAuthSection
294
+ isConnected={creds.isOAuthConnected}
295
+ phase={oauth.phase}
296
+ onSignIn={handleOAuthSignIn}
297
+ isVendorApprovalBlocked={creds.isVendorApprovalBlocked}
298
+ onSwitchToManual={() => creds.setManualOverride(true)}
299
+ disabled={isConnectingPhase}
300
+ />
301
+ )}
302
+
303
+ {creds.missingVariables.length > 0 && (
304
+ <div className="mt-2">
305
+ <EnvVarForm
306
+ variables={creds.missingVariables}
307
+ onSubmit={(values) => handleCredentialSubmit(values)}
308
+ isSubmitting={creds.isSaving}
309
+ disabled={isConnectingPhase}
310
+ hideSaveToggle
311
+ />
312
+ </div>
313
+ )}
314
+
315
+ {creds.authMode === "oauth" && creds.manualOverride && (
316
+ <button
317
+ type="button"
318
+ onClick={() => creds.setManualOverride(false)}
319
+ disabled={isConnectingPhase}
320
+ className="mt-2 text-xs text-muted-foreground underline hover:text-foreground"
321
+ >
322
+ Sign in with OAuth instead
323
+ </button>
324
+ )}
325
+
326
+ {creds.isReady && creds.authMode === "manual" && (
327
+ <button
328
+ type="button"
329
+ onClick={handleConnect}
330
+ disabled={isConnectingPhase}
331
+ className={cn(
332
+ "mt-4 inline-flex w-full items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium",
333
+ "bg-primary text-primary-foreground",
334
+ "hover:bg-primary/90",
335
+ "disabled:pointer-events-none disabled:opacity-50",
336
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
337
+ )}
338
+ >
339
+ {isConnectingPhase && <LoadingSpinner size="sm" />}
340
+ {isConnectingPhase ? "Connecting..." : "Connect"}
341
+ </button>
342
+ )}
343
+
344
+ {creds.isReady && creds.authMode === "oauth" && creds.isOAuthConnected && (
345
+ <button
346
+ type="button"
347
+ onClick={handleConnect}
348
+ disabled={isConnectingPhase}
349
+ className={cn(
350
+ "mt-4 inline-flex w-full items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium",
351
+ "bg-primary text-primary-foreground",
352
+ "hover:bg-primary/90",
353
+ "disabled:pointer-events-none disabled:opacity-50",
354
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
355
+ )}
356
+ >
357
+ {isConnectingPhase && <LoadingSpinner size="sm" />}
358
+ {isConnectingPhase ? "Discovering tools..." : "Discover Tools"}
359
+ </button>
360
+ )}
361
+
362
+ {!creds.isReady &&
363
+ creds.missingVariables.length === 0 &&
364
+ creds.authMode === "manual" && (
365
+ <p className="mt-4 text-center text-xs text-muted-foreground">
366
+ No credentials required — this server is ready to connect.
367
+ </p>
368
+ )}
369
+ </>
370
+ );
371
+ }
372
+
373
+ // ---------------------------------------------------------------------------
374
+ // Sub-components
375
+ // ---------------------------------------------------------------------------
376
+
377
+ function DialogHeader({
378
+ title,
379
+ onClose,
380
+ }: {
381
+ readonly title: string;
382
+ readonly onClose: () => void;
383
+ }) {
384
+ return (
385
+ <div className="mb-4 flex items-start justify-between gap-3">
386
+ <h2 className="text-base font-semibold text-foreground">{title}</h2>
387
+ <button
388
+ type="button"
389
+ onClick={onClose}
390
+ aria-label="Close"
391
+ className="inline-flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
392
+ >
393
+ <CloseIcon />
394
+ </button>
395
+ </div>
396
+ );
397
+ }
398
+
399
+ function OAuthSection({
400
+ isConnected,
401
+ phase,
402
+ onSignIn,
403
+ isVendorApprovalBlocked,
404
+ onSwitchToManual,
405
+ disabled,
406
+ }: {
407
+ readonly isConnected: boolean;
408
+ readonly phase: OAuthConnectPhase;
409
+ readonly onSignIn: () => void;
410
+ readonly isVendorApprovalBlocked: boolean;
411
+ readonly onSwitchToManual: () => void;
412
+ readonly disabled?: boolean;
413
+ }) {
414
+ if (isConnected) {
415
+ return (
416
+ <div className="flex items-center gap-2 rounded-md border border-border bg-muted/30 px-3 py-2">
417
+ <span className="size-2 shrink-0 rounded-full bg-green-500" />
418
+ <span className="text-sm text-foreground">OAuth connected</span>
419
+ </div>
420
+ );
421
+ }
422
+
423
+ const isInProgress = phase !== "idle" && phase !== "done";
424
+ const phaseLabel: Record<string, string> = {
425
+ initiating: "Starting OAuth...",
426
+ "awaiting-callback": "Waiting for authorization...",
427
+ completing: "Completing OAuth...",
428
+ connecting: "Discovering tools...",
429
+ };
430
+
431
+ return (
432
+ <div className="flex flex-col gap-2">
433
+ <button
434
+ type="button"
435
+ onClick={onSignIn}
436
+ disabled={disabled || isInProgress || isVendorApprovalBlocked}
437
+ className={cn(
438
+ "inline-flex w-full items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium",
439
+ "bg-primary text-primary-foreground",
440
+ "hover:bg-primary/90",
441
+ "disabled:pointer-events-none disabled:opacity-50",
442
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
443
+ )}
444
+ >
445
+ {isInProgress && <LoadingSpinner size="sm" />}
446
+ {isInProgress ? (phaseLabel[phase] ?? "Connecting...") : "Sign in with OAuth"}
447
+ </button>
448
+ {isVendorApprovalBlocked && (
449
+ <p className="text-xs text-muted-foreground">
450
+ OAuth sign-in is pending vendor approval. You can enter your token manually instead.
451
+ </p>
452
+ )}
453
+ <button
454
+ type="button"
455
+ onClick={onSwitchToManual}
456
+ disabled={disabled || isInProgress}
457
+ className="text-xs text-muted-foreground underline hover:text-foreground"
458
+ >
459
+ Enter token manually
460
+ </button>
461
+ </div>
462
+ );
463
+ }
464
+
465
+ function LoadingSpinner({ size = "md" }: { readonly size?: "sm" | "md" }) {
466
+ const cls = size === "sm" ? "size-3.5" : "size-5";
467
+ return (
468
+ <svg
469
+ className={cn(cls, "animate-spin text-current")}
470
+ viewBox="0 0 24 24"
471
+ fill="none"
472
+ aria-hidden="true"
473
+ >
474
+ <circle
475
+ cx="12"
476
+ cy="12"
477
+ r="10"
478
+ stroke="currentColor"
479
+ strokeWidth="3"
480
+ strokeLinecap="round"
481
+ className="opacity-25"
482
+ />
483
+ <path
484
+ d="M12 2a10 10 0 0 1 10 10"
485
+ stroke="currentColor"
486
+ strokeWidth="3"
487
+ strokeLinecap="round"
488
+ className="opacity-75"
489
+ />
490
+ </svg>
491
+ );
492
+ }
493
+
494
+ function SuccessIcon() {
495
+ return (
496
+ <svg
497
+ className="size-10 text-green-500"
498
+ viewBox="0 0 24 24"
499
+ fill="none"
500
+ stroke="currentColor"
501
+ strokeWidth="2"
502
+ strokeLinecap="round"
503
+ strokeLinejoin="round"
504
+ aria-hidden="true"
505
+ >
506
+ <circle cx="12" cy="12" r="10" />
507
+ <path d="m9 12 2 2 4-4" />
508
+ </svg>
509
+ );
510
+ }
511
+
512
+ function CloseIcon() {
513
+ return (
514
+ <svg
515
+ className="size-4"
516
+ viewBox="0 0 16 16"
517
+ fill="none"
518
+ stroke="currentColor"
519
+ strokeWidth="1.5"
520
+ strokeLinecap="round"
521
+ strokeLinejoin="round"
522
+ aria-hidden="true"
523
+ >
524
+ <path d="m4 4 8 8M12 4l-8 8" />
525
+ </svg>
526
+ );
527
+ }
@@ -409,7 +409,7 @@ export function McpServerDetailView({
409
409
  ref={byoaDialogRef}
410
410
  onCancel={handleByoaDialogCancel}
411
411
  className={cn(
412
- "w-full max-w-md rounded-lg border border-border bg-background p-6 shadow-lg",
412
+ "m-auto w-full max-w-md rounded-lg border border-border bg-background p-6 shadow-lg",
413
413
  "backdrop:bg-black/50",
414
414
  )}
415
415
  >
@@ -868,6 +868,7 @@ function ConnectBar({
868
868
  <button
869
869
  type="button"
870
870
  onClick={onBringOwnApp}
871
+ data-cursor-target="byoa-cta-button"
871
872
  className="mt-1.5 inline-flex items-center gap-1 rounded-md bg-amber-600 px-2.5 py-1 text-[11px] font-medium text-white hover:bg-amber-700 dark:bg-amber-500 dark:text-amber-950 dark:hover:bg-amber-400"
872
873
  >
873
874
  Use your own OAuth app
@@ -13,6 +13,7 @@ import { cn } from "@stigmer/theme";
13
13
  import type { EnvVarInput, McpServerUsageInput, ResourceRef } from "@stigmer/sdk";
14
14
  import type { SearchResult } from "@stigmer/protos/ai/stigmer/search/v1/io_pb";
15
15
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
16
+ import { VendorApprovalStatus } from "@stigmer/protos/ai/stigmer/iam/oauthapp/v1/spec_pb";
16
17
  import type { EnvVarFormSubmitOptions } from "../environment/EnvVarForm";
17
18
  import { useMcpServerSearch } from "./useMcpServerSearch";
18
19
  import { useScrollShadows } from "../internal/useScrollShadows";
@@ -418,7 +419,12 @@ export function McpServerPicker({
418
419
 
419
420
  const oauthStatus = entry.mcpServer.status?.oauthStatus;
420
421
  const isVendorApprovalPending =
421
- hasOAuth && oauthStatus?.vendorApprovalStatus === 1; // VendorApprovalStatus.PENDING
422
+ hasOAuth &&
423
+ oauthStatus?.vendorApprovalStatus === VendorApprovalStatus.PENDING;
424
+ const isVendorApprovalBlocked =
425
+ hasOAuth &&
426
+ (oauthStatus?.vendorApprovalStatus === VendorApprovalStatus.PENDING ||
427
+ oauthStatus?.vendorApprovalStatus === VendorApprovalStatus.REJECTED);
422
428
 
423
429
  const oauthSignInProps =
424
430
  hasOAuth && !isManualOverride
@@ -440,6 +446,7 @@ export function McpServerPicker({
440
446
  error: oauth.error,
441
447
  onClearError: oauth.clearError,
442
448
  isVendorApprovalPending,
449
+ isVendorApprovalBlocked,
443
450
  vendorApprovalDocsUrl: oauthStatus?.vendorApprovalDocsUrl || null,
444
451
  }
445
452
  : undefined;
@@ -82,3 +82,6 @@ export type {
82
82
  UseMcpServerCredentialsReturn,
83
83
  McpServerAuthMode,
84
84
  } from "./useMcpServerCredentials";
85
+
86
+ export { McpServerConnectDialog } from "./McpServerConnectDialog";
87
+ export type { McpServerConnectDialogProps } from "./McpServerConnectDialog";