@stigmer/react 0.0.80 → 0.0.82
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/environment/index.d.ts +1 -1
- package/environment/index.d.ts.map +1 -1
- package/environment/index.js +1 -1
- package/environment/index.js.map +1 -1
- package/environment/systemEnvVars.d.ts +16 -0
- package/environment/systemEnvVars.d.ts.map +1 -1
- package/environment/systemEnvVars.js +32 -0
- package/environment/systemEnvVars.js.map +1 -1
- package/index.d.ts +1 -1
- package/index.d.ts.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/mcp-server/McpServerConfigPanel.d.ts +23 -1
- package/mcp-server/McpServerConfigPanel.d.ts.map +1 -1
- package/mcp-server/McpServerConfigPanel.js +14 -5
- package/mcp-server/McpServerConfigPanel.js.map +1 -1
- package/mcp-server/McpServerDetailView.d.ts.map +1 -1
- package/mcp-server/McpServerDetailView.js +36 -14
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/mcp-server/McpServerPicker.d.ts.map +1 -1
- package/mcp-server/McpServerPicker.js +27 -3
- package/mcp-server/McpServerPicker.js.map +1 -1
- package/mcp-server/OAuthCallbackHandler.d.ts.map +1 -1
- package/mcp-server/OAuthCallbackHandler.js +33 -9
- package/mcp-server/OAuthCallbackHandler.js.map +1 -1
- package/mcp-server/useMcpServerConnect.d.ts +22 -12
- package/mcp-server/useMcpServerConnect.d.ts.map +1 -1
- package/mcp-server/useMcpServerConnect.js +17 -10
- package/mcp-server/useMcpServerConnect.js.map +1 -1
- package/mcp-server/useMcpServerCredentials.d.ts +53 -2
- package/mcp-server/useMcpServerCredentials.d.ts.map +1 -1
- package/mcp-server/useMcpServerCredentials.js +37 -6
- package/mcp-server/useMcpServerCredentials.js.map +1 -1
- package/mcp-server/useMcpServerOAuthConnect.d.ts +10 -1
- package/mcp-server/useMcpServerOAuthConnect.d.ts.map +1 -1
- package/mcp-server/useMcpServerOAuthConnect.js +60 -11
- package/mcp-server/useMcpServerOAuthConnect.js.map +1 -1
- package/package.json +4 -4
- package/src/environment/index.ts +1 -0
- package/src/environment/systemEnvVars.ts +39 -0
- package/src/index.ts +1 -0
- package/src/mcp-server/McpServerConfigPanel.tsx +82 -4
- package/src/mcp-server/McpServerDetailView.tsx +104 -16
- package/src/mcp-server/McpServerPicker.tsx +56 -18
- package/src/mcp-server/OAuthCallbackHandler.tsx +39 -12
- package/src/mcp-server/useMcpServerConnect.ts +33 -14
- package/src/mcp-server/useMcpServerCredentials.ts +68 -6
- package/src/mcp-server/useMcpServerOAuthConnect.ts +63 -15
- package/styles.css +1 -1
|
@@ -121,7 +121,7 @@ export function McpServerDetailView({
|
|
|
121
121
|
className,
|
|
122
122
|
}: McpServerDetailViewProps) {
|
|
123
123
|
const { mcpServer, isLoading, error, refetch } = useMcpServer(org, slug);
|
|
124
|
-
const credentials = useMcpServerCredentials(org, mcpServer ?? null);
|
|
124
|
+
const credentials = useMcpServerCredentials(activeOrg ?? org, mcpServer ?? null);
|
|
125
125
|
const connection = useMcpServerConnect();
|
|
126
126
|
const oauth = useMcpServerOAuthConnect();
|
|
127
127
|
|
|
@@ -140,8 +140,9 @@ export function McpServerDetailView({
|
|
|
140
140
|
const handleOAuthSignIn = useCallback(async () => {
|
|
141
141
|
if (!mcpServer?.metadata?.id) return;
|
|
142
142
|
|
|
143
|
+
const envKeys = Object.keys(mcpServer.spec?.env ?? {});
|
|
143
144
|
try {
|
|
144
|
-
await oauth.startOAuth(mcpServer.metadata.id, activeOrg ?? org);
|
|
145
|
+
await oauth.startOAuth(mcpServer.metadata.id, activeOrg ?? org, envKeys);
|
|
145
146
|
credentials.refetch();
|
|
146
147
|
refetch();
|
|
147
148
|
} catch {
|
|
@@ -152,7 +153,11 @@ export function McpServerDetailView({
|
|
|
152
153
|
const handleConnectClick = useCallback(async () => {
|
|
153
154
|
if (!mcpServer?.metadata?.id) return;
|
|
154
155
|
|
|
155
|
-
if (
|
|
156
|
+
if (
|
|
157
|
+
credentials.authMode === "oauth" &&
|
|
158
|
+
!credentials.isOAuthConnected &&
|
|
159
|
+
!credentials.manualOverride
|
|
160
|
+
) {
|
|
156
161
|
handleOAuthSignIn();
|
|
157
162
|
return;
|
|
158
163
|
}
|
|
@@ -162,13 +167,14 @@ export function McpServerDetailView({
|
|
|
162
167
|
return;
|
|
163
168
|
}
|
|
164
169
|
|
|
170
|
+
const envKeys = Object.keys(mcpServer.spec?.env ?? {});
|
|
165
171
|
try {
|
|
166
|
-
await connection.connect(mcpServer.metadata.id);
|
|
172
|
+
await connection.connect(mcpServer.metadata.id, activeOrg ?? org, undefined, envKeys);
|
|
167
173
|
refetch();
|
|
168
174
|
} catch {
|
|
169
175
|
// error state is managed by the hook
|
|
170
176
|
}
|
|
171
|
-
}, [mcpServer, credentials.authMode, credentials.isOAuthConnected, credentials.isReady, connection, refetch, handleOAuthSignIn]);
|
|
177
|
+
}, [mcpServer, credentials.authMode, credentials.isOAuthConnected, credentials.manualOverride, credentials.isReady, connection, refetch, handleOAuthSignIn]);
|
|
172
178
|
|
|
173
179
|
const handleCredentialSubmit = useCallback(
|
|
174
180
|
async (
|
|
@@ -184,10 +190,12 @@ export function McpServerDetailView({
|
|
|
184
190
|
setShowCredentialForm(false);
|
|
185
191
|
|
|
186
192
|
if (mcpServer?.metadata?.id) {
|
|
193
|
+
const envKeys = Object.keys(mcpServer.spec?.env ?? {});
|
|
194
|
+
const connectOrg = activeOrg ?? org;
|
|
187
195
|
if (options.saveForFuture) {
|
|
188
|
-
await connection.connect(mcpServer.metadata.id);
|
|
196
|
+
await connection.connect(mcpServer.metadata.id, connectOrg, undefined, envKeys);
|
|
189
197
|
} else {
|
|
190
|
-
await connection.connect(mcpServer.metadata.id, values);
|
|
198
|
+
await connection.connect(mcpServer.metadata.id, connectOrg, values, envKeys);
|
|
191
199
|
}
|
|
192
200
|
refetch();
|
|
193
201
|
}
|
|
@@ -288,6 +296,17 @@ export function McpServerDetailView({
|
|
|
288
296
|
isOAuthConnected={credentials.isOAuthConnected}
|
|
289
297
|
accessTokenExpiresAt={credentials.accessTokenExpiresAt}
|
|
290
298
|
tokenLifetimeHint={credentials.tokenLifetimeHint}
|
|
299
|
+
isVendorApprovalPending={credentials.isVendorApprovalPending}
|
|
300
|
+
vendorApprovalDocsUrl={credentials.vendorApprovalDocsUrl}
|
|
301
|
+
manualOverride={credentials.manualOverride}
|
|
302
|
+
onManualOverride={() => {
|
|
303
|
+
credentials.setManualOverride(true);
|
|
304
|
+
setShowCredentialForm(true);
|
|
305
|
+
}}
|
|
306
|
+
onBackToOAuth={() => {
|
|
307
|
+
credentials.setManualOverride(false);
|
|
308
|
+
setShowCredentialForm(false);
|
|
309
|
+
}}
|
|
291
310
|
/>
|
|
292
311
|
|
|
293
312
|
{showCredentialForm && credentials.missingVariables.length > 0 && (
|
|
@@ -357,6 +376,11 @@ function ConnectBar({
|
|
|
357
376
|
isOAuthConnected,
|
|
358
377
|
accessTokenExpiresAt,
|
|
359
378
|
tokenLifetimeHint,
|
|
379
|
+
isVendorApprovalPending,
|
|
380
|
+
vendorApprovalDocsUrl,
|
|
381
|
+
manualOverride,
|
|
382
|
+
onManualOverride,
|
|
383
|
+
onBackToOAuth,
|
|
360
384
|
}: {
|
|
361
385
|
readonly isConnecting: boolean;
|
|
362
386
|
readonly connectionError: Error | null;
|
|
@@ -371,6 +395,11 @@ function ConnectBar({
|
|
|
371
395
|
readonly isOAuthConnected: boolean;
|
|
372
396
|
readonly accessTokenExpiresAt: bigint;
|
|
373
397
|
readonly tokenLifetimeHint: string | null;
|
|
398
|
+
readonly isVendorApprovalPending: boolean;
|
|
399
|
+
readonly vendorApprovalDocsUrl: string | null;
|
|
400
|
+
readonly manualOverride: boolean;
|
|
401
|
+
readonly onManualOverride: () => void;
|
|
402
|
+
readonly onBackToOAuth: () => void;
|
|
374
403
|
}) {
|
|
375
404
|
const isOAuthBusy =
|
|
376
405
|
oauthPhase === "initiating" ||
|
|
@@ -378,17 +407,22 @@ function ConnectBar({
|
|
|
378
407
|
oauthPhase === "completing" ||
|
|
379
408
|
oauthPhase === "connecting";
|
|
380
409
|
|
|
410
|
+
const showOAuthPrimary =
|
|
411
|
+
authMode === "oauth" && !isOAuthConnected && !manualOverride;
|
|
412
|
+
|
|
413
|
+
const oauthSignInDisabled = isVendorApprovalPending && showOAuthPrimary;
|
|
414
|
+
|
|
381
415
|
const buttonLabel = (() => {
|
|
382
416
|
if (isOAuthBusy) return oauthPhaseLabel(oauthPhase);
|
|
383
417
|
if (isConnecting) return "Connecting...";
|
|
384
|
-
if (
|
|
418
|
+
if (showOAuthPrimary) return "Sign in to connect";
|
|
385
419
|
if (hasDiscoveredTools) return "Reconnect";
|
|
386
420
|
return "Connect";
|
|
387
421
|
})();
|
|
388
422
|
|
|
389
423
|
const buttonIcon = (() => {
|
|
390
424
|
if (isOAuthBusy || isConnecting) return <Spinner />;
|
|
391
|
-
if (
|
|
425
|
+
if (showOAuthPrimary) return <OAuthIcon className="size-3.5" />;
|
|
392
426
|
if (hasDiscoveredTools) return <RefreshIcon className="size-3.5" />;
|
|
393
427
|
return <ConnectIcon className="size-3.5" />;
|
|
394
428
|
})();
|
|
@@ -402,6 +436,7 @@ function ConnectBar({
|
|
|
402
436
|
: "";
|
|
403
437
|
return `Tokens refresh automatically${hint}`;
|
|
404
438
|
}
|
|
439
|
+
if (manualOverride) return "Entering token manually";
|
|
405
440
|
if (hasDiscoveredTools) return formatConnectionSummary(toolCount, policyCount);
|
|
406
441
|
return "Not connected yet";
|
|
407
442
|
})();
|
|
@@ -410,37 +445,43 @@ function ConnectBar({
|
|
|
410
445
|
<div className="flex flex-col">
|
|
411
446
|
<div className="flex items-center justify-between px-3 py-2">
|
|
412
447
|
<div className="flex items-center gap-2">
|
|
413
|
-
{authMode === "oauth" && (
|
|
448
|
+
{authMode === "oauth" && !manualOverride && (
|
|
414
449
|
<span
|
|
415
450
|
className={cn(
|
|
416
451
|
"inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[10px] font-medium",
|
|
417
452
|
isOAuthConnected
|
|
418
453
|
? "bg-success/10 text-success"
|
|
419
|
-
:
|
|
454
|
+
: oauthSignInDisabled
|
|
455
|
+
? "bg-amber-500/10 text-amber-600 dark:text-amber-400"
|
|
456
|
+
: "bg-muted text-muted-foreground",
|
|
420
457
|
)}
|
|
421
458
|
>
|
|
422
459
|
<span
|
|
423
460
|
className={cn(
|
|
424
461
|
"size-1.5 rounded-full",
|
|
425
|
-
isOAuthConnected
|
|
462
|
+
isOAuthConnected
|
|
463
|
+
? "bg-success"
|
|
464
|
+
: oauthSignInDisabled
|
|
465
|
+
? "bg-amber-500"
|
|
466
|
+
: "bg-muted-foreground",
|
|
426
467
|
)}
|
|
427
468
|
aria-hidden="true"
|
|
428
469
|
/>
|
|
429
|
-
{isOAuthConnected ? "Connected" : "Not connected"}
|
|
470
|
+
{isOAuthConnected ? "Connected" : oauthSignInDisabled ? "Pending approval" : "Not connected"}
|
|
430
471
|
</span>
|
|
431
472
|
)}
|
|
432
473
|
<span className="text-xs text-muted-foreground">
|
|
433
|
-
{statusText}
|
|
474
|
+
{oauthSignInDisabled ? "OAuth sign-in is pending vendor approval" : statusText}
|
|
434
475
|
</span>
|
|
435
476
|
</div>
|
|
436
477
|
<button
|
|
437
478
|
type="button"
|
|
438
479
|
onClick={onConnect}
|
|
439
|
-
disabled={isConnecting || isOAuthBusy || credentialsLoading}
|
|
480
|
+
disabled={isConnecting || isOAuthBusy || credentialsLoading || oauthSignInDisabled}
|
|
440
481
|
data-cursor-target="connect-button"
|
|
441
482
|
className={cn(
|
|
442
483
|
"inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium",
|
|
443
|
-
|
|
484
|
+
showOAuthPrimary
|
|
444
485
|
? "bg-primary text-primary-foreground hover:bg-primary-hover"
|
|
445
486
|
: "border border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
|
|
446
487
|
"disabled:pointer-events-none disabled:opacity-50",
|
|
@@ -451,6 +492,53 @@ function ConnectBar({
|
|
|
451
492
|
</button>
|
|
452
493
|
</div>
|
|
453
494
|
|
|
495
|
+
{/* Vendor approval pending banner with docs link */}
|
|
496
|
+
{oauthSignInDisabled && (
|
|
497
|
+
<div className="flex items-start gap-2 border-t border-amber-500/20 bg-amber-500/5 px-3 py-2">
|
|
498
|
+
<WarningIcon className="mt-0.5 size-3.5 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
499
|
+
<div className="flex-1 text-xs text-amber-700 dark:text-amber-300">
|
|
500
|
+
<p>
|
|
501
|
+
The platform's OAuth app is awaiting vendor approval.
|
|
502
|
+
You can still connect by entering your own token manually.
|
|
503
|
+
</p>
|
|
504
|
+
{vendorApprovalDocsUrl && (
|
|
505
|
+
<a
|
|
506
|
+
href={vendorApprovalDocsUrl}
|
|
507
|
+
target="_blank"
|
|
508
|
+
rel="noopener noreferrer"
|
|
509
|
+
className="mt-1 inline-flex items-center gap-1 underline decoration-amber-600/40 underline-offset-2 hover:decoration-amber-600 dark:decoration-amber-400/40 dark:hover:decoration-amber-400"
|
|
510
|
+
>
|
|
511
|
+
Learn how to bring your own token
|
|
512
|
+
<ExternalLinkIcon className="size-3 shrink-0" />
|
|
513
|
+
</a>
|
|
514
|
+
)}
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
)}
|
|
518
|
+
|
|
519
|
+
{/* Secondary action: switch between OAuth and manual token entry */}
|
|
520
|
+
{authMode === "oauth" && !isOAuthConnected && !isOAuthBusy && !isConnecting && (
|
|
521
|
+
<div className="border-t border-border px-3 py-1.5">
|
|
522
|
+
{manualOverride ? (
|
|
523
|
+
<button
|
|
524
|
+
type="button"
|
|
525
|
+
onClick={onBackToOAuth}
|
|
526
|
+
className="text-[11px] text-muted-foreground underline decoration-muted-foreground/40 underline-offset-2 hover:text-foreground hover:decoration-foreground"
|
|
527
|
+
>
|
|
528
|
+
{isVendorApprovalPending ? "Back to OAuth status" : "Sign in with OAuth instead"}
|
|
529
|
+
</button>
|
|
530
|
+
) : (
|
|
531
|
+
<button
|
|
532
|
+
type="button"
|
|
533
|
+
onClick={onManualOverride}
|
|
534
|
+
className="text-[11px] text-muted-foreground underline decoration-muted-foreground/40 underline-offset-2 hover:text-foreground hover:decoration-foreground"
|
|
535
|
+
>
|
|
536
|
+
Enter token manually
|
|
537
|
+
</button>
|
|
538
|
+
)}
|
|
539
|
+
</div>
|
|
540
|
+
)}
|
|
541
|
+
|
|
454
542
|
{connectionError && (
|
|
455
543
|
<div className="flex items-start gap-2 border-t border-destructive/20 bg-destructive/5 px-3 py-2">
|
|
456
544
|
<WarningIcon className="mt-0.5 size-3.5 shrink-0 text-destructive" />
|
|
@@ -272,6 +272,9 @@ export function McpServerPicker({
|
|
|
272
272
|
? { type: "configure", serverKey: initialServerKey }
|
|
273
273
|
: { type: "list" },
|
|
274
274
|
);
|
|
275
|
+
const [manualOverrideKeys, setManualOverrideKeys] = useState<Set<string>>(
|
|
276
|
+
() => new Set(),
|
|
277
|
+
);
|
|
275
278
|
const searchRef = useRef<HTMLInputElement>(null);
|
|
276
279
|
const results_ = useScrollShadows();
|
|
277
280
|
const selected_ = useScrollShadows();
|
|
@@ -397,13 +400,15 @@ export function McpServerPicker({
|
|
|
397
400
|
const auth = entry.mcpServer.spec?.auth;
|
|
398
401
|
const oauthTargetEnvVar = auth?.targetEnvVar || null;
|
|
399
402
|
const hasOAuth = !!auth;
|
|
403
|
+
const isManualOverride = manualOverrideKeys.has(view.serverKey);
|
|
400
404
|
|
|
401
405
|
const entryMissingVars =
|
|
402
406
|
entry.status === "needsSetup" ? entry.missingVariables : [];
|
|
403
407
|
|
|
404
|
-
const filteredMissingVars =
|
|
405
|
-
|
|
406
|
-
|
|
408
|
+
const filteredMissingVars =
|
|
409
|
+
oauthTargetEnvVar && !isManualOverride
|
|
410
|
+
? entryMissingVars.filter((v) => v.key !== oauthTargetEnvVar)
|
|
411
|
+
: entryMissingVars;
|
|
407
412
|
|
|
408
413
|
const hasManualVars = filteredMissingVars.length > 0;
|
|
409
414
|
|
|
@@ -411,24 +416,55 @@ export function McpServerPicker({
|
|
|
411
416
|
? entryMissingVars.some((v) => v.key === oauthTargetEnvVar)
|
|
412
417
|
: false;
|
|
413
418
|
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
419
|
+
const vendorApprovalStatus = auth?.vendorApprovalStatus;
|
|
420
|
+
const isVendorApprovalPending =
|
|
421
|
+
hasOAuth && vendorApprovalStatus === 1; // VendorApprovalStatus.PENDING
|
|
422
|
+
|
|
423
|
+
const oauthSignInProps =
|
|
424
|
+
hasOAuth && !isManualOverride
|
|
425
|
+
? {
|
|
426
|
+
onSignIn: async () => {
|
|
427
|
+
if (!entry.mcpServer.metadata?.id) return;
|
|
428
|
+
try {
|
|
429
|
+
await oauth.startOAuth(
|
|
430
|
+
entry.mcpServer.metadata.id,
|
|
431
|
+
activeOrg ?? org,
|
|
432
|
+
);
|
|
433
|
+
setup.onServerAdded(ref);
|
|
434
|
+
} catch {
|
|
435
|
+
// error state managed by oauth hook
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
phase: oauth.phase,
|
|
439
|
+
isConnected: !oauthTokenMissing,
|
|
440
|
+
error: oauth.error,
|
|
441
|
+
onClearError: oauth.clearError,
|
|
442
|
+
isVendorApprovalPending,
|
|
443
|
+
vendorApprovalDocsUrl: auth?.vendorApprovalDocsUrl || null,
|
|
444
|
+
}
|
|
445
|
+
: undefined;
|
|
446
|
+
|
|
447
|
+
const handleSwitchToManual = hasOAuth
|
|
448
|
+
? () => {
|
|
449
|
+
setManualOverrideKeys((prev) => {
|
|
450
|
+
const next = new Set(prev);
|
|
451
|
+
next.add(view.serverKey);
|
|
452
|
+
return next;
|
|
453
|
+
});
|
|
429
454
|
}
|
|
430
455
|
: undefined;
|
|
431
456
|
|
|
457
|
+
const handleSwitchToOAuth =
|
|
458
|
+
hasOAuth && isManualOverride
|
|
459
|
+
? () => {
|
|
460
|
+
setManualOverrideKeys((prev) => {
|
|
461
|
+
const next = new Set(prev);
|
|
462
|
+
next.delete(view.serverKey);
|
|
463
|
+
return next;
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
: undefined;
|
|
467
|
+
|
|
432
468
|
return (
|
|
433
469
|
<div className={cn("w-72", className)}>
|
|
434
470
|
<McpServerConfigPanel
|
|
@@ -445,6 +481,8 @@ export function McpServerPicker({
|
|
|
445
481
|
}
|
|
446
482
|
: undefined
|
|
447
483
|
}
|
|
484
|
+
onSwitchToManual={handleSwitchToManual}
|
|
485
|
+
onSwitchToOAuth={handleSwitchToOAuth}
|
|
448
486
|
discoveredTools={entry.discoveredTools}
|
|
449
487
|
toolApprovals={entry.toolApprovals}
|
|
450
488
|
enabledTools={
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import { cn } from "@stigmer/theme";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
OAUTH_CALLBACK_MESSAGE_TYPE,
|
|
7
|
+
OAUTH_BROADCAST_CHANNEL,
|
|
8
|
+
} from "./useMcpServerOAuthConnect";
|
|
6
9
|
import type { OAuthCallbackMessage } from "./useMcpServerOAuthConnect";
|
|
7
10
|
|
|
8
11
|
/** Parameters extracted from the OAuth callback URL. */
|
|
@@ -92,28 +95,52 @@ export function OAuthCallbackHandler({
|
|
|
92
95
|
return;
|
|
93
96
|
}
|
|
94
97
|
|
|
98
|
+
const message: OAuthCallbackMessage = {
|
|
99
|
+
type: OAUTH_CALLBACK_MESSAGE_TYPE,
|
|
100
|
+
code,
|
|
101
|
+
state,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Always broadcast via BroadcastChannel — works even when
|
|
105
|
+
// Cross-Origin-Opener-Policy has severed window.opener.
|
|
106
|
+
let broadcastSent = false;
|
|
107
|
+
try {
|
|
108
|
+
const bc = new BroadcastChannel(OAUTH_BROADCAST_CHANNEL);
|
|
109
|
+
bc.postMessage(message);
|
|
110
|
+
bc.close();
|
|
111
|
+
broadcastSent = true;
|
|
112
|
+
} catch {
|
|
113
|
+
// BroadcastChannel unavailable — rely on postMessage below.
|
|
114
|
+
}
|
|
115
|
+
|
|
95
116
|
const opener = window.opener as Window | null;
|
|
96
117
|
if (opener && !opener.closed) {
|
|
97
|
-
const message: OAuthCallbackMessage = {
|
|
98
|
-
type: OAUTH_CALLBACK_MESSAGE_TYPE,
|
|
99
|
-
code,
|
|
100
|
-
state,
|
|
101
|
-
};
|
|
102
|
-
|
|
103
118
|
try {
|
|
104
119
|
opener.postMessage(message, window.location.origin);
|
|
105
120
|
setStatus("done");
|
|
106
121
|
window.close();
|
|
107
122
|
} catch {
|
|
108
|
-
|
|
109
|
-
"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
123
|
+
if (broadcastSent) {
|
|
124
|
+
setStatus("done");
|
|
125
|
+
window.close();
|
|
126
|
+
} else {
|
|
127
|
+
setErrorMessage(
|
|
128
|
+
"Could not communicate with the parent window. " +
|
|
129
|
+
"Please close this tab and try again.",
|
|
130
|
+
);
|
|
131
|
+
setStatus("error");
|
|
132
|
+
}
|
|
113
133
|
}
|
|
114
134
|
return;
|
|
115
135
|
}
|
|
116
136
|
|
|
137
|
+
// opener is null (COOP case) but BroadcastChannel delivered the message.
|
|
138
|
+
if (broadcastSent) {
|
|
139
|
+
setStatus("done");
|
|
140
|
+
window.close();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
117
144
|
if (onFallback) {
|
|
118
145
|
onFallback({ code, state });
|
|
119
146
|
setStatus("done");
|
|
@@ -6,7 +6,7 @@ import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/
|
|
|
6
6
|
import { ConnectInputSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
|
|
7
7
|
import type { EnvVarInput } from "@stigmer/sdk";
|
|
8
8
|
import { useStigmer } from "../hooks";
|
|
9
|
-
import {
|
|
9
|
+
import { resolveDeclaredSystemEnvVars } from "../environment/systemEnvVars";
|
|
10
10
|
import { toError } from "../internal/toError";
|
|
11
11
|
|
|
12
12
|
/** Return value of {@link useMcpServerConnect}. */
|
|
@@ -20,20 +20,30 @@ export interface UseMcpServerConnectReturn {
|
|
|
20
20
|
* (typically 5-15 seconds, ~30s timeout).
|
|
21
21
|
*
|
|
22
22
|
* Platform system env vars (`STIGMER_SERVER_ADDRESS`,
|
|
23
|
-
* `STIGMER_API_KEY`) are
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
* `STIGMER_API_KEY`) are injected only when the MCP server
|
|
24
|
+
* declares them in its `spec.env`. Pass `declaredEnvKeys` so the
|
|
25
|
+
* hook can determine which system vars the server actually needs.
|
|
26
|
+
* When omitted, no system vars are injected.
|
|
27
|
+
*
|
|
28
|
+
* When `runtimeEnv` is provided, those values are merged on top
|
|
29
|
+
* (caller values win). The backend merges the result on top of
|
|
30
|
+
* the user's personal environment so saved credentials (e.g.,
|
|
31
|
+
* OAuth tokens) are still resolved.
|
|
28
32
|
*
|
|
29
33
|
* @param mcpServerId - System-generated ID of the MCP server (metadata.id).
|
|
34
|
+
* @param org - The caller's active organization slug. Required for
|
|
35
|
+
* OAuth grant lookup and personal environment resolution.
|
|
30
36
|
* @param runtimeEnv - Optional additional environment variables.
|
|
37
|
+
* @param declaredEnvKeys - Keys from the server's `spec.env` declaration.
|
|
38
|
+
* System vars are only injected when declared here.
|
|
31
39
|
* @returns The updated McpServer with populated status.discovered_capabilities
|
|
32
40
|
* and status.tool_approvals.
|
|
33
41
|
*/
|
|
34
42
|
readonly connect: (
|
|
35
43
|
mcpServerId: string,
|
|
44
|
+
org: string,
|
|
36
45
|
runtimeEnv?: Record<string, EnvVarInput>,
|
|
46
|
+
declaredEnvKeys?: readonly string[],
|
|
37
47
|
) => Promise<McpServer>;
|
|
38
48
|
/** `true` while the connect RPC is in flight. */
|
|
39
49
|
readonly isConnecting: boolean;
|
|
@@ -52,10 +62,10 @@ export interface UseMcpServerConnectReturn {
|
|
|
52
62
|
* approval policy via a structured-output LLM call.
|
|
53
63
|
*
|
|
54
64
|
* Platform system env vars (`STIGMER_SERVER_ADDRESS`,
|
|
55
|
-
* `STIGMER_API_KEY`) are
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
65
|
+
* `STIGMER_API_KEY`) are injected only when the target MCP server
|
|
66
|
+
* declares them in `spec.env`. Pass the server's declared env keys
|
|
67
|
+
* via `declaredEnvKeys` so the hook knows which system vars to
|
|
68
|
+
* include. When not provided, no system vars are injected.
|
|
59
69
|
*
|
|
60
70
|
* Additional one-time credentials can be passed via `runtimeEnv`
|
|
61
71
|
* and will override both system vars and personal env values.
|
|
@@ -67,13 +77,15 @@ export interface UseMcpServerConnectReturn {
|
|
|
67
77
|
*
|
|
68
78
|
* // Saved credentials: already in personal environment
|
|
69
79
|
* async function handleConnectSaved() {
|
|
70
|
-
*
|
|
80
|
+
* const envKeys = Object.keys(mcpServer.spec?.env ?? {});
|
|
81
|
+
* await connect(mcpServer.metadata.id, undefined, envKeys);
|
|
71
82
|
* refetch();
|
|
72
83
|
* }
|
|
73
84
|
*
|
|
74
85
|
* // One-time use: pass credentials directly
|
|
75
86
|
* async function handleConnectTemporary(values: Record<string, EnvVarInput>) {
|
|
76
|
-
*
|
|
87
|
+
* const envKeys = Object.keys(mcpServer.spec?.env ?? {});
|
|
88
|
+
* await connect(mcpServer.metadata.id, values, envKeys);
|
|
77
89
|
* refetch();
|
|
78
90
|
* }
|
|
79
91
|
* ```
|
|
@@ -88,13 +100,17 @@ export function useMcpServerConnect(): UseMcpServerConnectReturn {
|
|
|
88
100
|
const connect = useCallback(
|
|
89
101
|
async (
|
|
90
102
|
mcpServerId: string,
|
|
103
|
+
org: string,
|
|
91
104
|
runtimeEnv?: Record<string, EnvVarInput>,
|
|
105
|
+
declaredEnvKeys?: readonly string[],
|
|
92
106
|
): Promise<McpServer> => {
|
|
93
107
|
setIsConnecting(true);
|
|
94
108
|
setError(null);
|
|
95
109
|
|
|
96
110
|
try {
|
|
97
|
-
const systemEnv =
|
|
111
|
+
const systemEnv = declaredEnvKeys
|
|
112
|
+
? await resolveDeclaredSystemEnvVars(stigmer, declaredEnvKeys)
|
|
113
|
+
: {};
|
|
98
114
|
const mergedEnv = { ...systemEnv, ...(runtimeEnv ?? {}) };
|
|
99
115
|
|
|
100
116
|
const runtimeEnvMap: Record<string, { value: string; isSecret: boolean }> = {};
|
|
@@ -107,7 +123,10 @@ export function useMcpServerConnect(): UseMcpServerConnectReturn {
|
|
|
107
123
|
|
|
108
124
|
const input = create(ConnectInputSchema, {
|
|
109
125
|
mcpServerId,
|
|
110
|
-
|
|
126
|
+
org,
|
|
127
|
+
...(Object.keys(runtimeEnvMap).length > 0
|
|
128
|
+
? { runtimeEnv: runtimeEnvMap }
|
|
129
|
+
: {}),
|
|
111
130
|
});
|
|
112
131
|
|
|
113
132
|
return await stigmer.mcpServer.connect(input);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useCallback, useMemo } from "react";
|
|
3
|
+
import { useCallback, useMemo, useState } from "react";
|
|
4
4
|
import type { EnvVarInput } from "@stigmer/sdk";
|
|
5
5
|
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
6
|
+
import { VendorApprovalStatus } from "@stigmer/protos/ai/stigmer/iam/oauthapp/v1/spec_pb";
|
|
6
7
|
import { usePersonalEnvironment } from "../environment/usePersonalEnvironment";
|
|
7
8
|
import { diffEnv } from "../environment/diffEnv";
|
|
8
9
|
import { SYSTEM_ENV_VAR_KEYS } from "../environment/systemEnvVars";
|
|
@@ -91,6 +92,35 @@ export interface UseMcpServerCredentialsReturn {
|
|
|
91
92
|
readonly isSaving: boolean;
|
|
92
93
|
/** Re-check the personal environment. */
|
|
93
94
|
readonly refetch: () => void;
|
|
95
|
+
/**
|
|
96
|
+
* `true` when the referenced OAuthApp's vendor approval is still pending.
|
|
97
|
+
* When pending, the platform-managed OAuth sign-in flow is unavailable
|
|
98
|
+
* and the sign-in button should be disabled. Users can still connect
|
|
99
|
+
* via manual token entry (manual override).
|
|
100
|
+
*/
|
|
101
|
+
readonly isVendorApprovalPending: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Documentation URL for users who want to bring their own OAuth
|
|
104
|
+
* credentials while the platform's OAuth app is pending vendor approval.
|
|
105
|
+
* `null` when no documentation link is available.
|
|
106
|
+
*/
|
|
107
|
+
readonly vendorApprovalDocsUrl: string | null;
|
|
108
|
+
/**
|
|
109
|
+
* When `true`, the user has opted to bypass OAuth and enter the
|
|
110
|
+
* `target_env_var` token manually. In this state:
|
|
111
|
+
*
|
|
112
|
+
* - {@link missingVariables} includes the OAuth-managed variable
|
|
113
|
+
* - {@link isReady} no longer requires an active OAuth grant
|
|
114
|
+
*
|
|
115
|
+
* Only meaningful when `authMode` is `"oauth"`. Has no effect on
|
|
116
|
+
* manual-only servers.
|
|
117
|
+
*/
|
|
118
|
+
readonly manualOverride: boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Toggle the manual override. Pass `true` to switch from OAuth to
|
|
121
|
+
* manual token entry; `false` to revert to the OAuth flow.
|
|
122
|
+
*/
|
|
123
|
+
readonly setManualOverride: (override: boolean) => void;
|
|
94
124
|
}
|
|
95
125
|
|
|
96
126
|
/**
|
|
@@ -121,9 +151,31 @@ export interface UseMcpServerCredentialsReturn {
|
|
|
121
151
|
* ```tsx
|
|
122
152
|
* const creds = useMcpServerCredentials("acme", mcpServer);
|
|
123
153
|
*
|
|
124
|
-
* // OAuth server — sign-in button +
|
|
154
|
+
* // OAuth server — sign-in button + manual override escape hatch
|
|
125
155
|
* if (creds.authMode === "oauth" && !creds.isOAuthConnected) {
|
|
126
|
-
*
|
|
156
|
+
* if (creds.manualOverride) {
|
|
157
|
+
* // User opted to enter the token manually
|
|
158
|
+
* return (
|
|
159
|
+
* <>
|
|
160
|
+
* <EnvVarForm
|
|
161
|
+
* variables={creds.missingVariables}
|
|
162
|
+
* onSubmit={(values) => creds.saveCredentials(values)}
|
|
163
|
+
* isSubmitting={creds.isSaving}
|
|
164
|
+
* />
|
|
165
|
+
* <button onClick={() => creds.setManualOverride(false)}>
|
|
166
|
+
* Sign in with OAuth instead
|
|
167
|
+
* </button>
|
|
168
|
+
* </>
|
|
169
|
+
* );
|
|
170
|
+
* }
|
|
171
|
+
* return (
|
|
172
|
+
* <>
|
|
173
|
+
* <button onClick={startOAuth}>Sign in</button>
|
|
174
|
+
* <button onClick={() => creds.setManualOverride(true)}>
|
|
175
|
+
* Enter token manually
|
|
176
|
+
* </button>
|
|
177
|
+
* </>
|
|
178
|
+
* );
|
|
127
179
|
* }
|
|
128
180
|
*
|
|
129
181
|
* // Manual vars still needed (mixed mode or manual-only)
|
|
@@ -144,12 +196,18 @@ export function useMcpServerCredentials(
|
|
|
144
196
|
mcpServer: McpServer | null,
|
|
145
197
|
): UseMcpServerCredentialsReturn {
|
|
146
198
|
const personalEnv = usePersonalEnvironment(org);
|
|
199
|
+
const [manualOverride, setManualOverride] = useState(false);
|
|
147
200
|
|
|
148
201
|
const auth = mcpServer?.spec?.auth;
|
|
149
202
|
const authMode: McpServerAuthMode = auth ? "oauth" : "manual";
|
|
150
203
|
const oauthTargetEnvVar = auth?.targetEnvVar || null;
|
|
151
204
|
const tokenLifetimeHint = auth?.tokenLifetimeHint || null;
|
|
152
205
|
|
|
206
|
+
const isVendorApprovalPending =
|
|
207
|
+
authMode === "oauth" &&
|
|
208
|
+
auth?.vendorApprovalStatus === VendorApprovalStatus.PENDING;
|
|
209
|
+
const vendorApprovalDocsUrl = auth?.vendorApprovalDocsUrl || null;
|
|
210
|
+
|
|
153
211
|
const grantStatus = useOAuthGrantStatus(
|
|
154
212
|
authMode === "oauth" ? (mcpServer?.metadata?.id ?? null) : null,
|
|
155
213
|
authMode === "oauth" ? org : null,
|
|
@@ -178,15 +236,15 @@ export function useMcpServerCredentials(
|
|
|
178
236
|
);
|
|
179
237
|
|
|
180
238
|
const missingVariables = useMemo(() => {
|
|
181
|
-
if (!oauthTargetEnvVar) return requiredMissing;
|
|
239
|
+
if (!oauthTargetEnvVar || manualOverride) return requiredMissing;
|
|
182
240
|
return requiredMissing.filter((v) => v.key !== oauthTargetEnvVar);
|
|
183
|
-
}, [requiredMissing, oauthTargetEnvVar]);
|
|
241
|
+
}, [requiredMissing, oauthTargetEnvVar, manualOverride]);
|
|
184
242
|
|
|
185
243
|
const isReady =
|
|
186
244
|
!personalEnv.isLoading &&
|
|
187
245
|
!grantStatus.isLoading &&
|
|
188
246
|
missingVariables.length === 0 &&
|
|
189
|
-
(authMode === "manual" || isOAuthConnected);
|
|
247
|
+
(authMode === "manual" || manualOverride || isOAuthConnected);
|
|
190
248
|
|
|
191
249
|
const saveCredentials = useCallback(
|
|
192
250
|
async (values: Record<string, EnvVarInput>): Promise<void> => {
|
|
@@ -207,6 +265,8 @@ export function useMcpServerCredentials(
|
|
|
207
265
|
isOAuthConnected,
|
|
208
266
|
accessTokenExpiresAt: grantStatus.accessTokenExpiresAt,
|
|
209
267
|
tokenLifetimeHint,
|
|
268
|
+
isVendorApprovalPending,
|
|
269
|
+
vendorApprovalDocsUrl,
|
|
210
270
|
missingVariables,
|
|
211
271
|
isReady,
|
|
212
272
|
isLoading: personalEnv.isLoading || grantStatus.isLoading,
|
|
@@ -214,5 +274,7 @@ export function useMcpServerCredentials(
|
|
|
214
274
|
saveCredentials,
|
|
215
275
|
isSaving: personalEnv.isMutating,
|
|
216
276
|
refetch,
|
|
277
|
+
manualOverride,
|
|
278
|
+
setManualOverride,
|
|
217
279
|
};
|
|
218
280
|
}
|