@stigmer/react 0.0.68 → 0.0.70

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 (108) hide show
  1. package/README.md +1 -1
  2. package/composer/SessionComposer.d.ts.map +1 -1
  3. package/composer/SessionComposer.js +12 -3
  4. package/composer/SessionComposer.js.map +1 -1
  5. package/demo/__tests__/demo-client.test.d.ts +2 -0
  6. package/demo/__tests__/demo-client.test.d.ts.map +1 -0
  7. package/demo/__tests__/demo-client.test.js +133 -0
  8. package/demo/__tests__/demo-client.test.js.map +1 -0
  9. package/demo/__tests__/fixtures.test.d.ts +2 -0
  10. package/demo/__tests__/fixtures.test.d.ts.map +1 -0
  11. package/demo/__tests__/fixtures.test.js +135 -0
  12. package/demo/__tests__/fixtures.test.js.map +1 -0
  13. package/demo/__tests__/samples.test.d.ts +2 -0
  14. package/demo/__tests__/samples.test.d.ts.map +1 -0
  15. package/demo/__tests__/samples.test.js +152 -0
  16. package/demo/__tests__/samples.test.js.map +1 -0
  17. package/demo/client.d.ts +29 -0
  18. package/demo/client.d.ts.map +1 -0
  19. package/demo/client.js +52 -0
  20. package/demo/client.js.map +1 -0
  21. package/demo/fixtures.d.ts +190 -0
  22. package/demo/fixtures.d.ts.map +1 -0
  23. package/demo/fixtures.js +263 -0
  24. package/demo/fixtures.js.map +1 -0
  25. package/demo/index.d.ts +6 -0
  26. package/demo/index.d.ts.map +1 -0
  27. package/demo/index.js +6 -0
  28. package/demo/index.js.map +1 -0
  29. package/demo/samples.d.ts +166 -0
  30. package/demo/samples.d.ts.map +1 -0
  31. package/demo/samples.js +308 -0
  32. package/demo/samples.js.map +1 -0
  33. package/demo/transport.d.ts +59 -0
  34. package/demo/transport.d.ts.map +1 -0
  35. package/demo/transport.js +75 -0
  36. package/demo/transport.js.map +1 -0
  37. package/demo/types.d.ts +62 -0
  38. package/demo/types.d.ts.map +1 -0
  39. package/demo/types.js +16 -0
  40. package/demo/types.js.map +1 -0
  41. package/environment/EnvVarForm.d.ts.map +1 -1
  42. package/environment/EnvVarForm.js +1 -1
  43. package/environment/EnvVarForm.js.map +1 -1
  44. package/environment/__tests__/systemEnvVars.test.d.ts +2 -0
  45. package/environment/__tests__/systemEnvVars.test.d.ts.map +1 -0
  46. package/environment/__tests__/systemEnvVars.test.js +76 -0
  47. package/environment/__tests__/systemEnvVars.test.js.map +1 -0
  48. package/environment/index.d.ts +1 -0
  49. package/environment/index.d.ts.map +1 -1
  50. package/environment/index.js +1 -0
  51. package/environment/index.js.map +1 -1
  52. package/environment/systemEnvVars.d.ts +52 -0
  53. package/environment/systemEnvVars.d.ts.map +1 -0
  54. package/environment/systemEnvVars.js +91 -0
  55. package/environment/systemEnvVars.js.map +1 -0
  56. package/execution/ApprovalCard.d.ts.map +1 -1
  57. package/execution/ApprovalCard.js +3 -3
  58. package/execution/ApprovalCard.js.map +1 -1
  59. package/index.d.ts +1 -1
  60. package/index.d.ts.map +1 -1
  61. package/index.js +2 -2
  62. package/index.js.map +1 -1
  63. package/internal/Tabs.d.ts +41 -0
  64. package/internal/Tabs.d.ts.map +1 -0
  65. package/internal/Tabs.js +65 -0
  66. package/internal/Tabs.js.map +1 -0
  67. package/mcp-server/McpServerDetailView.d.ts +33 -7
  68. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  69. package/mcp-server/McpServerDetailView.js +53 -37
  70. package/mcp-server/McpServerDetailView.js.map +1 -1
  71. package/mcp-server/useMcpServerCredentials.d.ts.map +1 -1
  72. package/mcp-server/useMcpServerCredentials.js +2 -1
  73. package/mcp-server/useMcpServerCredentials.js.map +1 -1
  74. package/models/index.d.ts +1 -1
  75. package/models/index.d.ts.map +1 -1
  76. package/models/index.js +1 -1
  77. package/models/index.js.map +1 -1
  78. package/models/registry.d.ts +10 -0
  79. package/models/registry.d.ts.map +1 -1
  80. package/models/registry.js +13 -0
  81. package/models/registry.js.map +1 -1
  82. package/models/useModelRegistry.d.ts.map +1 -1
  83. package/models/useModelRegistry.js +7 -3
  84. package/models/useModelRegistry.js.map +1 -1
  85. package/package.json +9 -5
  86. package/src/composer/SessionComposer.tsx +21 -3
  87. package/src/demo/__tests__/demo-client.test.tsx +213 -0
  88. package/src/demo/__tests__/fixtures.test.ts +214 -0
  89. package/src/demo/__tests__/samples.test.ts +171 -0
  90. package/src/demo/client.ts +78 -0
  91. package/src/demo/fixtures.ts +401 -0
  92. package/src/demo/index.ts +12 -0
  93. package/src/demo/samples.ts +470 -0
  94. package/src/demo/transport.ts +116 -0
  95. package/src/demo/types.ts +69 -0
  96. package/src/environment/EnvVarForm.tsx +1 -0
  97. package/src/environment/__tests__/systemEnvVars.test.ts +120 -0
  98. package/src/environment/index.ts +6 -0
  99. package/src/environment/systemEnvVars.ts +104 -0
  100. package/src/execution/ApprovalCard.tsx +4 -0
  101. package/src/index.ts +5 -1
  102. package/src/internal/Tabs.tsx +166 -0
  103. package/src/mcp-server/McpServerDetailView.tsx +273 -204
  104. package/src/mcp-server/useMcpServerCredentials.ts +4 -1
  105. package/src/models/index.ts +1 -1
  106. package/src/models/registry.ts +14 -0
  107. package/src/models/useModelRegistry.ts +7 -2
  108. package/styles.css +1 -1
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { cn } from "@stigmer/theme";
5
5
  import { timestampDate } from "@bufbuild/protobuf/wkt";
