@stigmer/react 0.0.76 → 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 +96 -9
  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 +283 -97
  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 } 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,6 +192,7 @@ export function McpServerDetailView({
172
192
 
173
193
  const spec = mcpServer?.spec;
174
194
  const status = mcpServer?.status;
195
+ const hasSource = spec && (spec.repositoryUrl || spec.githubStars > 0);
175
196
  const specAudit = status?.audit?.specAudit;
176
197
  const capabilities = status?.discoveredCapabilities;
177
198
  const pinnedPolicies = spec?.pinnedToolApprovals ?? [];
@@ -196,6 +217,12 @@ export function McpServerDetailView({
196
217
  return items;
197
218
  }, [tools.length, totalPolicyCount, resourceTemplates.length]);
198
219
 
220
+ const combinedError = connection.error ?? oauth.error;
221
+ const combinedClearError = useCallback(() => {
222
+ connection.clearError();
223
+ oauth.clearError();
224
+ }, [connection, oauth]);
225
+
199
226
  if (isLoading) return <LoadingSkeleton className={className} />;
200
227
  if (error)
201
228
  return <ErrorMessage error={error} retry={refetch} className={className} />;
@@ -225,72 +252,78 @@ export function McpServerDetailView({
225
252
  isVisibilityPending={isVisibilityPending}
226
253
  />
227
254
 
255
+ {hasSource && <SourceSection spec={spec} />}
256
+
228
257
  {spec?.serverType.case && (
229
258
  <ServerConfigSection serverType={spec.serverType} />
230
259
  )}
231
260
 
232
261
  {spec?.envSpec && Object.keys(spec.envSpec.data).length > 0 && (
233
- <EnvSpecSection data={spec.envSpec.data} />
262
+ <EnvSpecSection
263
+ data={spec.envSpec.data}
264
+ oauthTargetEnvVar={credentials.oauthTargetEnvVar}
265
+ />
234
266
  )}
235
267
 
236
- <section>
237
- <h3 className="mb-2 text-xs font-medium uppercase tracking-wider text-muted-foreground">
238
- Capabilities
239
- </h3>
240
- <div className="overflow-hidden rounded-lg border border-border">
241
- <ConnectBar
242
- isConnecting={connection.isConnecting}
243
- connectionError={connection.error}
244
- onConnect={handleConnectClick}
245
- onClearConnectionError={connection.clearError}
246
- hasDiscoveredTools={hasDiscoveredTools}
247
- toolCount={tools.length}
248
- policyCount={totalPolicyCount}
249
- credentialsLoading={credentials.isLoading}
250
- />
251
-
252
- {showCredentialForm && credentials.missingVariables.length > 0 && (
253
- <div
254
- className="border-b border-border p-4"
255
- data-cursor-target="credential-form"
256
- >
257
- <EnvVarForm
258
- title="Credentials Required"
259
- 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."
260
- variables={credentials.missingVariables}
261
- onSubmit={(values, options) => handleCredentialSubmit(values, options)}
262
- onCancel={() => setShowCredentialForm(false)}
263
- isSubmitting={credentials.isSaving}
264
- poolValues={credentialPoolValues}
265
- className="w-full max-w-md"
266
- />
267
- </div>
268
- )}
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
+ />
269
283
 
270
- <Tabs
271
- tabs={capabilityTabs}
272
- activeTab={capabilityTab}
273
- onTabChange={(id) => setCapabilityTab(id as CapabilityTab)}
274
- 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"
275
288
  >
276
- {capabilityTab === "tools" && (
277
- <ToolsTabContent tools={tools} />
278
- )}
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
+ )}
279
313
 
280
- {capabilityTab === "policies" && (
281
- <PoliciesTabContent
282
- pinnedPolicies={pinnedPolicies}
283
- classifiedPolicies={classifiedPolicies}
284
- hasDiscoveredTools={hasDiscoveredTools}
285
- />
286
- )}
314
+ {capabilityTab === "policies" && (
315
+ <PoliciesTabContent
316
+ pinnedPolicies={pinnedPolicies}
317
+ classifiedPolicies={classifiedPolicies}
318
+ hasDiscoveredTools={hasDiscoveredTools}
319
+ />
320
+ )}
287
321
 
288
- {capabilityTab === "resources" && (
289
- <ResourceTemplatesList templates={resourceTemplates} />
290
- )}
291
- </Tabs>
292
- </div>
293
- </section>
322
+ {capabilityTab === "resources" && (
323
+ <ResourceTemplatesList templates={resourceTemplates} />
324
+ )}
325
+ </Tabs>
326
+ </Section>
294
327
 
295
328
  {spec && spec.tags.length > 0 && <TagsSection tags={spec.tags} />}
296
329
  </div>
@@ -310,6 +343,10 @@ function ConnectBar({
310
343
  toolCount,
311
344
  policyCount,
312
345
  credentialsLoading,
346
+ oauthPhase,
347
+ authMode,
348
+ isOAuthConnected,
349
+ tokenLifetimeHint,
313
350
  }: {
314
351
  readonly isConnecting: boolean;
315
352
  readonly connectionError: Error | null;
@@ -319,48 +356,90 @@ function ConnectBar({
319
356
  readonly toolCount: number;
320
357
  readonly policyCount: number;
321
358
  readonly credentialsLoading: boolean;
359
+ readonly oauthPhase: OAuthConnectPhase;
360
+ readonly authMode: "manual" | "oauth";
361
+ readonly isOAuthConnected: boolean;
362
+ readonly tokenLifetimeHint: string | null;
322
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
+
323
396
  return (
324
397
  <div className="flex flex-col">
325
- <div className="flex items-center justify-between border-b border-border px-3 py-2">
326
- <span className="text-xs text-muted-foreground">
327
- {hasDiscoveredTools
328
- ? formatConnectionSummary(toolCount, policyCount)
329
- : "Not connected yet"}
330
- </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>
331
423
  <button
332
424
  type="button"
333
425
  onClick={onConnect}
334
- disabled={isConnecting || credentialsLoading}
426
+ disabled={isConnecting || isOAuthBusy || credentialsLoading}
335
427
  data-cursor-target="connect-button"
336
428
  className={cn(
337
429
  "inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium",
338
- "border border-border bg-background text-foreground",
339
- "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",
340
433
  "disabled:pointer-events-none disabled:opacity-50",
341
434
  )}
342
435
  >
343
- {isConnecting ? (
344
- <>
345
- <Spinner />
346
- Connecting...
347
- </>
348
- ) : hasDiscoveredTools ? (
349
- <>
350
- <RefreshIcon className="size-3.5" />
351
- Reconnect
352
- </>
353
- ) : (
354
- <>
355
- <ConnectIcon className="size-3.5" />
356
- Connect
357
- </>
358
- )}
436
+ {buttonIcon}
437
+ {buttonLabel}
359
438
  </button>
360
439
  </div>
361
440
 
362
441
  {connectionError && (
363
- <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">
364
443
  <WarningIcon className="mt-0.5 size-3.5 shrink-0 text-destructive" />
365
444
  <p className="flex-1 text-xs text-destructive">
366
445
  {connectionError.message}
@@ -379,6 +458,21 @@ function ConnectBar({
379
458
  );
380
459
  }
381
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
+
382
476
  function formatConnectionSummary(toolCount: number, policyCount: number): string {
383
477
  const toolLabel = `${toolCount} tool${toolCount !== 1 ? "s" : ""}`;
384
478
  if (policyCount === 0) return toolLabel;
@@ -459,6 +553,11 @@ function Header({
459
553
  )
460
554
  )}
461
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
+ )}
462
561
  <div className="mt-0.5 flex flex-wrap items-center gap-x-1.5 text-xs text-muted-foreground">
463
562
  {meta?.org && <span>{meta.org}</span>}
464
563
  {status && (
@@ -592,6 +691,45 @@ function ServerConfigSection({
592
691
  );
593
692
  }
594
693
 
694
+ function SourceSection({
695
+ spec,
696
+ }: {
697
+ readonly spec: McpServerSpec;
698
+ }) {
699
+ return (
700
+ <Section title="Source">
701
+ <div className="flex flex-col gap-2 p-3">
702
+ {spec.repositoryUrl && (
703
+ <div className="flex items-baseline gap-2">
704
+ <span className="shrink-0 text-xs font-medium text-muted-foreground">
705
+ Repository
706
+ </span>
707
+ <a
708
+ href={spec.repositoryUrl}
709
+ target="_blank"
710
+ rel="noopener noreferrer"
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"
712
+ >
713
+ {spec.repositoryUrl}
714
+ <ExternalLinkIcon className="size-3 shrink-0" />
715
+ </a>
716
+ </div>
717
+ )}
718
+ {spec.githubStars > 0 && (
719
+ <div className="flex items-baseline gap-2">
720
+ <span className="shrink-0 text-xs font-medium text-muted-foreground">
721
+ Stars
722
+ </span>
723
+ <span className="text-xs text-foreground">
724
+ {spec.githubStars.toLocaleString()}
725
+ </span>
726
+ </div>
727
+ )}
728
+ </div>
729
+ </Section>
730
+ );
731
+ }
732
+
595
733
  function ResourceTemplatesList({
596
734
  templates,
597
735
  }: {
@@ -624,8 +762,10 @@ function ResourceTemplatesList({
624
762
 
625
763
  function EnvSpecSection({
626
764
  data,
765
+ oauthTargetEnvVar,
627
766
  }: {
628
767
  readonly data: { [key: string]: EnvironmentValue };
768
+ readonly oauthTargetEnvVar: string | null;
629
769
  }) {
630
770
  const entries = Object.entries(data).sort(([a], [b]) =>
631
771
  a.localeCompare(b),
@@ -634,21 +774,29 @@ function EnvSpecSection({
634
774
  return (
635
775
  <Section title={`Environment Variables (${entries.length})`}>
636
776
  <div className="flex flex-col divide-y divide-border">
637
- {entries.map(([name, env]) => (
638
- <div key={name} className="flex items-start gap-3 px-3 py-2">
639
- <code className="shrink-0 font-mono text-sm font-medium text-foreground">
640
- {name}
641
- </code>
642
- <span className="shrink-0 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
643
- {env.isSecret ? "secret" : "config"}
644
- </span>
645
- {env.description && (
646
- <span className="text-xs text-muted-foreground">
647
- {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"}
648
786
  </span>
649
- )}
650
- </div>
651
- ))}
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
+ })}
652
800
  </div>
653
801
  </Section>
654
802
  );
@@ -1046,6 +1194,44 @@ function SparklesIcon({ className }: { readonly className?: string }) {
1046
1194
  );
1047
1195
  }
1048
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
+
1216
+ function ExternalLinkIcon({ className }: { readonly className?: string }) {
1217
+ return (
1218
+ <svg
1219
+ className={className}
1220
+ viewBox="0 0 16 16"
1221
+ fill="none"
1222
+ stroke="currentColor"
1223
+ strokeWidth="1.5"
1224
+ strokeLinecap="round"
1225
+ strokeLinejoin="round"
1226
+ aria-hidden="true"
1227
+ >
1228
+ <path d="M12 8.667v4A1.333 1.333 0 0 1 10.667 14H3.333A1.333 1.333 0 0 1 2 12.667V5.333A1.333 1.333 0 0 1 3.333 4h4" />
1229
+ <path d="M10 2h4v4" />
1230
+ <path d="M6.667 9.333 14 2" />
1231
+ </svg>
1232
+ );
1233
+ }
1234
+
1049
1235
  function Spinner() {
1050
1236
  return (
1051
1237
  <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",