@stigmer/react 0.0.77 → 0.0.79
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 +92 -13
- 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 +54 -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/useMcpServerConnect.d.ts +15 -12
- package/mcp-server/useMcpServerConnect.d.ts.map +1 -1
- package/mcp-server/useMcpServerConnect.js +17 -17
- package/mcp-server/useMcpServerConnect.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 +82 -0
- package/mcp-server/useMcpServerOAuthConnect.d.ts.map +1 -0
- package/mcp-server/useMcpServerOAuthConnect.js +199 -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 +231 -172
- package/src/mcp-server/McpServerPicker.tsx +40 -2
- package/src/mcp-server/OAuthCallbackHandler.tsx +239 -0
- package/src/mcp-server/index.ts +17 -1
- package/src/mcp-server/useMcpServerConnect.ts +25 -22
- package/src/mcp-server/useMcpServerCredentials.ts +86 -13
- package/src/mcp-server/useMcpServerOAuthConnect.ts +312 -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,
|
|
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, org);
|
|
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
|
|
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
|
|
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
|
|
262
|
+
<EnvSpecSection
|
|
263
|
+
data={spec.envSpec.data}
|
|
264
|
+
oauthTargetEnvVar={credentials.oauthTargetEnvVar}
|
|
265
|
+
/>
|
|
238
266
|
)}
|
|
239
267
|
|
|
240
|
-
<
|
|
241
|
-
<
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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 "Save for future runs" 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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
+
)}
|
|
283
313
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
314
|
+
{capabilityTab === "policies" && (
|
|
315
|
+
<PoliciesTabContent
|
|
316
|
+
pinnedPolicies={pinnedPolicies}
|
|
317
|
+
classifiedPolicies={classifiedPolicies}
|
|
318
|
+
hasDiscoveredTools={hasDiscoveredTools}
|
|
319
|
+
/>
|
|
320
|
+
)}
|
|
291
321
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
330
|
-
<
|
|
331
|
-
{
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
"
|
|
343
|
-
|
|
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
|
-
{
|
|
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-
|
|
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
|
-
|
|
695
|
+
spec,
|
|
601
696
|
}: {
|
|
602
|
-
readonly
|
|
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
|
-
{
|
|
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={
|
|
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
|
-
{
|
|
713
|
+
{spec.repositoryUrl}
|
|
674
714
|
<ExternalLinkIcon className="size-3 shrink-0" />
|
|
675
715
|
</a>
|
|
676
716
|
</div>
|
|
677
717
|
)}
|
|
678
|
-
{
|
|
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
|
-
{
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
|
|
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, org);
|
|
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",
|