6
6
  import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
@@ -22,6 +22,9 @@ import { ErrorMessage } from "../error/ErrorMessage";
22
22
  import { EnvVarForm } from "../environment/EnvVarForm";
23
23
  import type { EnvVarFormVariable } from "../environment/EnvVarForm";
24
24
  import { VisibilityToggle } from "../library/VisibilityToggle";
25
+ import { Tabs, type TabItem } from "../internal/Tabs";
26
+
27
+ type CapabilityTab = "tools" | "policies" | "resources";
25
28
 
26
29
  export interface McpServerDetailViewProps {
27
30
  /** Organization slug that owns the MCP server. */
@@ -60,6 +63,29 @@ export interface McpServerDetailViewProps {
60
63
  sessionId: string;
61
64
  executionId: string;
62
65
  }) => void;
66
+ /**
67
+ * Initial active capability tab. Defaults to `"tools"`.
68
+ * Useful for deep-linking or demo scenarios that need to start on
69
+ * a specific tab.
70
+ */
71
+ readonly defaultCapabilityTab?: CapabilityTab;
72
+ /**
73
+ * When `true`, the credential form inside the Tools tab opens
74
+ * immediately on mount (instead of waiting for a Discover click).
75
+ * Useful for guided tours and demo scenarios that need to show the
76
+ * credential entry step without user interaction.
77
+ * @default false
78
+ */
79
+ readonly defaultShowCredentialForm?: boolean;
80
+ /**
81
+ * Pre-fill function for credential form inputs. When provided, the
82
+ * {@link EnvVarForm} receives these as `poolValues`, populating
83
+ * fields on mount. Useful for demo scenarios that simulate a user
84
+ * having already entered credentials.
85
+ */
86
+ readonly credentialPoolValues?: (
87
+ key: string,
88
+ ) => import("@stigmer/sdk").EnvVarInput | undefined;
63
89
  /** Additional CSS classes for the root container. */
64
90
  readonly className?: string;
65
91
  }
