@stigmer/react 0.0.43 → 0.0.45

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 (83) hide show
  1. package/execution/ArtifactContentRenderer.d.ts +58 -0
  2. package/execution/ArtifactContentRenderer.d.ts.map +1 -0
  3. package/execution/ArtifactContentRenderer.js +163 -0
  4. package/execution/ArtifactContentRenderer.js.map +1 -0
  5. package/execution/ArtifactPreviewModal.d.ts +3 -2
  6. package/execution/ArtifactPreviewModal.d.ts.map +1 -1
  7. package/execution/ArtifactPreviewModal.js +11 -62
  8. package/execution/ArtifactPreviewModal.js.map +1 -1
  9. package/execution/ExecutionPhaseBadge.js +1 -1
  10. package/execution/ExecutionPhaseBadge.js.map +1 -1
  11. package/execution/MessageThread.d.ts.map +1 -1
  12. package/execution/MessageThread.js +21 -2
  13. package/execution/MessageThread.js.map +1 -1
  14. package/execution/SetupProgress.d.ts +35 -0
  15. package/execution/SetupProgress.d.ts.map +1 -0
  16. package/execution/SetupProgress.js +65 -0
  17. package/execution/SetupProgress.js.map +1 -0
  18. package/execution/ToolCallGroup.d.ts.map +1 -1
  19. package/execution/ToolCallGroup.js +9 -1
  20. package/execution/ToolCallGroup.js.map +1 -1
  21. package/execution/ToolCallItem.js +8 -3
  22. package/execution/ToolCallItem.js.map +1 -1
  23. package/execution/artifact-utils.d.ts +50 -0
  24. package/execution/artifact-utils.d.ts.map +1 -1
  25. package/execution/artifact-utils.js +67 -5
  26. package/execution/artifact-utils.js.map +1 -1
  27. package/execution/index.d.ts +6 -1
  28. package/execution/index.d.ts.map +1 -1
  29. package/execution/index.js +3 -1
  30. package/execution/index.js.map +1 -1
  31. package/index.d.ts +4 -4
  32. package/index.d.ts.map +1 -1
  33. package/index.js +2 -2
  34. package/index.js.map +1 -1
  35. package/mcp-server/ApprovalPolicyGeneratorPanel.d.ts +34 -0
  36. package/mcp-server/ApprovalPolicyGeneratorPanel.d.ts.map +1 -0
  37. package/mcp-server/ApprovalPolicyGeneratorPanel.js +55 -0
  38. package/mcp-server/ApprovalPolicyGeneratorPanel.js.map +1 -0
  39. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  40. package/mcp-server/McpServerDetailView.js +101 -2
  41. package/mcp-server/McpServerDetailView.js.map +1 -1
  42. package/mcp-server/index.d.ts +8 -0
  43. package/mcp-server/index.d.ts.map +1 -1
  44. package/mcp-server/index.js +4 -0
  45. package/mcp-server/index.js.map +1 -1
  46. package/mcp-server/useDiscoverCapabilities.d.ts +59 -0
  47. package/mcp-server/useDiscoverCapabilities.d.ts.map +1 -0
  48. package/mcp-server/useDiscoverCapabilities.js +77 -0
  49. package/mcp-server/useDiscoverCapabilities.js.map +1 -0
  50. package/mcp-server/useMcpServerCredentials.d.ts +63 -0
  51. package/mcp-server/useMcpServerCredentials.d.ts.map +1 -0
  52. package/mcp-server/useMcpServerCredentials.js +64 -0
  53. package/mcp-server/useMcpServerCredentials.js.map +1 -0
  54. package/mcp-server/useTriggerApprovalPolicySession.d.ts +42 -0
  55. package/mcp-server/useTriggerApprovalPolicySession.d.ts.map +1 -0
  56. package/mcp-server/useTriggerApprovalPolicySession.js +111 -0
  57. package/mcp-server/useTriggerApprovalPolicySession.js.map +1 -0
  58. package/package.json +4 -4
  59. package/session/__tests__/useSessionConversation.test.js +223 -2
  60. package/session/__tests__/useSessionConversation.test.js.map +1 -1
  61. package/session/useSessionConversation.d.ts +8 -1
  62. package/session/useSessionConversation.d.ts.map +1 -1
  63. package/session/useSessionConversation.js +77 -6
  64. package/session/useSessionConversation.js.map +1 -1
  65. package/src/execution/ArtifactContentRenderer.tsx +376 -0
  66. package/src/execution/ArtifactPreviewModal.tsx +22 -114
  67. package/src/execution/ExecutionPhaseBadge.tsx +1 -1
  68. package/src/execution/MessageThread.tsx +35 -3
  69. package/src/execution/SetupProgress.tsx +120 -0
  70. package/src/execution/ToolCallGroup.tsx +15 -1
  71. package/src/execution/ToolCallItem.tsx +10 -3
  72. package/src/execution/artifact-utils.ts +88 -4
  73. package/src/execution/index.ts +9 -0
  74. package/src/index.ts +16 -0
  75. package/src/mcp-server/ApprovalPolicyGeneratorPanel.tsx +164 -0
  76. package/src/mcp-server/McpServerDetailView.tsx +428 -2
  77. package/src/mcp-server/index.ts +15 -0
  78. package/src/mcp-server/useDiscoverCapabilities.ts +117 -0
  79. package/src/mcp-server/useMcpServerCredentials.ts +108 -0
  80. package/src/mcp-server/useTriggerApprovalPolicySession.ts +161 -0
  81. package/src/session/__tests__/useSessionConversation.test.tsx +355 -2
  82. package/src/session/useSessionConversation.ts +104 -9
  83. package/styles.css +1 -1
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useRef } from "react";
3
+ import { useCallback, useEffect, 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";
@@ -8,11 +8,19 @@ 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
12
  import { ValidationState } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/status_pb";
12
13
  import type { EnvironmentValue } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
13
14
  import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
14
15
  import { useMcpServer } from "./useMcpServer";
16
+ import { useDiscoverCapabilities } from "./useDiscoverCapabilities";
17
+ import { useMcpServerCredentials } from "./useMcpServerCredentials";
18
+ import { useTriggerApprovalPolicySession } from "./useTriggerApprovalPolicySession";
19
+ import { ApprovalPolicyGeneratorPanel } from "./ApprovalPolicyGeneratorPanel";
20
+ import { serializeMcpServerYaml } from "../library/serialize-resource-yaml";
15
21
  import { ErrorMessage } from "../error/ErrorMessage";
22
+ import { EnvVarForm } from "../environment/EnvVarForm";
23
+ import type { EnvVarFormVariable } from "../environment/EnvVarForm";
16
24
  import { VisibilityToggle } from "../library/VisibilityToggle";
17
25
 
18
26
  export interface McpServerDetailViewProps {
@@ -73,6 +81,14 @@ export function McpServerDetailView({
73
81
  className,
74
82
  }: McpServerDetailViewProps) {
75
83
  const { mcpServer, isLoading, error, refetch } = useMcpServer(org, slug);
84
+ const credentials = useMcpServerCredentials(org, mcpServer ?? null);
85
+ const discovery = useDiscoverCapabilities();
86
+ const policySession = useTriggerApprovalPolicySession();
87
+
88
+ const [showCredentialForm, setShowCredentialForm] = useState(false);
89
+ const [policyPanelExecutionId, setPolicyPanelExecutionId] = useState<
90
+ string | null
91
+ >(null);
76
92
 
77
93
  const onResourceLoadRef = useRef(onResourceLoad);
78
94
  onResourceLoadRef.current = onResourceLoad;
@@ -83,6 +99,66 @@ export function McpServerDetailView({
83
99
  }
84
100
  }, [mcpServer]);
85
101
 
102
+ const handleDiscoverClick = useCallback(async () => {
103
+ if (!mcpServer?.metadata?.id) return;
104
+
105
+ if (!credentials.isReady) {
106
+ setShowCredentialForm(true);
107
+ return;
108
+ }
109
+
110
+ try {
111
+ await discovery.discover(mcpServer.metadata.id);
112
+ refetch();
113
+ } catch {
114
+ // error state is managed by the hook
115
+ }
116
+ }, [mcpServer, credentials.isReady, discovery, refetch]);
117
+
118
+ const handleCredentialSubmit = useCallback(
119
+ async (
120
+ values: Record<string, import("@stigmer/sdk").EnvVarInput>,
121
+ options: { saveForFuture: boolean },
122
+ ) => {
123
+ try {
124
+ if (options.saveForFuture) {
125
+ await credentials.saveCredentials(values);
126
+ credentials.refetch();
127
+ }
128
+
129
+ setShowCredentialForm(false);
130
+
131
+ if (mcpServer?.metadata?.id) {
132
+ if (options.saveForFuture) {
133
+ await discovery.discover(mcpServer.metadata.id);
134
+ } else {
135
+ await discovery.discover(mcpServer.metadata.id, values);
136
+ }
137
+ refetch();
138
+ }
139
+ } catch {
140
+ // error state is managed by the hooks
141
+ }
142
+ },
143
+ [credentials, mcpServer, discovery, refetch],
144
+ );
145
+
146
+ const handleGenerateApprovalPolicies = useCallback(async () => {
147
+ if (!mcpServer) return;
148
+
149
+ try {
150
+ const yaml = serializeMcpServerYaml(mcpServer);
151
+ const result = await policySession.trigger(yaml, org, slug);
152
+ setPolicyPanelExecutionId(result.executionId);
153
+ } catch {
154
+ // error state is managed by the hook
155
+ }
156
+ }, [mcpServer, org, slug, policySession]);
157
+
158
+ const handlePolicyPanelComplete = useCallback(() => {
159
+ refetch();
160
+ }, [refetch]);
161
+
86
162
  if (isLoading) return <LoadingSkeleton className={className} />;
87
163
  if (error)
88
164
  return <ErrorMessage error={error} retry={refetch} className={className} />;
@@ -92,6 +168,9 @@ export function McpServerDetailView({
92
168
  const status = mcpServer.status;
93
169
  const specAudit = status?.audit?.specAudit;
94
170
  const capabilities = status?.discoveredCapabilities;
171
+ const toolApprovals = spec?.defaultToolApprovals ?? [];
172
+ const hasDiscoveredTools =
173
+ capabilities != null && capabilities.tools.length > 0;
95
174
 
96
175
  return (
97
176
  <div className={cn("flex flex-col gap-6", className)}>
@@ -117,11 +196,26 @@ export function McpServerDetailView({
117
196
  isVisibilityPending={isVisibilityPending}
118
197
  />
119
198
 
199
+ {/* Discovery action */}
200
+ <DiscoverySection
201
+ isDiscovering={discovery.isDiscovering}
202
+ discoveryError={discovery.error}
203
+ onDiscover={handleDiscoverClick}
204
+ onClearDiscoveryError={discovery.clearError}
205
+ hasDiscoveredTools={hasDiscoveredTools}
206
+ showCredentialForm={showCredentialForm}
207
+ credentialVariables={credentials.missingVariables}
208
+ isSavingCredentials={credentials.isSaving}
209
+ onCredentialSubmit={handleCredentialSubmit}
210
+ onCredentialCancel={() => setShowCredentialForm(false)}
211
+ credentialsLoading={credentials.isLoading}
212
+ />
213
+
120
214
  {spec?.serverType.case && (
121
215
  <ServerConfigSection serverType={spec.serverType} />
122
216
  )}
123
217
 
124
- {capabilities && capabilities.tools.length > 0 && (
218
+ {hasDiscoveredTools && (
125
219
  <ToolsSection tools={capabilities.tools} />
126
220
  )}
127
221
 
@@ -131,6 +225,31 @@ export function McpServerDetailView({
131
225
  />
132
226
  )}
133
227
 
228
+ {/* Approval Policies */}
229
+ {toolApprovals.length > 0 && (
230
+ <ApprovalPoliciesSection policies={toolApprovals} />
231
+ )}
232
+
233
+ {/* Generate Approval Policies */}
234
+ {hasDiscoveredTools && (
235
+ <GenerateApprovalPoliciesSection
236
+ hasPolicies={toolApprovals.length > 0}
237
+ isTriggering={policySession.isTriggering}
238
+ triggerError={policySession.error}
239
+ onGenerate={handleGenerateApprovalPolicies}
240
+ onClearError={policySession.clearError}
241
+ />
242
+ )}
243
+
244
+ {/* Inline approval policy generation panel */}
245
+ {policyPanelExecutionId && (
246
+ <ApprovalPolicyGeneratorPanel
247
+ executionId={policyPanelExecutionId}
248
+ onClose={() => setPolicyPanelExecutionId(null)}
249
+ onComplete={handlePolicyPanelComplete}
250
+ />
251
+ )}
252
+
134
253
  {spec?.envSpec && Object.keys(spec.envSpec.data).length > 0 && (
135
254
  <EnvSpecSection data={spec.envSpec.data} />
136
255
  )}
@@ -450,6 +569,223 @@ function TagsSection({ tags }: { readonly tags: readonly string[] }) {
450
569
  );
451
570
  }
452
571
 
572
+ // ---------------------------------------------------------------------------
573
+ // Discovery section
574
+ // ---------------------------------------------------------------------------
575
+
576
+ function DiscoverySection({
577
+ isDiscovering,
578
+ discoveryError,
579
+ onDiscover,
580
+ onClearDiscoveryError,
581
+ hasDiscoveredTools,
582
+ showCredentialForm,
583
+ credentialVariables,
584
+ isSavingCredentials,
585
+ onCredentialSubmit,
586
+ onCredentialCancel,
587
+ credentialsLoading,
588
+ }: {
589
+ readonly isDiscovering: boolean;
590
+ readonly discoveryError: Error | null;
591
+ readonly onDiscover: () => void;
592
+ readonly onClearDiscoveryError: () => void;
593
+ readonly hasDiscoveredTools: boolean;
594
+ readonly showCredentialForm: boolean;
595
+ readonly credentialVariables: EnvVarFormVariable[];
596
+ readonly isSavingCredentials: boolean;
597
+ readonly onCredentialSubmit: (
598
+ values: Record<string, import("@stigmer/sdk").EnvVarInput>,
599
+ options: { saveForFuture: boolean },
600
+ ) => void;
601
+ readonly onCredentialCancel: () => void;
602
+ readonly credentialsLoading: boolean;
603
+ }) {
604
+ return (
605
+ <section className="flex flex-col gap-3">
606
+ <div className="flex items-center justify-between">
607
+ <h3 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
608
+ Discovery
609
+ </h3>
610
+ <button
611
+ type="button"
612
+ onClick={onDiscover}
613
+ disabled={isDiscovering || credentialsLoading}
614
+ className={cn(
615
+ "inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium",
616
+ "border border-border bg-background text-foreground",
617
+ "hover:bg-accent hover:text-accent-foreground",
618
+ "disabled:pointer-events-none disabled:opacity-50",
619
+ )}
620
+ >
621
+ {isDiscovering ? (
622
+ <>
623
+ <DiscoverSpinner />
624
+ Discovering...
625
+ </>
626
+ ) : hasDiscoveredTools ? (
627
+ <>
628
+ <RefreshIcon className="size-3.5" />
629
+ Re-discover
630
+ </>
631
+ ) : (
632
+ <>
633
+ <DiscoverIcon className="size-3.5" />
634
+ Discover Tools
635
+ </>
636
+ )}
637
+ </button>
638
+ </div>
639
+
640
+ {discoveryError && (
641
+ <div className="flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2">
642
+ <WarningIcon className="mt-0.5 size-3.5 shrink-0 text-destructive" />
643
+ <p className="flex-1 text-xs text-destructive">
644
+ {discoveryError.message}
645
+ </p>
646
+ <button
647
+ type="button"
648
+ onClick={onClearDiscoveryError}
649
+ className="shrink-0 text-xs text-destructive/70 hover:text-destructive"
650
+ aria-label="Dismiss error"
651
+ >
652
+ Dismiss
653
+ </button>
654
+ </div>
655
+ )}
656
+
657
+ {showCredentialForm && credentialVariables.length > 0 && (
658
+ <div className="rounded-lg border border-border p-4">
659
+ <EnvVarForm
660
+ title="Credentials Required"
661
+ 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."
662
+ variables={credentialVariables}
663
+ onSubmit={(values, options) => onCredentialSubmit(values, options)}
664
+ onCancel={onCredentialCancel}
665
+ isSubmitting={isSavingCredentials}
666
+ className="w-full max-w-md"
667
+ />
668
+ </div>
669
+ )}
670
+
671
+ {!hasDiscoveredTools && !isDiscovering && (
672
+ <p className="text-xs text-muted-foreground">
673
+ No tools discovered yet. Click &quot;Discover Tools&quot; to
674
+ connect to this MCP server and list its available tools and
675
+ resources.
676
+ </p>
677
+ )}
678
+ </section>
679
+ );
680
+ }
681
+
682
+ // ---------------------------------------------------------------------------
683
+ // Approval policies section
684
+ // ---------------------------------------------------------------------------
685
+
686
+ function ApprovalPoliciesSection({
687
+ policies,
688
+ }: {
689
+ readonly policies: readonly ToolApprovalPolicy[];
690
+ }) {
691
+ return (
692
+ <Section title={`Approval Policies (${policies.length})`}>
693
+ <div className="flex flex-col divide-y divide-border">
694
+ {policies.map((policy) => (
695
+ <div key={policy.toolName} className="px-3 py-2.5">
696
+ <div className="flex items-baseline gap-2">
697
+ <code className="font-mono text-sm font-medium text-foreground">
698
+ {policy.toolName}
699
+ </code>
700
+ <ShieldIcon className="size-3 text-amber-500 dark:text-amber-400" />
701
+ </div>
702
+ {policy.message && (
703
+ <p className="mt-0.5 text-xs text-muted-foreground">
704
+ {policy.message}
705
+ </p>
706
+ )}
707
+ </div>
708
+ ))}
709
+ </div>
710
+ </Section>
711
+ );
712
+ }
713
+
714
+ // ---------------------------------------------------------------------------
715
+ // Generate approval policies section
716
+ // ---------------------------------------------------------------------------
717
+
718
+ function GenerateApprovalPoliciesSection({
719
+ hasPolicies,
720
+ isTriggering,
721
+ triggerError,
722
+ onGenerate,
723
+ onClearError,
724
+ }: {
725
+ readonly hasPolicies: boolean;
726
+ readonly isTriggering: boolean;
727
+ readonly triggerError: Error | null;
728
+ readonly onGenerate: () => void;
729
+ readonly onClearError: () => void;
730
+ }) {
731
+ return (
732
+ <section className="flex flex-col gap-2">
733
+ <div className="flex items-center justify-between">
734
+ <div>
735
+ <h3 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
736
+ AI-Generated Policies
737
+ </h3>
738
+ <p className="mt-0.5 text-xs text-muted-foreground">
739
+ {hasPolicies
740
+ ? "Regenerate approval policies using AI to classify each tool's risk level."
741
+ : "Automatically generate approval policies for the discovered tools using AI."}
742
+ </p>
743
+ </div>
744
+ <button
745
+ type="button"
746
+ onClick={onGenerate}
747
+ disabled={isTriggering}
748
+ className={cn(
749
+ "inline-flex shrink-0 items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium",
750
+ "bg-primary text-primary-foreground",
751
+ "hover:bg-primary/90",
752
+ "disabled:pointer-events-none disabled:opacity-50",
753
+ )}
754
+ >
755
+ {isTriggering ? (
756
+ <>
757
+ <DiscoverSpinner />
758
+ Starting...
759
+ </>
760
+ ) : (
761
+ <>
762
+ <SparklesIcon className="size-3.5" />
763
+ {hasPolicies ? "Regenerate Policies" : "Generate Policies"}
764
+ </>
765
+ )}
766
+ </button>
767
+ </div>
768
+
769
+ {triggerError && (
770
+ <div className="flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2">
771
+ <WarningIcon className="mt-0.5 size-3.5 shrink-0 text-destructive" />
772
+ <p className="flex-1 text-xs text-destructive">
773
+ {triggerError.message}
774
+ </p>
775
+ <button
776
+ type="button"
777
+ onClick={onClearError}
778
+ className="shrink-0 text-xs text-destructive/70 hover:text-destructive"
779
+ aria-label="Dismiss error"
780
+ >
781
+ Dismiss
782
+ </button>
783
+ </div>
784
+ )}
785
+ </section>
786
+ );
787
+ }
788
+
453
789
  // ---------------------------------------------------------------------------
454
790
  // Shared layout primitives
455
791
  // ---------------------------------------------------------------------------
@@ -604,3 +940,93 @@ function WarningIcon({ className }: { readonly className?: string }) {
604
940
  </svg>
605
941
  );
606
942
  }
943
+
944
+ function DiscoverIcon({ className }: { readonly className?: string }) {
945
+ return (
946
+ <svg
947
+ className={className}
948
+ viewBox="0 0 16 16"
949
+ fill="none"
950
+ stroke="currentColor"
951
+ strokeWidth="1.5"
952
+ strokeLinecap="round"
953
+ strokeLinejoin="round"
954
+ aria-hidden="true"
955
+ >
956
+ <circle cx="7" cy="7" r="4.5" />
957
+ <path d="m10.5 10.5 3 3" />
958
+ </svg>
959
+ );
960
+ }
961
+
962
+ function RefreshIcon({ className }: { readonly className?: string }) {
963
+ return (
964
+ <svg
965
+ className={className}
966
+ viewBox="0 0 16 16"
967
+ fill="none"
968
+ stroke="currentColor"
969
+ strokeWidth="1.5"
970
+ strokeLinecap="round"
971
+ strokeLinejoin="round"
972
+ aria-hidden="true"
973
+ >
974
+ <path d="M2.5 8a5.5 5.5 0 0 1 9.36-3.92" />
975
+ <path d="M13.5 8a5.5 5.5 0 0 1-9.36 3.92" />
976
+ <path d="M12 2v3h-3" />
977
+ <path d="M4 14v-3h3" />
978
+ </svg>
979
+ );
980
+ }
981
+
982
+ function ShieldIcon({ className }: { readonly className?: string }) {
983
+ return (
984
+ <svg
985
+ className={className}
986
+ viewBox="0 0 16 16"
987
+ fill="none"
988
+ stroke="currentColor"
989
+ strokeWidth="1.5"
990
+ strokeLinecap="round"
991
+ strokeLinejoin="round"
992
+ aria-hidden="true"
993
+ >
994
+ <path d="M8 1.5 2.5 4v4c0 3.31 2.35 6.4 5.5 7 3.15-.6 5.5-3.69 5.5-7V4L8 1.5Z" />
995
+ </svg>
996
+ );
997
+ }
998
+
999
+ function SparklesIcon({ className }: { readonly className?: string }) {
1000
+ return (
1001
+ <svg
1002
+ className={className}
1003
+ viewBox="0 0 16 16"
1004
+ fill="none"
1005
+ stroke="currentColor"
1006
+ strokeWidth="1.5"
1007
+ strokeLinecap="round"
1008
+ strokeLinejoin="round"
1009
+ aria-hidden="true"
1010
+ >
1011
+ <path d="M8 1v2M8 13v2M1 8h2M13 8h2M3.05 3.05l1.41 1.41M11.54 11.54l1.41 1.41M3.05 12.95l1.41-1.41M11.54 4.46l1.41-1.41" />
1012
+ </svg>
1013
+ );
1014
+ }
1015
+
1016
+ function DiscoverSpinner() {
1017
+ return (
1018
+ <svg
1019
+ width="14"
1020
+ height="14"
1021
+ viewBox="0 0 16 16"
1022
+ fill="none"
1023
+ stroke="currentColor"
1024
+ strokeWidth="2"
1025
+ strokeLinecap="round"
1026
+ className="animate-spin"
1027
+ aria-hidden="true"
1028
+ >
1029
+ <path d="M8 2a6 6 0 1 0 6 6" />
1030
+ </svg>
1031
+ );
1032
+ }
@@ -45,3 +45,18 @@ export type {
45
45
 
46
46
  export { McpServerDetailView } from "./McpServerDetailView";
47
47
  export type { McpServerDetailViewProps } from "./McpServerDetailView";
48
+
49
+ export { useDiscoverCapabilities } from "./useDiscoverCapabilities";
50
+ export type { UseDiscoverCapabilitiesReturn } from "./useDiscoverCapabilities";
51
+
52
+ export { useMcpServerCredentials } from "./useMcpServerCredentials";
53
+ export type { UseMcpServerCredentialsReturn } from "./useMcpServerCredentials";
54
+
55
+ export { useTriggerApprovalPolicySession } from "./useTriggerApprovalPolicySession";
56
+ export type {
57
+ UseTriggerApprovalPolicySessionReturn,
58
+ TriggerApprovalPolicyResult,
59
+ } from "./useTriggerApprovalPolicySession";
60
+
61
+ export { ApprovalPolicyGeneratorPanel } from "./ApprovalPolicyGeneratorPanel";
62
+ export type { ApprovalPolicyGeneratorPanelProps } from "./ApprovalPolicyGeneratorPanel";
@@ -0,0 +1,117 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import { create } from "@bufbuild/protobuf";
5
+ import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
6
+ import { DiscoverCapabilitiesInputSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
7
+ import type { EnvVarInput } from "@stigmer/sdk";
8
+ import { useStigmer } from "../hooks";
9
+ import { toError } from "../internal/toError";
10
+
11
+ export interface UseDiscoverCapabilitiesReturn {
12
+ /**
13
+ * Trigger server-side MCP discovery for the given MCP server.
14
+ *
15
+ * When `runtimeEnv` is provided, the values are passed directly to
16
+ * the backend as one-time credentials (not persisted to any
17
+ * environment). When omitted, the backend resolves credentials from
18
+ * the user's personal environment.
19
+ *
20
+ * Blocks until discovery completes (~30s timeout).
21
+ *
22
+ * @param mcpServerId - System-generated ID of the MCP server (metadata.id).
23
+ * @param runtimeEnv - Optional one-time environment variables for discovery.
24
+ * @returns The updated McpServer with populated discovered_capabilities.
25
+ */
26
+ readonly discover: (
27
+ mcpServerId: string,
28
+ runtimeEnv?: Record<string, EnvVarInput>,
29
+ ) => Promise<McpServer>;
30
+ /** `true` while a discovery RPC is in flight. */
31
+ readonly isDiscovering: boolean;
32
+ /** Error from the most recent failed discovery, or `null`. */
33
+ readonly error: Error | null;
34
+ /** Clear the error state. */
35
+ readonly clearError: () => void;
36
+ }
37
+
38
+ /**
39
+ * Action hook for triggering server-side MCP server capability discovery.
40
+ *
41
+ * Calls the `discoverCapabilities` RPC which delegates to the agent-runner
42
+ * via a Temporal workflow. The RPC blocks until discovery completes
43
+ * (typically 5-15 seconds, ~30s timeout).
44
+ *
45
+ * Supports two modes:
46
+ * - **Save for future** (default): Credentials are saved to the personal
47
+ * environment first, then `discover(id)` is called without `runtimeEnv`.
48
+ * - **One-time use**: `discover(id, runtimeEnv)` passes credentials
49
+ * directly to the backend. They are used for this discovery only and
50
+ * not persisted to any environment.
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * const { discover, isDiscovering, error } = useDiscoverCapabilities();
55
+ * const { refetch } = useMcpServer(org, slug);
56
+ *
57
+ * // Save for future: credentials already in personal environment
58
+ * async function handleDiscoverSaved() {
59
+ * await discover(mcpServer.metadata.id);
60
+ * refetch();
61
+ * }
62
+ *
63
+ * // One-time use: pass credentials directly
64
+ * async function handleDiscoverTemporary(values: Record<string, EnvVarInput>) {
65
+ * await discover(mcpServer.metadata.id, values);
66
+ * refetch();
67
+ * }
68
+ * ```
69
+ */
70
+ export function useDiscoverCapabilities(): UseDiscoverCapabilitiesReturn {
71
+ const stigmer = useStigmer();
72
+ const [isDiscovering, setIsDiscovering] = useState(false);
73
+ const [error, setError] = useState<Error | null>(null);
74
+
75
+ const clearError = useCallback(() => setError(null), []);
76
+
77
+ const discover = useCallback(
78
+ async (
79
+ mcpServerId: string,
80
+ runtimeEnv?: Record<string, EnvVarInput>,
81
+ ): Promise<McpServer> => {
82
+ setIsDiscovering(true);
83
+ setError(null);
84
+
85
+ try {
86
+ const runtimeEnvMap: Record<string, { value: string; isSecret: boolean }> = {};
87
+ if (runtimeEnv) {
88
+ for (const [key, input] of Object.entries(runtimeEnv)) {
89
+ runtimeEnvMap[key] = {
90
+ value: input.value,
91
+ isSecret: input.isSecret ?? false,
92
+ };
93
+ }
94
+ }
95
+
96
+ const input = create(DiscoverCapabilitiesInputSchema, {
97
+ mcpServerId,
98
+ ...(Object.keys(runtimeEnvMap).length > 0 && {
99
+ runtimeEnv: runtimeEnvMap,
100
+ }),
101
+ });
102
+
103
+ const result = await stigmer.mcpServer.discoverCapabilities(input);
104
+ return result;
105
+ } catch (err) {
106
+ const wrapped = toError(err);
107
+ setError(wrapped);
108
+ throw wrapped;
109
+ } finally {
110
+ setIsDiscovering(false);
111
+ }
112
+ },
113
+ [stigmer],
114
+ );
115
+
116
+ return { discover, isDiscovering, error, clearError };
117
+ }