@stigmer/react 0.0.77 → 0.0.78

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 (40) hide show
  1. package/index.d.ts +2 -2
  2. package/index.d.ts.map +1 -1
  3. package/index.js +2 -2
  4. package/index.js.map +1 -1
  5. package/mcp-server/McpServerConfigPanel.d.ts +28 -1
  6. package/mcp-server/McpServerConfigPanel.d.ts.map +1 -1
  7. package/mcp-server/McpServerConfigPanel.js +23 -2
  8. package/mcp-server/McpServerConfigPanel.js.map +1 -1
  9. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  10. package/mcp-server/McpServerDetailView.js +92 -13
  11. package/mcp-server/McpServerDetailView.js.map +1 -1
  12. package/mcp-server/McpServerPicker.d.ts.map +1 -1
  13. package/mcp-server/McpServerPicker.js +34 -2
  14. package/mcp-server/McpServerPicker.js.map +1 -1
  15. package/mcp-server/OAuthCallbackHandler.d.ts +52 -0
  16. package/mcp-server/OAuthCallbackHandler.d.ts.map +1 -0
  17. package/mcp-server/OAuthCallbackHandler.js +98 -0
  18. package/mcp-server/OAuthCallbackHandler.js.map +1 -0
  19. package/mcp-server/index.d.ts +6 -2
  20. package/mcp-server/index.d.ts.map +1 -1
  21. package/mcp-server/index.js +2 -0
  22. package/mcp-server/index.js.map +1 -1
  23. package/mcp-server/useMcpServerCredentials.d.ts +59 -7
  24. package/mcp-server/useMcpServerCredentials.d.ts.map +1 -1
  25. package/mcp-server/useMcpServerCredentials.js +37 -10
  26. package/mcp-server/useMcpServerCredentials.js.map +1 -1
  27. package/mcp-server/useMcpServerOAuthConnect.d.ts +81 -0
  28. package/mcp-server/useMcpServerOAuthConnect.d.ts.map +1 -0
  29. package/mcp-server/useMcpServerOAuthConnect.js +187 -0
  30. package/mcp-server/useMcpServerOAuthConnect.js.map +1 -0
  31. package/package.json +4 -4
  32. package/src/index.ts +9 -1
  33. package/src/mcp-server/McpServerConfigPanel.tsx +153 -3
  34. package/src/mcp-server/McpServerDetailView.tsx +231 -172
  35. package/src/mcp-server/McpServerPicker.tsx +40 -2
  36. package/src/mcp-server/OAuthCallbackHandler.tsx +237 -0
  37. package/src/mcp-server/index.ts +17 -1
  38. package/src/mcp-server/useMcpServerCredentials.ts +86 -13
  39. package/src/mcp-server/useMcpServerOAuthConnect.ts +298 -0
  40. package/styles.css +1 -1
@@ -8,13 +8,15 @@ import type {
8
8
  DiscoveredTool,
9
9
  DiscoveredResourceTemplate,
10
10
  } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/status_pb";
11
- import type { ToolApprovalPolicy, McpServerSource } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
11
+ import type { ToolApprovalPolicy, McpServerSpec } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
12
12
  import { ValidationState } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/status_pb";
13
13
  import type { EnvironmentValue } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
14
14
  import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
15
15
  import { useMcpServer } from "./useMcpServer";
16
16
  import { useMcpServerConnect } from "./useMcpServerConnect";
17
17
  import { useMcpServerCredentials } from "./useMcpServerCredentials";
18
+ import { useMcpServerOAuthConnect } from "./useMcpServerOAuthConnect";
19
+ import type { OAuthConnectPhase } from "./useMcpServerOAuthConnect";
18
20
  import { ErrorMessage } from "../error/ErrorMessage";
19
21
  import { EnvVarForm } from "../environment/EnvVarForm";
20
22
  import type { EnvVarFormVariable } from "../environment/EnvVarForm";
