@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.
- package/index.d.ts +2 -2
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- package/index.js.map +1 -1
- package/mcp-server/McpServerConfigPanel.d.ts +28 -1
- package/mcp-server/McpServerConfigPanel.d.ts.map +1 -1
- package/mcp-server/McpServerConfigPanel.js +23 -2
- package/mcp-server/McpServerConfigPanel.js.map +1 -1
- package/mcp-server/McpServerDetailView.d.ts.map +1 -1
- package/mcp-server/McpServerDetailView.js +96 -9
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/mcp-server/McpServerPicker.d.ts.map +1 -1
- package/mcp-server/McpServerPicker.js +34 -2
- package/mcp-server/McpServerPicker.js.map +1 -1
- package/mcp-server/OAuthCallbackHandler.d.ts +52 -0
- package/mcp-server/OAuthCallbackHandler.d.ts.map +1 -0
- package/mcp-server/OAuthCallbackHandler.js +98 -0
- package/mcp-server/OAuthCallbackHandler.js.map +1 -0
- package/mcp-server/index.d.ts +6 -2
- package/mcp-server/index.d.ts.map +1 -1
- package/mcp-server/index.js +2 -0
- package/mcp-server/index.js.map +1 -1
- package/mcp-server/useMcpServerCredentials.d.ts +59 -7
- package/mcp-server/useMcpServerCredentials.d.ts.map +1 -1
- package/mcp-server/useMcpServerCredentials.js +37 -10
- package/mcp-server/useMcpServerCredentials.js.map +1 -1
- package/mcp-server/useMcpServerOAuthConnect.d.ts +81 -0
- package/mcp-server/useMcpServerOAuthConnect.d.ts.map +1 -0
- package/mcp-server/useMcpServerOAuthConnect.js +187 -0
- package/mcp-server/useMcpServerOAuthConnect.js.map +1 -0
- package/package.json +4 -4
- package/src/index.ts +9 -1
- package/src/mcp-server/McpServerConfigPanel.tsx +153 -3
- package/src/mcp-server/McpServerDetailView.tsx +283 -97
- package/src/mcp-server/McpServerPicker.tsx +40 -2
- package/src/mcp-server/OAuthCallbackHandler.tsx +237 -0
- package/src/mcp-server/index.ts +17 -1
- package/src/mcp-server/useMcpServerCredentials.ts +86 -13
- package/src/mcp-server/useMcpServerOAuthConnect.ts +298 -0
- 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
|
|
262
|
+
<EnvSpecSection
|
|
263
|
+
data={spec.envSpec.data}
|
|
264
|
+
oauthTargetEnvVar={credentials.oauthTargetEnvVar}
|
|
265
|
+
/>
|
|
234
266
|
)}
|
|
235
267
|
|
|
236
|
-
<
|
|
237
|
-
<
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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 "Save for future runs" 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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
289
|
+
<EnvVarForm
|
|
290
|
+
title="Credentials Required"
|
|
291
|
+
description="Enter the credentials needed to connect to this MCP server. Toggle "Save for future runs" 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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
314
|
+
{capabilityTab === "policies" && (
|
|
315
|
+
<PoliciesTabContent
|
|
316
|
+
pinnedPolicies={pinnedPolicies}
|
|
317
|
+
classifiedPolicies={classifiedPolicies}
|
|
318
|
+
hasDiscoveredTools={hasDiscoveredTools}
|
|
319
|
+
/>
|
|
320
|
+
)}
|
|
287
321
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
326
|
-
<
|
|
327
|
-
{
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
"
|
|
339
|
-
|
|
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
|
-
{
|
|
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-
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
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:
|
|
432
|
+
variables: filteredMissingVars,
|
|
395
433
|
onSubmit: (values, opts) =>
|
|
396
434
|
setup.onSubmitEnvVars(ref, values, opts),
|
|
397
435
|
isSubmitting: entry.status === "submitting",
|