@@ -68,13 +94,16 @@ export interface McpServerDetailViewProps {
68
94
  * Read-only detail view for an MCP Server integration.
69
95
  *
70
96
  * Fetches the server via {@link useMcpServer} internally and renders
71
- * its full configuration in structured sections: validation banner
72
- * (if invalid), header, server configuration (type-specific), discovered
73
- * tools, resource templates, environment variables, and tags. Sections
74
- * with no data are omitted entirely.
97
+ * its full configuration: validation banner (if invalid), header,
98
+ * a **tabbed capabilities panel** (Tools, Policies, and optionally
99
+ * Resources), server configuration, environment variables, and tags.
100
+ *
101
+ * The capabilities panel groups the three core concerns of an MCP
102
+ * server into tabs, each co-locating its action with its output:
75
103
  *
76
- * The discovered tools section is the star of this view — it answers
77
- * the question every user asks: "What can this server do?"
104
+ * - **Tools** Discover button + discovered tool list
105
+ * - **Policies** Existing approval policies + AI generate action
106
+ * - **Resources** — Discovered resource templates (tab hidden when empty)
78
107
  *
79
108
  * Handles loading, error, and not-found states automatically.
80
109
  * Zero Console dependencies — safe for platform builder embedding.
@@ -93,6 +122,9 @@ export function McpServerDetailView({
93
122
  onVisibilityChange,
94
123
  isVisibilityPending,
95
124
  onPolicySessionCreated,
125
+ defaultCapabilityTab = "tools",
126
+ defaultShowCredentialForm = false,
127
+ credentialPoolValues,
96
128
  className,
97
129
  }: McpServerDetailViewProps) {
98
130
  const { mcpServer, isLoading, error, refetch } = useMcpServer(org, slug);
@@ -100,10 +132,11 @@ export function McpServerDetailView({
100
132
  const discovery = useDiscoverCapabilities();
101
133
  const policySession = useTriggerApprovalPolicySession();
102
134
 
103
- const [showCredentialForm, setShowCredentialForm] = useState(false);
135
+ const [showCredentialForm, setShowCredentialForm] = useState(defaultShowCredentialForm);
104
136
  const [policyPanelExecutionId, setPolicyPanelExecutionId] = useState<
105
137
  string | null
106
138
  >(null);
139
+ const [capabilityTab, setCapabilityTab] = useState<CapabilityTab>(defaultCapabilityTab);
107
140
 
108
141
  const onResourceLoadRef = useRef(onResourceLoad);
109
142
  onResourceLoadRef.current = onResourceLoad;
@@ -181,19 +214,35 @@ export function McpServerDetailView({
181
214
  refetch();
182
215
  }, [refetch]);
183
216
 
217
+ const spec = mcpServer?.spec;
218
+ const status = mcpServer?.status;
219
+ const specAudit = status?.audit?.specAudit;
220
+ const capabilities = status?.discoveredCapabilities;
221
+ const toolApprovals = spec?.defaultToolApprovals ?? [];
222
+ const tools = capabilities?.tools ?? [];
223
+ const resourceTemplates = capabilities?.resourceTemplates ?? [];
224
+ const hasDiscoveredTools = tools.length > 0;
225
+
226
+ const capabilityTabs: TabItem[] = useMemo(() => {
227
+ const items: TabItem[] = [
228
+ { id: "tools", label: "Tools", badge: tools.length },
229
+ { id: "policies", label: "Policies", badge: toolApprovals.length },
230
+ ];
231
+ if (resourceTemplates.length > 0) {
232
+ items.push({
233
+ id: "resources",
234
+ label: "Resources",
235
+ badge: resourceTemplates.length,
236
+ });
237
+ }
238
+ return items;
239
+ }, [tools.length, toolApprovals.length, resourceTemplates.length]);
240
+
184
241
  if (isLoading) return <LoadingSkeleton className={className} />;
185
242
  if (error)
186
243
  return <ErrorMessage error={error} retry={refetch} className={className} />;
187
244
  if (!mcpServer) return <NotFoundState className={className} />;
188
245
 
189
- const spec = mcpServer.spec;
190
- const status = mcpServer.status;
191
- const specAudit = status?.audit?.specAudit;
192
- const capabilities = status?.discoveredCapabilities;
193
- const toolApprovals = spec?.defaultToolApprovals ?? [];
194
- const hasDiscoveredTools =
195
- capabilities != null && capabilities.tools.length > 0;
196
-
197
246
  return (
198
247
  <div className={cn("flex flex-col gap-6", className)}>
199
248
  {status?.validationState === ValidationState.invalid &&
@@ -218,63 +267,63 @@ export function McpServerDetailView({
218
267
  isVisibilityPending={isVisibilityPending}
219
268
  />
220
269
 
221
- {/* Discovery action */}
222
- <DiscoverySection
223
- isDiscovering={discovery.isDiscovering}
224
- discoveryError={discovery.error}
225
- onDiscover={handleDiscoverClick}
226
- onClearDiscoveryError={discovery.clearError}
227
- hasDiscoveredTools={hasDiscoveredTools}
228
- showCredentialForm={showCredentialForm}
229
- credentialVariables={credentials.missingVariables}
230
- isSavingCredentials={credentials.isSaving}
231
- onCredentialSubmit={handleCredentialSubmit}
232
- onCredentialCancel={() => setShowCredentialForm(false)}
233
- credentialsLoading={credentials.isLoading}
234
- />
235
-
236
270
  {spec?.serverType.case && (
237
271
  <ServerConfigSection serverType={spec.serverType} />
238
272
  )}
239
273
 
240
- {hasDiscoveredTools && (
241
- <ToolsSection tools={capabilities.tools} />
242
- )}
243
-
244
- {capabilities && capabilities.resourceTemplates.length > 0 && (
245
- <ResourceTemplatesSection
246
- templates={capabilities.resourceTemplates}
247
- />
248
- )}
249
-
250
- {/* Approval Policies */}
251
- {toolApprovals.length > 0 && (
252
- <ApprovalPoliciesSection policies={toolApprovals} />
274
+ {spec?.envSpec && Object.keys(spec.envSpec.data).length > 0 && (
275
+ <EnvSpecSection data={spec.envSpec.data} />
253
276
  )}
254
277
 
255
- {/* Generate Approval Policies */}
256
- {hasDiscoveredTools && (
257
- <GenerateApprovalPoliciesSection
258
- hasPolicies={toolApprovals.length > 0}
259
- isTriggering={policySession.isTriggering}
260
- triggerError={policySession.error}
261
- onGenerate={handleGenerateApprovalPolicies}
262
- onClearError={policySession.clearError}
263
- />
264
- )}
278
+ <section>
279
+ <h3 className="mb-2 text-xs font-medium uppercase tracking-wider text-muted-foreground">
280
+ Capabilities
281
+ </h3>
282
+ <div className="overflow-hidden rounded-lg border border-border">
283
+ <Tabs
284
+ tabs={capabilityTabs}
285
+ activeTab={capabilityTab}
286
+ onTabChange={(id) => setCapabilityTab(id as CapabilityTab)}
287
+ aria-label="MCP server capabilities"
288
+ >
289
+ {capabilityTab === "tools" && (
290
+ <ToolsTabContent
291
+ tools={tools}
292
+ isDiscovering={discovery.isDiscovering}
293
+ discoveryError={discovery.error}
294
+ onDiscover={handleDiscoverClick}
295
+ onClearDiscoveryError={discovery.clearError}
296
+ hasDiscoveredTools={hasDiscoveredTools}
297
+ showCredentialForm={showCredentialForm}
298
+ credentialVariables={credentials.missingVariables}
299
+ isSavingCredentials={credentials.isSaving}
300
+ onCredentialSubmit={handleCredentialSubmit}
301
+ onCredentialCancel={() => setShowCredentialForm(false)}
302
+ credentialsLoading={credentials.isLoading}
303
+ credentialPoolValues={credentialPoolValues}
304
+ />
305
+ )}
265
306
 
266
- {/* Inline approval policy generation panel */}
267
- {policyPanelExecutionId && (
268
- <ApprovalPolicyGeneratorPanel
269
- executionId={policyPanelExecutionId}
270
- onClose={() => setPolicyPanelExecutionId(null)}
271
- onComplete={handlePolicyPanelComplete}
272
- />
273
- )}
307
+ {capabilityTab === "policies" && (
308
+ <PoliciesTabContent
309
+ policies={toolApprovals}
310
+ hasDiscoveredTools={hasDiscoveredTools}
311
+ isTriggering={policySession.isTriggering}
312
+ triggerError={policySession.error}
313
+ onGenerate={handleGenerateApprovalPolicies}
314
+ onClearTriggerError={policySession.clearError}
315
+ policyPanelExecutionId={policyPanelExecutionId}
316
+ onPolicyPanelClose={() => setPolicyPanelExecutionId(null)}
317
+ onPolicyPanelComplete={handlePolicyPanelComplete}
318
+ />
319
+ )}
274
320
 
275
- {spec?.envSpec && Object.keys(spec.envSpec.data).length > 0 && (
276
- <EnvSpecSection data={spec.envSpec.data} />
277
- )}
321
+ {capabilityTab === "resources" && (
322
+ <ResourceTemplatesList templates={resourceTemplates} />
323
+ )}
324
+ </Tabs>
325
+ </div>
326
+ </section>
278
327
 
279
328
  {spec && spec.tags.length > 0 && <TagsSection tags={spec.tags} />}
280
329
  </div>
@@ -487,58 +536,33 @@ function ServerConfigSection({
487
536
  );
488
537
  }
489
538
 
490
- function ToolsSection({
491
- tools,
492
- }: {
493
- readonly tools: readonly DiscoveredTool[];
494
- }) {
495
- return (
496
- <Section title={`Tools (${tools.length})`}>
497
- <div className="flex flex-col divide-y divide-border">
498
- {tools.map((tool) => (
499
- <div key={tool.name} className="px-3 py-2.5">
500
- <code className="font-mono text-sm font-medium text-foreground">
501
- {tool.name}
502
- </code>
503
- {tool.description && (
504
- <p className="mt-0.5 text-xs text-muted-foreground">
505
- {tool.description}
506
- </p>
507
- )}
508
- </div>
509
- ))}
510
- </div>
511
- </Section>
512
- );
513
- }
514
-
515
- function ResourceTemplatesSection({
539
+ function ResourceTemplatesList({
516
540
  templates,
517
541
  }: {
518
542
  readonly templates: readonly DiscoveredResourceTemplate[];
519
543
  }) {
544
+ if (templates.length === 0) return null;
545
+
520
546
  return (
521
- <Section title={`Resource Templates (${templates.length})`}>
522
- <div className="flex flex-col divide-y divide-border">
523
- {templates.map((tpl) => (
524
- <div key={tpl.uriTemplate || tpl.name} className="px-3 py-2.5">
525
- <div className="flex items-baseline gap-2">
526
- <span className="text-sm font-medium text-foreground">
527
- {tpl.name}
528
- </span>
529
- <code className="font-mono text-[10px] text-muted-foreground">
530
- {tpl.uriTemplate}
531
- </code>
532
- </div>
533
- {tpl.description && (
534
- <p className="mt-0.5 text-xs text-muted-foreground">
535
- {tpl.description}
536
- </p>
537
- )}
547
+ <div className="flex flex-col divide-y divide-border">
548
+ {templates.map((tpl) => (
549
+ <div key={tpl.uriTemplate || tpl.name} className="px-3 py-2.5">
550
+ <div className="flex items-baseline gap-2">
551
+ <span className="text-sm font-medium text-foreground">
552
+ {tpl.name}
553
+ </span>
554
+ <code className="font-mono text-[10px] text-muted-foreground">
555
+ {tpl.uriTemplate}
556
+ </code>
538
557
  </div>
539
- ))}
540
- </div>
541
- </Section>
558
+ {tpl.description && (
559
+ <p className="mt-0.5 text-xs text-muted-foreground">
560
+ {tpl.description}
561
+ </p>
562
+ )}
563
+ </div>
564
+ ))}
565
+ </div>
542
566
  );
543
567
  }
544
568
 
@@ -592,10 +616,11 @@ function TagsSection({ tags }: { readonly tags: readonly string[] }) {
592
616
  }
593
617
 
594
618
  // ---------------------------------------------------------------------------
595
- // Discovery section
619
+ // Capability tab contents
596
620
  // ---------------------------------------------------------------------------
597
621
 
598
- function DiscoverySection({
622
+ function ToolsTabContent({
623
+ tools,
599
624
  isDiscovering,
600
625
  discoveryError,
601
626
  onDiscover,
@@ -607,7 +632,9 @@ function DiscoverySection({
607
632
  onCredentialSubmit,
608
633
  onCredentialCancel,
609
634
  credentialsLoading,
635
+ credentialPoolValues,
610
636
  }: {
637
+ readonly tools: readonly DiscoveredTool[];
611
638
  readonly isDiscovering: boolean;
612
639
  readonly discoveryError: Error | null;
613
640
  readonly onDiscover: () => void;
@@ -622,19 +649,26 @@ function DiscoverySection({
622
649
  ) => void;
623
650
  readonly onCredentialCancel: () => void;
624
651
  readonly credentialsLoading: boolean;
652
+ readonly credentialPoolValues?: (
653
+ key: string,
654
+ ) => import("@stigmer/sdk").EnvVarInput | undefined;
625
655
  }) {
626
656
  return (
627
- <section className="flex flex-col gap-3">
628
- <div className="flex items-center justify-between">
629
- <h3 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
630
- Discovery
631
- </h3>
657
+ <div className="flex flex-col">
658
+ {/* Discovery action bar */}
659
+ <div className="flex items-center justify-between border-b border-border px-3 py-2">
660
+ <span className="text-xs text-muted-foreground">
661
+ {hasDiscoveredTools
662
+ ? `${tools.length} tool${tools.length !== 1 ? "s" : ""} discovered`
663
+ : "No tools discovered yet"}
664
+ </span>
632
665
  <button
633
666
  type="button"
634
667
  onClick={onDiscover}
635
668
  disabled={isDiscovering || credentialsLoading}
669
+ data-cursor-target="discover-button"
636
670
  className={cn(
637
- "inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium",
671
+ "inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium",
638
672
  "border border-border bg-background text-foreground",
639
673
  "hover:bg-accent hover:text-accent-foreground",
640
674
  "disabled:pointer-events-none disabled:opacity-50",
@@ -653,14 +687,14 @@ function DiscoverySection({
653
687
  ) : (
654
688
  <>
655
689
  <DiscoverIcon className="size-3.5" />
656
- Discover Tools
690
+ Discover
657
691
  </>
658
692
  )}
659
693
  </button>
660
694
  </div>
661
695
 
662
696
  {discoveryError && (
663
- <div className="flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2">
697
+ <div className="flex items-start gap-2 border-b border-destructive/20 bg-destructive/5 px-3 py-2">
664
698
  <WarningIcon className="mt-0.5 size-3.5 shrink-0 text-destructive" />
665
699
  <p className="flex-1 text-xs text-destructive">
666
700
  {discoveryError.message}
@@ -677,7 +711,10 @@ function DiscoverySection({
677
711
  )}
678
712
 
679
713
  {showCredentialForm && credentialVariables.length > 0 && (
680
- <div className="rounded-lg border border-border p-4">
714
+ <div
715
+ className="border-b border-border p-4"
716
+ data-cursor-target="credential-form"
717
+ >
681
718
  <EnvVarForm
682
719
  title="Credentials Required"
683
720
  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 to use them for this discovery only."
@@ -685,118 +722,111 @@ function DiscoverySection({
685
722
  onSubmit={(values, options) => onCredentialSubmit(values, options)}
686
723
  onCancel={onCredentialCancel}
687
724
  isSubmitting={isSavingCredentials}
725
+ poolValues={credentialPoolValues}
688
726
  className="w-full max-w-md"
689
727
  />
690
728
  </div>
691
729
  )}
692
730
 
693
- {!hasDiscoveredTools && !isDiscovering && (
694
- <p className="text-xs text-muted-foreground">
695
- No tools discovered yet. Click &quot;Discover Tools&quot; to
696
- connect to this MCP server and list its available tools and
697
- resources.
698
- </p>
699
- )}
700
- </section>
701
- );
702
- }
703
-
704
- // ---------------------------------------------------------------------------
705
- // Approval policies section
706
- // ---------------------------------------------------------------------------
707
-
708
- function ApprovalPoliciesSection({
709
- policies,
710
- }: {
711
- readonly policies: readonly ToolApprovalPolicy[];
712
- }) {
713
- return (
714
- <Section title={`Approval Policies (${policies.length})`}>
715
- <div className="flex flex-col divide-y divide-border">
716
- {policies.map((policy) => (
717
- <div key={policy.toolName} className="px-3 py-2.5">
718
- <div className="flex items-baseline gap-2">
731
+ {hasDiscoveredTools ? (
732
+ <div className="flex flex-col divide-y divide-border">
733
+ {tools.map((tool) => (
734
+ <div key={tool.name} className="px-3 py-2.5">
719
735
  <code className="font-mono text-sm font-medium text-foreground">
720
- {policy.toolName}
736
+ {tool.name}
721
737
  </code>
722
- <ShieldIcon className="size-3 text-amber-500 dark:text-amber-400" />
738
+ {tool.description && (
739
+ <p className="mt-0.5 text-xs text-muted-foreground">
740
+ {tool.description}
741
+ </p>
742
+ )}
723
743
  </div>
724
- {policy.message && (
725
- <p className="mt-0.5 text-xs text-muted-foreground">
726
- {policy.message}
727
- </p>
728
- )}
744
+ ))}
745
+ </div>
746
+ ) : (
747
+ !isDiscovering && (
748
+ <div className="px-3 py-8 text-center">
749
+ <DiscoverIcon className="mx-auto mb-2 size-6 text-muted-foreground/40" />
750
+ <p className="text-xs text-muted-foreground">
751
+ Click &quot;Discover&quot; to connect to this MCP server and
752
+ list its available tools and resources.
753
+ </p>
729
754
  </div>
730
- ))}
731
- </div>
732
- </Section>
755
+ )
756
+ )}
757
+ </div>
733
758
  );
734
759
  }
735
760
 
736
- // ---------------------------------------------------------------------------
737
- // Generate approval policies section
738
- // ---------------------------------------------------------------------------
739
-
740
- function GenerateApprovalPoliciesSection({
741
- hasPolicies,
761
+ function PoliciesTabContent({
762
+ policies,
763
+ hasDiscoveredTools,
742
764
  isTriggering,
743
765
  triggerError,
744
766
  onGenerate,
745
- onClearError,
767
+ onClearTriggerError,
768
+ policyPanelExecutionId,
769
+ onPolicyPanelClose,
770
+ onPolicyPanelComplete,
746
771
  }: {
747
- readonly hasPolicies: boolean;
772
+ readonly policies: readonly ToolApprovalPolicy[];
773
+ readonly hasDiscoveredTools: boolean;
748
774
  readonly isTriggering: boolean;
749
775
  readonly triggerError: Error | null;
750
776
  readonly onGenerate: () => void;
751
- readonly onClearError: () => void;
777
+ readonly onClearTriggerError: () => void;
778
+ readonly policyPanelExecutionId: string | null;
779
+ readonly onPolicyPanelClose: () => void;
780
+ readonly onPolicyPanelComplete: () => void;
752
781
  }) {
782
+ const hasPolicies = policies.length > 0;
783
+
753
784
  return (
754
- <section className="flex flex-col gap-2">
755
- <div className="flex items-center justify-between">
756
- <div>
757
- <h3 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
758
- AI-Generated Policies
759
- </h3>
760
- <p className="mt-0.5 text-xs text-muted-foreground">
785
+ <div className="flex flex-col">
786
+ {/* Generate action bar */}
787
+ {hasDiscoveredTools && (
788
+ <div className="flex items-center justify-between border-b border-border px-3 py-2">
789
+ <span className="text-xs text-muted-foreground">
761
790
  {hasPolicies
762
- ? "Regenerate approval policies using AI to classify each tool's risk level."
763
- : "Automatically generate approval policies for the discovered tools using AI."}
764
- </p>
791
+ ? "Regenerate to reclassify each tool\u2019s risk level with AI."
792
+ : "Generate approval policies for discovered tools using AI."}
793
+ </span>
794
+ <button
795
+ type="button"
796
+ onClick={onGenerate}
797
+ disabled={isTriggering}
798
+ data-cursor-target="generate-policies-button"
799
+ className={cn(
800
+ "inline-flex shrink-0 items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium",
801
+ "bg-primary text-primary-foreground",
802
+ "hover:bg-primary/90",
803
+ "disabled:pointer-events-none disabled:opacity-50",
804
+ )}
805
+ >
806
+ {isTriggering ? (
807
+ <>
808
+ <DiscoverSpinner />
809
+ Starting...
810
+ </>
811
+ ) : (
812
+ <>
813
+ <SparklesIcon className="size-3.5" />
814
+ {hasPolicies ? "Regenerate" : "Generate"}
815
+ </>
816
+ )}
817
+ </button>
765
818
  </div>
766
- <button
767
- type="button"
768
- onClick={onGenerate}
769
- disabled={isTriggering}
770
- className={cn(
771
- "inline-flex shrink-0 items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium",
772
- "bg-primary text-primary-foreground",
773
- "hover:bg-primary/90",
774
- "disabled:pointer-events-none disabled:opacity-50",
775
- )}
776
- >
777
- {isTriggering ? (
778
- <>
779
- <DiscoverSpinner />
780
- Starting...
781
- </>
782
- ) : (
783
- <>
784
- <SparklesIcon className="size-3.5" />
785
- {hasPolicies ? "Regenerate Policies" : "Generate Policies"}
786
- </>
787
- )}
788
- </button>
789
- </div>
819
+ )}
790
820
 
791
821
  {triggerError && (
792
- <div className="flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2">
822
+ <div className="flex items-start gap-2 border-b border-destructive/20 bg-destructive/5 px-3 py-2">
793
823
  <WarningIcon className="mt-0.5 size-3.5 shrink-0 text-destructive" />
794
824
  <p className="flex-1 text-xs text-destructive">
795
825
  {triggerError.message}
796
826
  </p>
797
827
  <button
798
828
  type="button"
799
- onClick={onClearError}
829
+ onClick={onClearTriggerError}
800
830
  className="shrink-0 text-xs text-destructive/70 hover:text-destructive"
801
831
  aria-label="Dismiss error"
802
832
  >
@@ -804,7 +834,46 @@ function GenerateApprovalPoliciesSection({
804
834
  </button>
805
835
  </div>
806
836
  )}
807
- </section>
837
+
838
+ {hasPolicies ? (
839
+ <div className="flex flex-col divide-y divide-border">
840
+ {policies.map((policy) => (
841
+ <div key={policy.toolName} className="px-3 py-2.5">
842
+ <div className="flex items-baseline gap-2">
843
+ <code className="font-mono text-sm font-medium text-foreground">
844
+ {policy.toolName}
845
+ </code>
846
+ <ShieldIcon className="size-3 text-amber-500 dark:text-amber-400" />
847
+ </div>
848
+ {policy.message && (
849
+ <p className="mt-0.5 text-xs text-muted-foreground">
850
+ {policy.message}
851
+ </p>
852
+ )}
853
+ </div>
854
+ ))}
855
+ </div>
856
+ ) : (
857
+ <div className="px-3 py-8 text-center">
858
+ <ShieldIcon className="mx-auto mb-2 size-6 text-muted-foreground/40" />
859
+ <p className="text-xs text-muted-foreground">
860
+ {hasDiscoveredTools
861
+ ? "No approval policies yet. Generate them to add human oversight for sensitive tools."
862
+ : "Discover tools first, then generate approval policies."}
863
+ </p>
864
+ </div>
865
+ )}
866
+
867
+ {policyPanelExecutionId && (
868
+ <div className="border-t border-border">
869
+ <ApprovalPolicyGeneratorPanel
870
+ executionId={policyPanelExecutionId}
871
+ onClose={onPolicyPanelClose}
872
+ onComplete={onPolicyPanelComplete}
873
+ />
874
+ </div>
875
+ )}
876
+ </div>
808
877
  );
809
878
  }
810
879
 
@@ -5,6 +5,7 @@ import type { EnvVarInput } from "@stigmer/sdk";
5
5
  import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
6
6
  import { usePersonalEnvironment } from "../environment/usePersonalEnvironment";
7
7
  import { diffEnvSpec } from "../environment/diffEnvSpec";
8
+ import { SYSTEM_ENV_VAR_KEYS } from "../environment/systemEnvVars";
8
9
  import type { EnvVarFormVariable } from "../environment/EnvVarForm";
9
10
 
10
11
  export interface UseMcpServerCredentialsReturn {
@@ -82,7 +83,9 @@ export function useMcpServerCredentials(
82
83
  const existingKeys = new Set(
83
84
  Object.keys(personalEnv.environment?.spec?.data ?? {}),
84
85
  );
85
- return diffEnvSpec(envSpecData, existingKeys);
86
+ return diffEnvSpec(envSpecData, existingKeys).filter(
87
+ (v) => !SYSTEM_ENV_VAR_KEYS.has(v.key),
88
+ );
86
89
  }, [mcpServer, personalEnv.environment]);
87
90
 
88
91
  const isReady =