@@ -113,6 +115,7 @@ export function McpServerDetailView({
113
115
  const { mcpServer, isLoading, error, refetch } = useMcpServer(org, slug);
114
116
  const credentials = useMcpServerCredentials(org, mcpServer ?? null);
115
117
  const connection = useMcpServerConnect();
118
+ const oauth = useMcpServerOAuthConnect();
116
119
 
117
120
  const [showCredentialForm, setShowCredentialForm] = useState(defaultShowCredentialForm);
118
121
  const [capabilityTab, setCapabilityTab] = useState<CapabilityTab>(defaultCapabilityTab);
@@ -126,9 +129,26 @@ export function McpServerDetailView({
126
129
  }
127
130
  }, [mcpServer]);
128
131
 
132
+ const handleOAuthSignIn = useCallback(async () => {
133
+ if (!mcpServer?.metadata?.id) return;
134
+
135
+ try {
136
+ await oauth.startOAuth(mcpServer.metadata.id);
137
+ credentials.refetch();
138
+ refetch();
139
+ } catch {
140
+ // error state is managed by the oauth hook
141
+ }
142
+ }, [mcpServer, oauth, credentials, refetch]);
143
+
129
144
  const handleConnectClick = useCallback(async () => {
130
145
  if (!mcpServer?.metadata?.id) return;
131
146
 
147
+ if (credentials.authMode === "oauth" && !credentials.isOAuthConnected) {
148
+ handleOAuthSignIn();
149
+ return;
150
+ }
151
+
132
152
  if (!credentials.isReady) {
133
153
  setShowCredentialForm(true);
134
154
  return;
@@ -140,7 +160,7 @@ export function McpServerDetailView({
140
160
  } catch {
141
161
  // error state is managed by the hook
142
162
  }
143
- }, [mcpServer, credentials.isReady, connection, refetch]);
163
+ }, [mcpServer, credentials.authMode, credentials.isOAuthConnected, credentials.isReady, connection, refetch, handleOAuthSignIn]);
144
164
 
145
165
  const handleCredentialSubmit = useCallback(
146
166
  async (
@@ -172,8 +192,7 @@ export function McpServerDetailView({
172
192
 
173
193
  const spec = mcpServer?.spec;
174
194
  const status = mcpServer?.status;
175
- const source = spec?.source;
176
- const hasSource = source && (source.registry || source.repositoryUrl);
195
+ const hasSource = spec && (spec.repositoryUrl || spec.githubStars > 0);
177
196
  const specAudit = status?.audit?.specAudit;
178
197
  const capabilities = status?.discoveredCapabilities;
179
198
  const pinnedPolicies = spec?.pinnedToolApprovals ?? [];
@@ -198,6 +217,12 @@ export function McpServerDetailView({
198
217
  return items;
199
218
  }, [tools.length, totalPolicyCount, resourceTemplates.length]);
200
219
 
220
+ const combinedError = connection.error ?? oauth.error;
221
+ const combinedClearError = useCallback(() => {
222
+ connection.clearError();
223
+ oauth.clearError();
224
+ }, [connection, oauth]);
225
+
201
226
  if (isLoading) return <LoadingSkeleton className={className} />;
202
227
  if (error)
203
228
  return <ErrorMessage error={error} retry={refetch} className={className} />;
@@ -227,74 +252,78 @@ export function McpServerDetailView({
227
252
  isVisibilityPending={isVisibilityPending}
228
253
  />
229
254
 
230
- {hasSource && <SourceSection source={source} />}
255
+ {hasSource && <SourceSection spec={spec} />}
231
256
 
232
257
  {spec?.serverType.case && (
233
258
  <ServerConfigSection serverType={spec.serverType} />
234
259
  )}
235
260
 
236
261
  {spec?.envSpec && Object.keys(spec.envSpec.data).length > 0 && (
237
- <EnvSpecSection data={spec.envSpec.data} />
262
+ <EnvSpecSection
263
+ data={spec.envSpec.data}
264
+ oauthTargetEnvVar={credentials.oauthTargetEnvVar}
265
+ />
238
266
  )}
239
267
 
240
- <section>
241
- <h3 className="mb-2 text-xs font-medium uppercase tracking-wider text-muted-foreground">
242
- Capabilities
243
- </h3>
244
- <div className="overflow-hidden rounded-lg border border-border">
245
- <ConnectBar
246
- isConnecting={connection.isConnecting}
247
- connectionError={connection.error}
248
- onConnect={handleConnectClick}
249
- onClearConnectionError={connection.clearError}
250
- hasDiscoveredTools={hasDiscoveredTools}
251
- toolCount={tools.length}
252
- policyCount={totalPolicyCount}
253
- credentialsLoading={credentials.isLoading}
254
- />
255
-
256
- {showCredentialForm && credentials.missingVariables.length > 0 && (
257
- <div
258
- className="border-b border-border p-4"
259
- data-cursor-target="credential-form"
260
- >
261
- <EnvVarForm
262
- title="Credentials Required"
263
- description="Enter the credentials needed to connect to this MCP server. Toggle &quot;Save for future runs&quot; to persist them in your personal environment, or leave it off for one-time use."
264
- variables={credentials.missingVariables}
265
- onSubmit={(values, options) => handleCredentialSubmit(values, options)}
266
- onCancel={() => setShowCredentialForm(false)}
267
- isSubmitting={credentials.isSaving}
268
- poolValues={credentialPoolValues}
269
- className="w-full max-w-md"
270
- />
271
- </div>
272
- )}
268
+ <Section title="Connection">
269
+ <ConnectBar
270
+ isConnecting={connection.isConnecting || oauth.isInProgress}
271
+ connectionError={combinedError}
272
+ onConnect={handleConnectClick}
273
+ onClearConnectionError={combinedClearError}
274
+ hasDiscoveredTools={hasDiscoveredTools}
275
+ toolCount={tools.length}
276
+ policyCount={totalPolicyCount}
277
+ credentialsLoading={credentials.isLoading}
278
+ oauthPhase={oauth.phase}
279
+ authMode={credentials.authMode}
280
+ isOAuthConnected={credentials.isOAuthConnected}
281
+ tokenLifetimeHint={credentials.tokenLifetimeHint}
282
+ />
273
283
 
274
- <Tabs
275
- tabs={capabilityTabs}
276
- activeTab={capabilityTab}
277
- onTabChange={(id) => setCapabilityTab(id as CapabilityTab)}
278
- aria-label="MCP server capabilities"
284
+ {showCredentialForm && credentials.missingVariables.length > 0 && (
285
+ <div
286
+ className="border-b border-border p-4"
287
+ data-cursor-target="credential-form"
279
288
  >
280
- {capabilityTab === "tools" && (
281
- <ToolsTabContent tools={tools} />
282
- )}
289
+ <EnvVarForm
290
+ title="Credentials Required"
291
+ description="Enter the credentials needed to connect to this MCP server. Toggle &quot;Save for future runs&quot; to persist them in your personal environment, or leave it off for one-time use."
292
+ variables={credentials.missingVariables}
293
+ onSubmit={(values, options) => handleCredentialSubmit(values, options)}
294
+ onCancel={() => setShowCredentialForm(false)}
295
+ isSubmitting={credentials.isSaving}
296
+ poolValues={credentialPoolValues}
297
+ className="w-full max-w-md"
298
+ />
299
+ </div>
300
+ )}
301
+ </Section>
302
+
303
+ <Section title="Capabilities">
304
+ <Tabs
305
+ tabs={capabilityTabs}
306
+ activeTab={capabilityTab}
307
+ onTabChange={(id) => setCapabilityTab(id as CapabilityTab)}
308
+ aria-label="MCP server capabilities"
309
+ >
310
+ {capabilityTab === "tools" && (
311
+ <ToolsTabContent tools={tools} />
312
+ )}
283
313
 
284
- {capabilityTab === "policies" && (
285
- <PoliciesTabContent
286
- pinnedPolicies={pinnedPolicies}
287
- classifiedPolicies={classifiedPolicies}
288
- hasDiscoveredTools={hasDiscoveredTools}
289
- />
290
- )}
314
+ {capabilityTab === "policies" && (
315
+ <PoliciesTabContent
316
+ pinnedPolicies={pinnedPolicies}
317
+ classifiedPolicies={classifiedPolicies}
318
+ hasDiscoveredTools={hasDiscoveredTools}
319
+ />
320
+ )}
291
321
 
292
- {capabilityTab === "resources" && (
293
- <ResourceTemplatesList templates={resourceTemplates} />
294
- )}
295
- </Tabs>
296
- </div>
297
- </section>
322
+ {capabilityTab === "resources" && (
323
+ <ResourceTemplatesList templates={resourceTemplates} />
324
+ )}
325
+ </Tabs>
326
+ </Section>
298
327
 
299
328
  {spec && spec.tags.length > 0 && <TagsSection tags={spec.tags} />}
300
329
  </div>
@@ -314,6 +343,10 @@ function ConnectBar({
314
343
  toolCount,
315
344
  policyCount,
316
345
  credentialsLoading,
346
+ oauthPhase,
347
+ authMode,
348
+ isOAuthConnected,
349
+ tokenLifetimeHint,
317
350
  }: {
318
351
  readonly isConnecting: boolean;
319
352
  readonly connectionError: Error | null;
@@ -323,48 +356,90 @@ function ConnectBar({
323
356
  readonly toolCount: number;
324
357
  readonly policyCount: number;
325
358
  readonly credentialsLoading: boolean;
359
+ readonly oauthPhase: OAuthConnectPhase;
360
+ readonly authMode: "manual" | "oauth";
361
+ readonly isOAuthConnected: boolean;
362
+ readonly tokenLifetimeHint: string | null;
326
363
  }) {
364
+ const isOAuthBusy =
365
+ oauthPhase === "initiating" ||
366
+ oauthPhase === "awaiting-callback" ||
367
+ oauthPhase === "completing" ||
368
+ oauthPhase === "connecting";
369
+
370
+ const buttonLabel = (() => {
371
+ if (isOAuthBusy) return oauthPhaseLabel(oauthPhase);
372
+ if (isConnecting) return "Connecting...";
373
+ if (authMode === "oauth" && !isOAuthConnected) return "Sign in to connect";
374
+ if (hasDiscoveredTools) return "Reconnect";
375
+ return "Connect";
376
+ })();
377
+
378
+ const buttonIcon = (() => {
379
+ if (isOAuthBusy || isConnecting) return <Spinner />;
380
+ if (authMode === "oauth" && !isOAuthConnected) return <OAuthIcon className="size-3.5" />;
381
+ if (hasDiscoveredTools) return <RefreshIcon className="size-3.5" />;
382
+ return <ConnectIcon className="size-3.5" />;
383
+ })();
384
+
385
+ const statusText = (() => {
386
+ if (authMode === "oauth" && isOAuthConnected) {
387
+ const hint = tokenLifetimeHint && tokenLifetimeHint !== "never"
388
+ ? ` \u00B7 Session lasts ~${tokenLifetimeHint}`
389
+ : "";
390
+ return `Tokens refresh automatically${hint}`;
391
+ }
392
+ if (hasDiscoveredTools) return formatConnectionSummary(toolCount, policyCount);
393
+ return "Not connected yet";
394
+ })();
395
+
327
396
  return (
328
397
  <div className="flex flex-col">
329
- <div className="flex items-center justify-between border-b border-border px-3 py-2">
330
- <span className="text-xs text-muted-foreground">
331
- {hasDiscoveredTools
332
- ? formatConnectionSummary(toolCount, policyCount)
333
- : "Not connected yet"}
334
- </span>
398
+ <div className="flex items-center justify-between px-3 py-2">
399
+ <div className="flex items-center gap-2">
400
+ {authMode === "oauth" && (
401
+ <span
402
+ className={cn(
403
+ "inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[10px] font-medium",
404
+ isOAuthConnected
405
+ ? "bg-success/10 text-success"
406
+ : "bg-muted text-muted-foreground",
407
+ )}
408
+ >
409
+ <span
410
+ className={cn(
411
+ "size-1.5 rounded-full",
412
+ isOAuthConnected ? "bg-success" : "bg-muted-foreground",
413
+ )}
414
+ aria-hidden="true"
415
+ />
416
+ {isOAuthConnected ? "Connected" : "Not connected"}
417
+ </span>
418
+ )}
419
+ <span className="text-xs text-muted-foreground">
420
+ {statusText}
421
+ </span>
422
+ </div>
335
423
  <button
336
424
  type="button"
337
425
  onClick={onConnect}
338
- disabled={isConnecting || credentialsLoading}
426
+ disabled={isConnecting || isOAuthBusy || credentialsLoading}
339
427
  data-cursor-target="connect-button"
340
428
  className={cn(
341
429
  "inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium",
342
- "border border-border bg-background text-foreground",
343
- "hover:bg-accent hover:text-accent-foreground",
430
+ authMode === "oauth" && !isOAuthConnected
431
+ ? "bg-primary text-primary-foreground hover:bg-primary-hover"
432
+ : "border border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
344
433
  "disabled:pointer-events-none disabled:opacity-50",
345
434
  )}
346
435
  >
347
- {isConnecting ? (
348
- <>
349
- <Spinner />
350
- Connecting...
351
- </>
352
- ) : hasDiscoveredTools ? (
353
- <>
354
- <RefreshIcon className="size-3.5" />
355
- Reconnect
356
- </>
357
- ) : (
358
- <>
359
- <ConnectIcon className="size-3.5" />
360
- Connect
361
- </>
362
- )}
436
+ {buttonIcon}
437
+ {buttonLabel}
363
438
  </button>
364
439
  </div>
365
440
 
366
441
  {connectionError && (
367
- <div className="flex items-start gap-2 border-b border-destructive/20 bg-destructive/5 px-3 py-2">
442
+ <div className="flex items-start gap-2 border-t border-destructive/20 bg-destructive/5 px-3 py-2">
368
443
  <WarningIcon className="mt-0.5 size-3.5 shrink-0 text-destructive" />
369
444
  <p className="flex-1 text-xs text-destructive">
370
445
  {connectionError.message}
@@ -383,6 +458,21 @@ function ConnectBar({
383
458
  );
384
459
  }
385
460
 
461
+ function oauthPhaseLabel(phase: OAuthConnectPhase): string {
462
+ switch (phase) {
463
+ case "initiating":
464
+ return "Starting sign-in...";
465
+ case "awaiting-callback":
466
+ return "Waiting for authorization...";
467
+ case "completing":
468
+ return "Completing sign-in...";
469
+ case "connecting":
470
+ return "Discovering tools...";
471
+ default:
472
+ return "Connecting...";
473
+ }
474
+ }
475
+
386
476
  function formatConnectionSummary(toolCount: number, policyCount: number): string {
387
477
  const toolLabel = `${toolCount} tool${toolCount !== 1 ? "s" : ""}`;
388
478
  if (policyCount === 0) return toolLabel;
@@ -463,6 +553,11 @@ function Header({
463
553
  )
464
554
  )}
465
555
  </div>
556
+ {meta?.slug && (
557
+ <span className="mt-0.5 block truncate font-mono text-xs text-muted-foreground">
558
+ {meta.org ? `${meta.org}/${meta.slug}` : meta.slug}
559
+ </span>
560
+ )}
466
561
  <div className="mt-0.5 flex flex-wrap items-center gap-x-1.5 text-xs text-muted-foreground">
467
562
  {meta?.org && <span>{meta.org}</span>}
468
563
  {status && (
@@ -597,101 +692,36 @@ function ServerConfigSection({
597
692
  }
598
693
 
599
694
  function SourceSection({
600
- source,
695
+ spec,
601
696
  }: {
602
- readonly source: McpServerSource;
697
+ readonly spec: McpServerSpec;
603
698
  }) {
604
699
  return (
605
700
  <Section title="Source">
606
701
  <div className="flex flex-col gap-2 p-3">
607
- {source.registry && (
608
- <div className="flex items-baseline gap-2">
609
- <span className="shrink-0 text-xs font-medium text-muted-foreground">
610
- Registry
611
- </span>
612
- <span className="text-xs text-foreground">{source.registry}</span>
613
- </div>
614
- )}
615
- {source.registryName && (
616
- <div className="flex items-baseline gap-2">
617
- <span className="shrink-0 text-xs font-medium text-muted-foreground">
618
- Name
619
- </span>
620
- <span className="font-mono text-xs text-foreground">
621
- {source.registryName}
622
- </span>
623
- </div>
624
- )}
625
- {source.version && (
626
- <div className="flex items-baseline gap-2">
627
- <span className="shrink-0 text-xs font-medium text-muted-foreground">
628
- Version
629
- </span>
630
- <span className="font-mono text-xs text-foreground">
631
- {source.version}
632
- </span>
633
- </div>
634
- )}
635
- {source.qualityTier && (
636
- <div className="flex items-baseline gap-2">
637
- <span className="shrink-0 text-xs font-medium text-muted-foreground">
638
- Quality
639
- </span>
640
- <span className="inline-flex items-baseline gap-1.5 text-xs">
641
- <span className="rounded bg-muted px-1.5 py-0.5 font-medium capitalize text-foreground">
642
- {source.qualityTier}
643
- </span>
644
- {source.qualityScore > 0 && (
645
- <span className="text-muted-foreground">
646
- {source.qualityScore} / 100
647
- </span>
648
- )}
649
- </span>
650
- </div>
651
- )}
652
- {source.subcategory && (
653
- <div className="flex items-baseline gap-2">
654
- <span className="shrink-0 text-xs font-medium text-muted-foreground">
655
- Category
656
- </span>
657
- <span className="text-xs text-foreground">
658
- {source.subcategory}
659
- </span>
660
- </div>
661
- )}
662
- {source.repositoryUrl && (
702
+ {spec.repositoryUrl && (
663
703
  <div className="flex items-baseline gap-2">
664
704
  <span className="shrink-0 text-xs font-medium text-muted-foreground">
665
705
  Repository
666
706
  </span>
667
707
  <a
668
- href={source.repositoryUrl}
708
+ href={spec.repositoryUrl}
669
709
  target="_blank"
670
710
  rel="noopener noreferrer"
671
711
  className="inline-flex items-center gap-1 break-all font-mono text-xs text-foreground underline decoration-muted-foreground/40 underline-offset-2 hover:decoration-foreground"
672
712
  >
673
- {source.repositoryUrl}
713
+ {spec.repositoryUrl}
674
714
  <ExternalLinkIcon className="size-3 shrink-0" />
675
715
  </a>
676
716
  </div>
677
717
  )}
678
- {source.githubStars > 0 && (
718
+ {spec.githubStars > 0 && (
679
719
  <div className="flex items-baseline gap-2">
680
720
  <span className="shrink-0 text-xs font-medium text-muted-foreground">
681
721
  Stars
682
722
  </span>
683
723
  <span className="text-xs text-foreground">
684
- {source.githubStars.toLocaleString()}
685
- </span>
686
- </div>
687
- )}
688
- {source.lastSyncedAt && (
689
- <div className="flex items-baseline gap-2">
690
- <span className="shrink-0 text-xs font-medium text-muted-foreground">
691
- Last Synced
692
- </span>
693
- <span className="text-xs text-foreground">
694
- {formatDate(timestampDate(source.lastSyncedAt))}
724
+ {spec.githubStars.toLocaleString()}
695
725
  </span>
696
726
  </div>
697
727
  )}
@@ -732,8 +762,10 @@ function ResourceTemplatesList({
732
762
 
733
763
  function EnvSpecSection({
734
764
  data,
765
+ oauthTargetEnvVar,
735
766
  }: {
736
767
  readonly data: { [key: string]: EnvironmentValue };
768
+ readonly oauthTargetEnvVar: string | null;
737
769
  }) {
738
770
  const entries = Object.entries(data).sort(([a], [b]) =>
739
771
  a.localeCompare(b),
@@ -742,21 +774,29 @@ function EnvSpecSection({
742
774
  return (
743
775
  <Section title={`Environment Variables (${entries.length})`}>
744
776
  <div className="flex flex-col divide-y divide-border">
745
- {entries.map(([name, env]) => (
746
- <div key={name} className="flex items-start gap-3 px-3 py-2">
747
- <code className="shrink-0 font-mono text-sm font-medium text-foreground">
748
- {name}
749
- </code>
750
- <span className="shrink-0 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
751
- {env.isSecret ? "secret" : "config"}
752
- </span>
753
- {env.description && (
754
- <span className="text-xs text-muted-foreground">
755
- {env.description}
777
+ {entries.map(([name, env]) => {
778
+ const isOAuthManaged = name === oauthTargetEnvVar;
779
+ return (
780
+ <div key={name} className="flex items-start gap-3 px-3 py-2">
781
+ <code className="shrink-0 font-mono text-sm font-medium text-foreground">
782
+ {name}
783
+ </code>
784
+ <span className="shrink-0 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
785
+ {env.isSecret ? "secret" : "config"}
756
786
  </span>
757
- )}
758
- </div>
759
- ))}
787
+ {isOAuthManaged && (
788
+ <span className="shrink-0 rounded bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium text-primary">
789
+ oauth
790
+ </span>
791
+ )}
792
+ {env.description && (
793
+ <span className="text-xs text-muted-foreground">
794
+ {env.description}
795
+ </span>
796
+ )}
797
+ </div>
798
+ );
799
+ })}
760
800
  </div>
761
801
  </Section>
762
802
  );
@@ -1154,6 +1194,25 @@ function SparklesIcon({ className }: { readonly className?: string }) {
1154
1194
  );
1155
1195
  }
1156
1196
 
1197
+ function OAuthIcon({ className }: { readonly className?: string }) {
1198
+ return (
1199
+ <svg
1200
+ className={className}
1201
+ viewBox="0 0 16 16"
1202
+ fill="none"
1203
+ stroke="currentColor"
1204
+ strokeWidth="1.5"
1205
+ strokeLinecap="round"
1206
+ strokeLinejoin="round"
1207
+ aria-hidden="true"
1208
+ >
1209
+ <rect x="1.5" y="5" width="13" height="8" rx="1.5" />
1210
+ <path d="M4.5 5V3.5a3.5 3.5 0 0 1 7 0V5" />
1211
+ <circle cx="8" cy="9.5" r="1.25" />
1212
+ </svg>
1213
+ );
1214
+ }
1215
+
1157
1216
  function ExternalLinkIcon({ className }: { readonly className?: string }) {
1158
1217
  return (
1159
1218
  <svg
@@ -19,6 +19,7 @@ import { useScrollShadows } from "../internal/useScrollShadows";
19
19
  import { ScrollFade } from "../internal/ScrollFade";
20
20
  import { McpServerConfigPanel } from "./McpServerConfigPanel";
21
21
  import type { McpServerSetupEntry } from "./mcpServerSetupReducer";
22
+ import { useMcpServerOAuthConnect } from "./useMcpServerOAuthConnect";
22
23
 
23
24
  // ---------------------------------------------------------------------------
24
25
  // Setup integration props
@@ -255,6 +256,7 @@ export function McpServerPicker({
255
256
 
256
257
  const { results, isLoading, error, query, setQuery } =
257
258
  useMcpServerSearch(org, { scope });
259
+ const oauth = useMcpServerOAuthConnect();
258
260
 
259
261
  const [focusIndex, setFocusIndex] = useState(-1);
260
262
  const [view, setView] = useState<PickerView>(() =>
@@ -384,14 +386,50 @@ export function McpServerPicker({
384
386
  const needsCredentials =
385
387
  entry.status === "needsSetup" || entry.status === "submitting";
386
388
 
389
+ const auth = entry.mcpServer.spec?.auth;
390
+ const oauthTargetEnvVar = auth?.targetEnvVar || null;
391
+ const hasOAuth = !!auth;
392
+
393
+ const entryMissingVars =
394
+ entry.status === "needsSetup" ? entry.missingVariables : [];
395
+
396
+ const filteredMissingVars = oauthTargetEnvVar
397
+ ? entryMissingVars.filter((v) => v.key !== oauthTargetEnvVar)
398
+ : entryMissingVars;
399
+
400
+ const hasManualVars = filteredMissingVars.length > 0;
401
+
402
+ const oauthTokenMissing = oauthTargetEnvVar
403
+ ? entryMissingVars.some((v) => v.key === oauthTargetEnvVar)
404
+ : false;
405
+
406
+ const oauthSignInProps = hasOAuth
407
+ ? {
408
+ onSignIn: async () => {
409
+ if (!entry.mcpServer.metadata?.id) return;
410
+ try {
411
+ await oauth.startOAuth(entry.mcpServer.metadata.id);
412
+ setup.onServerAdded(ref);
413
+ } catch {
414
+ // error state managed by oauth hook
415
+ }
416
+ },
417
+ phase: oauth.phase,
418
+ isConnected: !oauthTokenMissing,
419
+ error: oauth.error,
420
+ onClearError: oauth.clearError,
421
+ }
422
+ : undefined;
423
+
387
424
  return (
388
425
  <div className={cn("w-72", className)}>
389
426
  <McpServerConfigPanel
390
427
  mcpServer={entry.mcpServer}
428
+ oauthSignIn={oauthSignInProps}
391
429
  credentials={
392
- needsCredentials
430
+ needsCredentials && hasManualVars
393
431
  ? {
394
- variables: entry.missingVariables,
432
+ variables: filteredMissingVars,
395
433
  onSubmit: (values, opts) =>
396
434
  setup.onSubmitEnvVars(ref, values, opts),
397
435
  isSubmitting: entry.status === "submitting",