@burdenoff/fluidgrids-fe-sdk 2026.1028.5 → 2026.1028.7

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 (47) hide show
  1. package/dist/ExecutionDetailPage-A4JQGL34.js +20 -0
  2. package/dist/{ExecutionsListPage-XQ5FQJ4M.js → ExecutionsListPage-BT3ADBXP.js} +8 -8
  3. package/dist/WorkflowBuilder-SQQI4VSF.js +23 -0
  4. package/dist/{WorkflowsList-QBKX733V.js → WorkflowsList-FZ4W64YZ.js} +8 -8
  5. package/dist/{chunk-CSBKB5EX.js → chunk-27XYEFCK.js} +1 -1
  6. package/dist/{chunk-SRZITTB2.js → chunk-2YDUYSB3.js} +5 -5
  7. package/dist/chunk-3ENHRYM5.js +749 -0
  8. package/dist/{chunk-H7CJXSMI.js → chunk-5YP3U7BU.js} +3 -3
  9. package/dist/{chunk-JBLYBD2B.js → chunk-6J4RR46J.js} +6 -1
  10. package/dist/{chunk-5LKZ4PNK.js → chunk-6NUHJPGJ.js} +4 -2
  11. package/dist/{chunk-Y6YOY33Z.js → chunk-BBORXXGZ.js} +1 -1
  12. package/dist/{chunk-VUVTOUQ5.js → chunk-DUXOFCPO.js} +1 -1
  13. package/dist/{chunk-P2NRMJ7Z.js → chunk-HDUCP6EO.js} +1 -1
  14. package/dist/{chunk-FOEEDKBP.js → chunk-IRKB2GGJ.js} +22 -1
  15. package/dist/{chunk-SSJIK6WS.js → chunk-JNJDVC7V.js} +2 -2
  16. package/dist/{chunk-WBH2XEWO.js → chunk-JUUYPCFY.js} +4 -1
  17. package/dist/chunk-LOZ6XNOX.js +10550 -0
  18. package/dist/{chunk-QGXA2USP.js → chunk-MKKBEVXO.js} +1 -1
  19. package/dist/chunk-MUOJVRW7.js +328 -0
  20. package/dist/{chunk-JAKWVVVQ.js → chunk-NW6H7EN3.js} +15 -3
  21. package/dist/{chunk-WRMIYMXE.js → chunk-PBX42F4Q.js} +1401 -7
  22. package/dist/{chunk-63BP6O6E.js → chunk-PYCKEQ4W.js} +2 -2
  23. package/dist/{chunk-S4VYW4BR.js → chunk-QMBXEECD.js} +120 -6
  24. package/dist/{chunk-AYFKS4MM.js → chunk-QW24ZBSR.js} +1 -1
  25. package/dist/{chunk-UKTLRE7N.js → chunk-RJ3YK6O7.js} +5 -5
  26. package/dist/{chunk-UUUDSVC7.js → chunk-RKMTSOYH.js} +2 -2
  27. package/dist/{chunk-LWF5J43N.js → chunk-T7TRJBCG.js} +1 -1
  28. package/dist/{chunk-DB23N7KO.js → chunk-U7WLROQT.js} +3 -3
  29. package/dist/{chunk-JH6YDVT5.js → chunk-XYZKIFXY.js} +426 -10667
  30. package/dist/{chunk-ZF55RVJ3.js → chunk-ZB4BNTWR.js} +4 -4
  31. package/dist/executions.js +13 -11
  32. package/dist/global-operations-D2USVOPM.js +2 -0
  33. package/dist/history.js +5 -5
  34. package/dist/index.d.ts +357 -76
  35. package/dist/index.js +36 -33
  36. package/dist/nodes.js +2 -2
  37. package/dist/observability.js +7 -5
  38. package/dist/settings.js +7 -6
  39. package/dist/templates.js +11 -9
  40. package/dist/{workflow-builder-C7qKY7LD.d.ts → workflow-builder-BxyapBo5.d.ts} +18 -0
  41. package/dist/workflow-builder.d.ts +1 -1
  42. package/dist/workflow-builder.js +13 -11
  43. package/dist/workflows-list.js +8 -8
  44. package/package.json +1 -1
  45. package/dist/ExecutionDetailPage-UFB7VMGE.js +0 -18
  46. package/dist/WorkflowBuilder-2UHPYNQI.js +0 -21
  47. package/dist/global-operations-ZWQZ7DNT.js +0 -2
@@ -1,18 +1,22 @@
1
- import { ActionPoliciesTab } from './chunk-VUVTOUQ5.js';
2
- import { useFluidGridsFlag, FLUIDGRIDS_FLAGS, LogStreamingTab } from './chunk-LWF5J43N.js';
1
+ import { ActionPoliciesTab } from './chunk-DUXOFCPO.js';
2
+ import { useFluidGridsFlag, FLUIDGRIDS_FLAGS, LogStreamingTab } from './chunk-T7TRJBCG.js';
3
+ import { AgentsTab } from './chunk-3ENHRYM5.js';
3
4
  import { FluidGridsDocsLink } from './chunk-4VJ3MDOV.js';
4
- import { useFlowStore, useSaveStatusStore, useFluidGridsPermission, FLUIDGRIDS_PERMISSIONS, throwIfResultError } from './chunk-5LKZ4PNK.js';
5
- import { useWorkflowDefinitionQuery, useUpdateWorkflowDefinitionMutation, useWorkflowDefinitionsQuery } from './chunk-JBLYBD2B.js';
5
+ import { useFlowStore, useSaveStatusStore, useFluidGridsPermission, FLUIDGRIDS_PERMISSIONS, throwIfResultError, describeOperationError, useConnections } from './chunk-6NUHJPGJ.js';
6
+ import { getGlobalStoreClient } from './chunk-PXIFUMLA.js';
7
+ import { getFluidGridsConfig } from './chunk-CY6KX4RG.js';
8
+ import { useWorkflowDefinitionQuery, useUpdateWorkflowDefinitionMutation, useWorkflowDefinitionsQuery } from './chunk-6J4RR46J.js';
6
9
  import { useTr } from './chunk-UULVIT25.js';
7
10
  import { getFluidGridsClient } from './chunk-NTWG3CJX.js';
8
11
  import { useState, useMemo, useEffect, useCallback } from 'react';
9
12
  import { useNavigate, useParams } from 'react-router';
10
13
  import { Icon } from '@iconify/react';
11
14
  import { Button } from '@burdenoff/fe-libs/button';
12
- import { Tabs, TabsList, TabsTrigger, TabsContent, Input, Switch, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Card, CardHeader, Label, TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@burdenoff/fe-libs/ui';
15
+ import { Tabs, TabsList, TabsTrigger, TabsContent, Input, Switch, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Label, Card, CardHeader, Badge, Checkbox, TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@burdenoff/fe-libs/ui';
13
16
  import { toast } from 'sonner';
14
17
  import { jsxs, jsx } from 'react/jsx-runtime';
15
18
  import { gql } from '@apollo/client';
19
+ import { cn } from '@burdenoff/fe-libs/utils';
16
20
 
17
21
  function SettingRow({
18
22
  label,
@@ -94,6 +98,29 @@ function fromSavingChoice(choice) {
94
98
  if (choice === "skip") return false;
95
99
  return void 0;
96
100
  }
101
+ var MAX_REDACT_FIELDS = 50;
102
+ var MAX_REDACT_FIELD_LENGTH = 200;
103
+ var MAX_REDACT_PATH_SEGMENTS = 16;
104
+ function parseRedactFields(text) {
105
+ const seen = /* @__PURE__ */ new Set();
106
+ const fields = [];
107
+ for (const raw of text.split(/[,\n]/)) {
108
+ const segments = raw.trim().split(".").map((segment) => segment.trim());
109
+ const field = segments.join(".");
110
+ if (field.length === 0 || field.length > MAX_REDACT_FIELD_LENGTH || segments.length > MAX_REDACT_PATH_SEGMENTS || segments.some((segment) => segment.length === 0)) {
111
+ continue;
112
+ }
113
+ const key = field.toLowerCase();
114
+ if (seen.has(key)) continue;
115
+ seen.add(key);
116
+ fields.push(field);
117
+ if (fields.length >= MAX_REDACT_FIELDS) break;
118
+ }
119
+ return fields.length > 0 ? fields : void 0;
120
+ }
121
+ function formatRedactFields(fields) {
122
+ return (fields ?? []).join(", ");
123
+ }
97
124
  function compactOverrides(value) {
98
125
  if (!value) return void 0;
99
126
  const entries = Object.entries(value).filter(([, v]) => v !== void 0);
@@ -370,6 +397,1261 @@ function parseOptionalPositiveInt(value) {
370
397
  return Number.isFinite(parsed) ? Math.max(1, Math.trunc(parsed)) : void 0;
371
398
  }
372
399
 
400
+ // src/modules/mcp-gateway/utils/mcp-form.ts
401
+ var MCP_TOOL_NAME = /^[a-z][a-z0-9_-]{0,63}$/;
402
+ var MCP_TOKEN_DEFAULT_DAYS = 90;
403
+ var MCP_TOKEN_MAX_DAYS = 365;
404
+ var McpFormError = class extends Error {
405
+ constructor(message) {
406
+ super(message);
407
+ this.name = "McpFormError";
408
+ }
409
+ };
410
+ function assertToolName(raw) {
411
+ const name = raw.trim();
412
+ if (!MCP_TOOL_NAME.test(name)) {
413
+ throw new McpFormError(
414
+ "Use lowercase letters, digits, _ and - (starting with a letter, 64 characters at most)."
415
+ );
416
+ }
417
+ return name;
418
+ }
419
+ function emptyTokenForm() {
420
+ return {
421
+ name: "",
422
+ allTools: false,
423
+ tools: [],
424
+ pinVersion: null,
425
+ expiresInDays: MCP_TOKEN_DEFAULT_DAYS
426
+ };
427
+ }
428
+ function tokenScopes(form) {
429
+ const scopes = form.allTools ? ["mcp:*"] : [...new Set(form.tools)].map((tool) => `mcp:tool:${tool}`);
430
+ if (form.pinVersion !== null) scopes.push(`mcp:toolset:${form.pinVersion}`);
431
+ return scopes;
432
+ }
433
+ function validateTokenForm(form, now = /* @__PURE__ */ new Date()) {
434
+ const name = form.name.trim();
435
+ if (!name) throw new McpFormError("Give the token a name.");
436
+ if (!form.allTools && form.tools.length === 0) {
437
+ throw new McpFormError("Choose the tools the token may call.");
438
+ }
439
+ const days = Math.trunc(form.expiresInDays);
440
+ if (!Number.isFinite(days) || days < 1 || days > MCP_TOKEN_MAX_DAYS) {
441
+ throw new McpFormError(
442
+ `A token lasts between 1 and ${MCP_TOKEN_MAX_DAYS} days.`
443
+ );
444
+ }
445
+ for (const tool of form.tools) assertToolName(tool);
446
+ return {
447
+ name,
448
+ scopes: tokenScopes(form),
449
+ expiresAt: new Date(now.getTime() + days * 864e5).toISOString()
450
+ };
451
+ }
452
+ function isMcpKey(key) {
453
+ return key.scopes.some((scope) => scope.startsWith("mcp:"));
454
+ }
455
+ function describeScopes(scopes) {
456
+ const mcp = scopes.filter((scope) => scope.startsWith("mcp:"));
457
+ if (mcp.length === 0) return "No MCP access";
458
+ const parts = [];
459
+ if (mcp.includes("mcp:*")) parts.push("All tools");
460
+ const tools = mcp.filter((scope) => scope.startsWith("mcp:tool:")).map((scope) => scope.slice("mcp:tool:".length));
461
+ if (tools.length > 0) parts.push(tools.join(", "));
462
+ const versions = mcp.filter((scope) => scope.startsWith("mcp:toolset:")).map((scope) => `v${scope.slice("mcp:toolset:".length)}`);
463
+ if (versions.length > 0) parts.push(`pinned to ${versions.join(", ")}`);
464
+ return parts.join(" \xB7 ");
465
+ }
466
+ function endpointUrl(gatewayUrl, path) {
467
+ const base = gatewayUrl.replace(/\/workspaces\/graphql\/?$/, "");
468
+ return `${base.replace(/\/+$/, "")}${path}`;
469
+ }
470
+ function callTone(status) {
471
+ switch (status) {
472
+ case "SUCCEEDED":
473
+ return "success";
474
+ case "RUNNING":
475
+ case "PENDING_APPROVAL":
476
+ return "warning";
477
+ case "FAILED":
478
+ case "DENIED":
479
+ return "danger";
480
+ default:
481
+ return "neutral";
482
+ }
483
+ }
484
+ function canDecide(call, now = /* @__PURE__ */ new Date()) {
485
+ if (call.approvalStatus !== "PENDING") return false;
486
+ return !call.approvalExpiresAt || new Date(call.approvalExpiresAt).getTime() > now.getTime();
487
+ }
488
+
489
+ // src/modules/mcp-gateway/hooks/useMcpGateway.ts
490
+ var TOOL_FIELDS = `
491
+ id
492
+ toolName
493
+ kind
494
+ description
495
+ workflowKey
496
+ nodeType
497
+ nodeVersion
498
+ operation
499
+ connectionId
500
+ adapter
501
+ readOnly
502
+ allowWrite
503
+ enabled
504
+ updatedAt
505
+ `;
506
+ var TOOL_SET_FIELDS = `
507
+ id
508
+ version
509
+ status
510
+ note
511
+ publishedAt
512
+ tools { name kind description readOnly workflowKey nodeType operation }
513
+ `;
514
+ var CALL_FIELDS = `
515
+ id
516
+ toolSetVersion
517
+ toolName
518
+ toolKind
519
+ actorId
520
+ decision
521
+ decisionReason
522
+ policyName
523
+ status
524
+ runId
525
+ latencyMs
526
+ errorCode
527
+ errorMessage
528
+ approvalStatus
529
+ approvalExpiresAt
530
+ argsSha256
531
+ createdAt
532
+ `;
533
+ var OVERVIEW = gql`
534
+ query McpGatewayOverview {
535
+ mcpEndpoint { path productKey workspaceId latestVersion }
536
+ mcpExposedTools { ${TOOL_FIELDS} }
537
+ mcpToolSets { ${TOOL_SET_FIELDS} }
538
+ }
539
+ `;
540
+ var CALLS = gql`
541
+ query McpToolCalls($first: Int, $after: String, $filter: McpToolCallFilter) {
542
+ mcpToolCalls(first: $first, after: $after, filter: $filter) {
543
+ items { ${CALL_FIELDS} }
544
+ nextCursor
545
+ hasMore
546
+ }
547
+ }
548
+ `;
549
+ var EXPOSE_WORKFLOW = gql`
550
+ mutation ExposeWorkflowAsMcpTool($input: ExposeWorkflowAsMcpToolInput!) {
551
+ exposeWorkflowAsMcpTool(input: $input) { ${TOOL_FIELDS} }
552
+ }
553
+ `;
554
+ var EXPOSE_CONNECTOR = gql`
555
+ mutation ExposeConnectorOperationAsMcpTool($input: ExposeConnectorOperationAsMcpToolInput!) {
556
+ exposeConnectorOperationAsMcpTool(input: $input) { ${TOOL_FIELDS} }
557
+ }
558
+ `;
559
+ var SET_ENABLED = gql`
560
+ mutation SetMcpToolEnabled($id: ID!, $enabled: Boolean!) {
561
+ setMcpToolEnabled(id: $id, enabled: $enabled) {
562
+ id
563
+ enabled
564
+ }
565
+ }
566
+ `;
567
+ var REMOVE_TOOL = gql`
568
+ mutation RemoveMcpTool($id: ID!) {
569
+ removeMcpTool(id: $id)
570
+ }
571
+ `;
572
+ var PUBLISH = gql`
573
+ mutation PublishMcpToolSet($note: String) {
574
+ publishMcpToolSet(note: $note) {
575
+ created
576
+ toolSet { ${TOOL_SET_FIELDS} }
577
+ skipped { toolName reason }
578
+ }
579
+ }
580
+ `;
581
+ var DEPRECATE = gql`
582
+ mutation DeprecateMcpToolSet($version: Int!) {
583
+ deprecateMcpToolSet(version: $version) {
584
+ id
585
+ status
586
+ }
587
+ }
588
+ `;
589
+ var DECIDE = gql`
590
+ mutation DecideMcpToolCallApproval($id: ID!, $approve: Boolean!) {
591
+ decideMcpToolCallApproval(id: $id, approve: $approve) {
592
+ id
593
+ approvalStatus
594
+ }
595
+ }
596
+ `;
597
+ var MY_API_KEYS = gql`
598
+ query McpApiKeys($pagination: PaginationInput) {
599
+ myApiKeys(pagination: $pagination) {
600
+ apiKeys {
601
+ id
602
+ name
603
+ prefix
604
+ appId
605
+ appName
606
+ scopes
607
+ expiresAt
608
+ lastUsedAt
609
+ createdAt
610
+ }
611
+ }
612
+ }
613
+ `;
614
+ var CREATE_APP = gql`
615
+ mutation CreateMcpClientApp($input: CreateAppInput!) {
616
+ createApp(input: $input) {
617
+ app {
618
+ id
619
+ }
620
+ }
621
+ }
622
+ `;
623
+ var GRANT_WORKSPACE = gql`
624
+ mutation GrantMcpClientWorkspace($input: GrantAppWorkspaceAccessInput!) {
625
+ grantAppWorkspaceAccess(input: $input) {
626
+ id
627
+ status
628
+ }
629
+ }
630
+ `;
631
+ var GENERATE_KEY = gql`
632
+ mutation GenerateMcpApiKey($input: GenerateAPIKeyInput!) {
633
+ generateAPIKey(input: $input) {
634
+ apiKey
635
+ apiSecret
636
+ keyId
637
+ }
638
+ }
639
+ `;
640
+ var REVOKE_KEY = gql`
641
+ mutation RevokeMcpApiKey($keyId: String!) {
642
+ revokeAPIKey(keyId: $keyId)
643
+ }
644
+ `;
645
+ function globalClient() {
646
+ const client = getGlobalStoreClient();
647
+ if (!client) {
648
+ throw new McpFormError("Token management is not available in this app.");
649
+ }
650
+ return client;
651
+ }
652
+ function useMcpGateway() {
653
+ const [endpoint, setEndpoint] = useState(null);
654
+ const [tools, setTools] = useState([]);
655
+ const [toolSets, setToolSets] = useState([]);
656
+ const [loading, setLoading] = useState(true);
657
+ const [error, setError] = useState(null);
658
+ const reload = useCallback(async () => {
659
+ setLoading(true);
660
+ try {
661
+ const result = await getFluidGridsClient().query({ query: OVERVIEW, fetchPolicy: "network-only" });
662
+ throwIfResultError(result);
663
+ setEndpoint(result.data?.mcpEndpoint ?? null);
664
+ setTools(result.data?.mcpExposedTools ?? []);
665
+ setToolSets(result.data?.mcpToolSets ?? []);
666
+ setError(null);
667
+ } catch (err) {
668
+ setError(err);
669
+ } finally {
670
+ setLoading(false);
671
+ }
672
+ }, []);
673
+ useEffect(() => {
674
+ void reload();
675
+ }, [reload]);
676
+ const mutateAndReload = useCallback(
677
+ async (mutation, variables) => {
678
+ const result = await getFluidGridsClient().mutate({
679
+ mutation,
680
+ variables
681
+ });
682
+ throwIfResultError(result);
683
+ await reload();
684
+ return result.data;
685
+ },
686
+ [reload]
687
+ );
688
+ const exposeWorkflow = useCallback(
689
+ (input) => mutateAndReload(EXPOSE_WORKFLOW, { input }),
690
+ [mutateAndReload]
691
+ );
692
+ const exposeConnector = useCallback(
693
+ (input) => mutateAndReload(EXPOSE_CONNECTOR, { input }),
694
+ [mutateAndReload]
695
+ );
696
+ const setEnabled = useCallback(
697
+ (id, enabled) => mutateAndReload(SET_ENABLED, { id, enabled }),
698
+ [mutateAndReload]
699
+ );
700
+ const removeTool = useCallback(
701
+ (id) => mutateAndReload(REMOVE_TOOL, { id }),
702
+ [mutateAndReload]
703
+ );
704
+ const publish = useCallback(
705
+ async (note) => {
706
+ const data = await mutateAndReload(PUBLISH, { note: note || null });
707
+ if (!data?.publishMcpToolSet)
708
+ throw new McpFormError("Publishing returned nothing.");
709
+ return data.publishMcpToolSet;
710
+ },
711
+ [mutateAndReload]
712
+ );
713
+ const deprecate = useCallback(
714
+ (version) => mutateAndReload(DEPRECATE, { version }),
715
+ [mutateAndReload]
716
+ );
717
+ return {
718
+ endpoint,
719
+ tools,
720
+ toolSets,
721
+ loading,
722
+ error,
723
+ reload,
724
+ exposeWorkflow,
725
+ exposeConnector,
726
+ setEnabled,
727
+ removeTool,
728
+ publish,
729
+ deprecate
730
+ };
731
+ }
732
+ function useMcpToolCalls(pageSize = 25) {
733
+ const [calls, setCalls] = useState([]);
734
+ const [cursor, setCursor] = useState(null);
735
+ const [hasMore, setHasMore] = useState(false);
736
+ const [loading, setLoading] = useState(true);
737
+ const [statusFilter, setStatusFilter] = useState(null);
738
+ const fetchPage = useCallback(
739
+ async (after) => {
740
+ const result = await getFluidGridsClient().query({
741
+ query: CALLS,
742
+ variables: {
743
+ first: pageSize,
744
+ after,
745
+ filter: statusFilter ? { status: statusFilter } : null
746
+ },
747
+ fetchPolicy: "network-only"
748
+ });
749
+ throwIfResultError(result);
750
+ return result.data?.mcpToolCalls ?? {
751
+ items: [],
752
+ nextCursor: null,
753
+ hasMore: false
754
+ };
755
+ },
756
+ [pageSize, statusFilter]
757
+ );
758
+ const reload = useCallback(async () => {
759
+ setLoading(true);
760
+ try {
761
+ const page = await fetchPage(null);
762
+ setCalls(page.items);
763
+ setCursor(page.nextCursor);
764
+ setHasMore(page.hasMore);
765
+ } finally {
766
+ setLoading(false);
767
+ }
768
+ }, [fetchPage]);
769
+ const loadMore = useCallback(async () => {
770
+ if (!cursor) return;
771
+ const page = await fetchPage(cursor);
772
+ setCalls((current) => [...current, ...page.items]);
773
+ setCursor(page.nextCursor);
774
+ setHasMore(page.hasMore);
775
+ }, [cursor, fetchPage]);
776
+ useEffect(() => {
777
+ void reload().catch(() => void 0);
778
+ }, [reload]);
779
+ const decide = useCallback(
780
+ async (id, approve) => {
781
+ const result = await getFluidGridsClient().mutate({
782
+ mutation: DECIDE,
783
+ variables: { id, approve }
784
+ });
785
+ throwIfResultError(result);
786
+ await reload();
787
+ },
788
+ [reload]
789
+ );
790
+ return {
791
+ calls,
792
+ hasMore,
793
+ loading,
794
+ statusFilter,
795
+ setStatusFilter,
796
+ reload,
797
+ loadMore,
798
+ decide
799
+ };
800
+ }
801
+ function useMcpTokens() {
802
+ const [keys, setKeys] = useState([]);
803
+ const [available, setAvailable] = useState(true);
804
+ const [loading, setLoading] = useState(true);
805
+ const reload = useCallback(async () => {
806
+ const client = getGlobalStoreClient();
807
+ if (!client) {
808
+ setAvailable(false);
809
+ setLoading(false);
810
+ return;
811
+ }
812
+ setLoading(true);
813
+ try {
814
+ const result = await client.query({
815
+ query: MY_API_KEYS,
816
+ variables: { pagination: { limit: 100, offset: 0 } },
817
+ fetchPolicy: "network-only"
818
+ });
819
+ throwIfResultError(result);
820
+ setKeys((result.data?.myApiKeys.apiKeys ?? []).filter(isMcpKey));
821
+ } finally {
822
+ setLoading(false);
823
+ }
824
+ }, []);
825
+ useEffect(() => {
826
+ void reload().catch(() => void 0);
827
+ }, [reload]);
828
+ const create = useCallback(
829
+ async (input) => {
830
+ const client = globalClient();
831
+ const { workspaceId, tenantId } = getFluidGridsConfig();
832
+ if (!workspaceId || !tenantId) {
833
+ throw new McpFormError(
834
+ "The workspace context is not ready yet. Try again shortly."
835
+ );
836
+ }
837
+ const app = await client.mutate({
838
+ mutation: CREATE_APP,
839
+ variables: {
840
+ input: {
841
+ name: `MCP: ${input.name}`.slice(0, 120),
842
+ description: "MCP client created in FluidGrids Settings \u2192 MCP",
843
+ type: "API_INTEGRATION",
844
+ scopes: input.scopes
845
+ }
846
+ }
847
+ });
848
+ throwIfResultError(app);
849
+ const appId = app.data?.createApp.app.id;
850
+ if (!appId)
851
+ throw new McpFormError("The MCP client could not be created.");
852
+ throwIfResultError(
853
+ await client.mutate({
854
+ mutation: GRANT_WORKSPACE,
855
+ variables: { input: { appId, workspaceId, tenantId } }
856
+ })
857
+ );
858
+ const key = await client.mutate({
859
+ mutation: GENERATE_KEY,
860
+ variables: {
861
+ input: {
862
+ appId,
863
+ name: input.name,
864
+ scope: input.scopes,
865
+ expiresAt: input.expiresAt
866
+ }
867
+ }
868
+ });
869
+ throwIfResultError(key);
870
+ if (!key.data?.generateAPIKey)
871
+ throw new McpFormError("The API key could not be created.");
872
+ await reload();
873
+ return key.data.generateAPIKey;
874
+ },
875
+ [reload]
876
+ );
877
+ const revoke = useCallback(
878
+ async (keyId) => {
879
+ throwIfResultError(
880
+ await globalClient().mutate({
881
+ mutation: REVOKE_KEY,
882
+ variables: { keyId }
883
+ })
884
+ );
885
+ await reload();
886
+ },
887
+ [reload]
888
+ );
889
+ return { keys, available, loading, reload, create, revoke };
890
+ }
891
+ var FIELD = "rounded-md border border-border-secondary bg-background-secondary px-2 py-1.5 text-sm text-text-primary";
892
+ var CARD = "rounded-lg border border-border-secondary bg-background-primary p-4 space-y-3";
893
+ var TONE_CLASSES = {
894
+ success: "bg-status-success-bg-subtle text-status-success-text border-status-success-border",
895
+ warning: "bg-status-warning-bg-subtle text-status-warning-text border-status-warning-border",
896
+ danger: "bg-status-error-bg-subtle text-status-error-text border-status-error-border",
897
+ neutral: "bg-background-secondary text-text-secondary border-border-secondary"
898
+ };
899
+ function errorMessage(err) {
900
+ return err instanceof McpFormError ? err.message : describeOperationError(err);
901
+ }
902
+ function ToneBadge({
903
+ tone,
904
+ children
905
+ }) {
906
+ return /* @__PURE__ */ jsx(Badge, { variant: "outline", className: cn("text-xs", TONE_CLASSES[tone]), children });
907
+ }
908
+ async function copy(value, done, failed = "Copy failed") {
909
+ try {
910
+ await navigator.clipboard.writeText(value);
911
+ toast.success(done);
912
+ } catch {
913
+ toast.error(failed);
914
+ }
915
+ }
916
+ async function attempt(action, success) {
917
+ try {
918
+ await action();
919
+ if (success) toast.success(success);
920
+ return true;
921
+ } catch (err) {
922
+ toast.error(errorMessage(err));
923
+ return false;
924
+ }
925
+ }
926
+ function ConnectorToolForm({
927
+ onSave
928
+ }) {
929
+ const tr = useTr();
930
+ const { connections } = useConnections();
931
+ const [toolName, setToolName] = useState("");
932
+ const [nodeType, setNodeType] = useState("");
933
+ const [operation, setOperation] = useState("");
934
+ const [connectionId, setConnectionId] = useState("");
935
+ const [description, setDescription] = useState("");
936
+ const [allowWrite, setAllowWrite] = useState(false);
937
+ const [busy, setBusy] = useState(false);
938
+ const connection = connections.find((entry) => entry.id === connectionId);
939
+ const submit = async () => {
940
+ setBusy(true);
941
+ try {
942
+ const saved = await onSave({
943
+ toolName: assertToolName(toolName),
944
+ nodeType: nodeType.trim(),
945
+ operation: operation.trim(),
946
+ connectionId,
947
+ adapter: connection?.provider.key ?? connection?.providerId ?? "",
948
+ description: description.trim() || void 0,
949
+ allowWrite
950
+ });
951
+ if (saved) {
952
+ setToolName("");
953
+ setOperation("");
954
+ setDescription("");
955
+ setAllowWrite(false);
956
+ }
957
+ } catch (err) {
958
+ toast.error(errorMessage(err));
959
+ } finally {
960
+ setBusy(false);
961
+ }
962
+ };
963
+ return /* @__PURE__ */ jsxs(
964
+ "div",
965
+ {
966
+ className: "space-y-2 border-t border-border-secondary pt-3",
967
+ "data-testid": "mcp-connector-form",
968
+ children: [
969
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-text-primary", children: tr("fluidgrids.mcp.connector.title", "Expose a connector operation") }),
970
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-text-secondary", children: tr(
971
+ "fluidgrids.mcp.connector.help",
972
+ "Read operations (get, list, search, ...) can be exposed as they are. An operation that changes data is exposed only when you allow writes for this tool, and every call still passes your action policies."
973
+ ) }),
974
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-2 sm:grid-cols-2", children: [
975
+ /* @__PURE__ */ jsx(
976
+ Input,
977
+ {
978
+ "aria-label": tr("fluidgrids.mcp.connector.toolName", "Tool name"),
979
+ placeholder: "list_customers",
980
+ value: toolName,
981
+ onChange: (e) => setToolName(e.target.value)
982
+ }
983
+ ),
984
+ /* @__PURE__ */ jsx(
985
+ Input,
986
+ {
987
+ "aria-label": tr("fluidgrids.mcp.connector.nodeType", "Node type"),
988
+ placeholder: "store/stripe",
989
+ value: nodeType,
990
+ onChange: (e) => setNodeType(e.target.value)
991
+ }
992
+ ),
993
+ /* @__PURE__ */ jsx(
994
+ Input,
995
+ {
996
+ "aria-label": tr("fluidgrids.mcp.connector.operation", "Operation"),
997
+ placeholder: "listCustomers",
998
+ value: operation,
999
+ onChange: (e) => setOperation(e.target.value)
1000
+ }
1001
+ ),
1002
+ /* @__PURE__ */ jsxs(
1003
+ "select",
1004
+ {
1005
+ "aria-label": tr("fluidgrids.mcp.connector.connection", "Connection"),
1006
+ className: FIELD,
1007
+ value: connectionId,
1008
+ onChange: (e) => setConnectionId(e.target.value),
1009
+ children: [
1010
+ /* @__PURE__ */ jsx("option", { value: "", children: tr(
1011
+ "fluidgrids.mcp.connector.pickConnection",
1012
+ "Choose a connection"
1013
+ ) }),
1014
+ connections.map((entry) => /* @__PURE__ */ jsxs("option", { value: entry.id, children: [
1015
+ entry.name,
1016
+ " (",
1017
+ entry.provider.name,
1018
+ ")"
1019
+ ] }, entry.id))
1020
+ ]
1021
+ }
1022
+ ),
1023
+ /* @__PURE__ */ jsx(
1024
+ Input,
1025
+ {
1026
+ className: "sm:col-span-2",
1027
+ "aria-label": tr("fluidgrids.mcp.connector.description", "Description"),
1028
+ placeholder: tr(
1029
+ "fluidgrids.mcp.connector.descriptionPlaceholder",
1030
+ "What the tool does (optional)"
1031
+ ),
1032
+ value: description,
1033
+ onChange: (e) => setDescription(e.target.value)
1034
+ }
1035
+ )
1036
+ ] }),
1037
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
1038
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1039
+ /* @__PURE__ */ jsx(
1040
+ Checkbox,
1041
+ {
1042
+ id: "mcp-allow-write",
1043
+ checked: allowWrite,
1044
+ onCheckedChange: (on) => setAllowWrite(on === true)
1045
+ }
1046
+ ),
1047
+ /* @__PURE__ */ jsx(Label, { htmlFor: "mcp-allow-write", className: "text-sm", children: tr(
1048
+ "fluidgrids.mcp.connector.allowWrite",
1049
+ "Allow this tool to change data"
1050
+ ) })
1051
+ ] }),
1052
+ /* @__PURE__ */ jsx(
1053
+ Button,
1054
+ {
1055
+ size: "sm",
1056
+ onClick: () => void submit(),
1057
+ disabled: busy || !connectionId,
1058
+ children: tr("fluidgrids.mcp.connector.add", "Add tool")
1059
+ }
1060
+ )
1061
+ ] })
1062
+ ]
1063
+ }
1064
+ );
1065
+ }
1066
+ function TokenSection({
1067
+ toolNames,
1068
+ versions
1069
+ }) {
1070
+ const tr = useTr();
1071
+ const tokens = useMcpTokens();
1072
+ const [form, setForm] = useState(emptyTokenForm);
1073
+ const [created, setCreated] = useState(null);
1074
+ const [busy, setBusy] = useState(false);
1075
+ if (!tokens.available) {
1076
+ return /* @__PURE__ */ jsx("p", { className: "text-sm text-text-secondary", children: tr(
1077
+ "fluidgrids.mcp.tokens.unavailable",
1078
+ "MCP tokens are managed from the developer portal in this app."
1079
+ ) });
1080
+ }
1081
+ const submit = async () => {
1082
+ setBusy(true);
1083
+ try {
1084
+ const valid = validateTokenForm(form);
1085
+ setCreated(await tokens.create(valid));
1086
+ setForm(emptyTokenForm());
1087
+ } catch (err) {
1088
+ toast.error(errorMessage(err));
1089
+ } finally {
1090
+ setBusy(false);
1091
+ }
1092
+ };
1093
+ const toggleTool = (name, on) => setForm((current) => ({
1094
+ ...current,
1095
+ tools: on ? [.../* @__PURE__ */ new Set([...current.tools, name])] : current.tools.filter((tool) => tool !== name)
1096
+ }));
1097
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", "data-testid": "mcp-tokens", children: [
1098
+ created && /* @__PURE__ */ jsxs(
1099
+ "div",
1100
+ {
1101
+ className: "rounded-md border border-status-warning-border bg-status-warning-bg-subtle p-3 space-y-2",
1102
+ "data-testid": "mcp-token-created",
1103
+ children: [
1104
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-status-warning-text", children: tr(
1105
+ "fluidgrids.mcp.tokens.once",
1106
+ "Copy the key and secret now: the secret is not shown again. A client exchanges them for an app token (authenticateAPIKey) and a workspace token (issueWorkspaceToken)."
1107
+ ) }),
1108
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
1109
+ /* @__PURE__ */ jsxs(
1110
+ Button,
1111
+ {
1112
+ size: "sm",
1113
+ variant: "outline",
1114
+ onClick: () => void copy(
1115
+ created.apiKey,
1116
+ tr("fluidgrids.mcp.tokens.keyCopied", "Key copied")
1117
+ ),
1118
+ children: [
1119
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:content-copy", width: 14 }),
1120
+ " ",
1121
+ tr("fluidgrids.mcp.tokens.copyKey", "Copy key")
1122
+ ]
1123
+ }
1124
+ ),
1125
+ /* @__PURE__ */ jsxs(
1126
+ Button,
1127
+ {
1128
+ size: "sm",
1129
+ variant: "outline",
1130
+ onClick: () => void copy(
1131
+ created.apiSecret,
1132
+ tr("fluidgrids.mcp.tokens.secretCopied", "Secret copied")
1133
+ ),
1134
+ children: [
1135
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:content-copy", width: 14 }),
1136
+ " ",
1137
+ tr("fluidgrids.mcp.tokens.copySecret", "Copy secret")
1138
+ ]
1139
+ }
1140
+ ),
1141
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: () => setCreated(null), children: tr("fluidgrids.mcp.tokens.done", "Done") })
1142
+ ] })
1143
+ ]
1144
+ }
1145
+ ),
1146
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-2 sm:grid-cols-3", children: [
1147
+ /* @__PURE__ */ jsx(
1148
+ Input,
1149
+ {
1150
+ "aria-label": tr("fluidgrids.mcp.tokens.name", "Token name"),
1151
+ placeholder: tr(
1152
+ "fluidgrids.mcp.tokens.namePlaceholder",
1153
+ "e.g. Claude Desktop"
1154
+ ),
1155
+ value: form.name,
1156
+ onChange: (e) => setForm({ ...form, name: e.target.value }),
1157
+ "data-testid": "mcp-token-name"
1158
+ }
1159
+ ),
1160
+ /* @__PURE__ */ jsxs(
1161
+ "select",
1162
+ {
1163
+ "aria-label": tr("fluidgrids.mcp.tokens.version", "Tool set version"),
1164
+ className: FIELD,
1165
+ value: form.pinVersion ?? "",
1166
+ onChange: (e) => setForm({
1167
+ ...form,
1168
+ pinVersion: e.target.value ? Number(e.target.value) : null
1169
+ }),
1170
+ children: [
1171
+ /* @__PURE__ */ jsx("option", { value: "", children: tr("fluidgrids.mcp.tokens.latest", "Newest version") }),
1172
+ versions.map((version) => /* @__PURE__ */ jsx("option", { value: version, children: tr("fluidgrids.mcp.tokens.pinned", "Pinned to v{{version}}", {
1173
+ version
1174
+ }) }, version))
1175
+ ]
1176
+ }
1177
+ ),
1178
+ /* @__PURE__ */ jsx(
1179
+ Input,
1180
+ {
1181
+ type: "number",
1182
+ min: 1,
1183
+ max: MCP_TOKEN_MAX_DAYS,
1184
+ "aria-label": tr("fluidgrids.mcp.tokens.days", "Expires after (days)"),
1185
+ value: form.expiresInDays,
1186
+ onChange: (e) => setForm({ ...form, expiresInDays: Number(e.target.value) })
1187
+ }
1188
+ )
1189
+ ] }),
1190
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-3", children: [
1191
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1192
+ /* @__PURE__ */ jsx(
1193
+ Checkbox,
1194
+ {
1195
+ id: "mcp-token-all",
1196
+ checked: form.allTools,
1197
+ onCheckedChange: (on) => setForm({ ...form, allTools: on === true })
1198
+ }
1199
+ ),
1200
+ /* @__PURE__ */ jsx(Label, { htmlFor: "mcp-token-all", className: "text-sm", children: tr("fluidgrids.mcp.tokens.allTools", "All tools") })
1201
+ ] }),
1202
+ !form.allTools && toolNames.map((name) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1203
+ /* @__PURE__ */ jsx(
1204
+ Checkbox,
1205
+ {
1206
+ id: `mcp-token-tool-${name}`,
1207
+ checked: form.tools.includes(name),
1208
+ onCheckedChange: (on) => toggleTool(name, on === true)
1209
+ }
1210
+ ),
1211
+ /* @__PURE__ */ jsx(
1212
+ Label,
1213
+ {
1214
+ htmlFor: `mcp-token-tool-${name}`,
1215
+ className: "text-sm font-mono",
1216
+ children: name
1217
+ }
1218
+ )
1219
+ ] }, name))
1220
+ ] }),
1221
+ /* @__PURE__ */ jsx(
1222
+ Button,
1223
+ {
1224
+ size: "sm",
1225
+ onClick: () => void submit(),
1226
+ disabled: busy,
1227
+ "data-testid": "mcp-token-create",
1228
+ children: tr("fluidgrids.mcp.tokens.create", "Create token")
1229
+ }
1230
+ ),
1231
+ /* @__PURE__ */ jsxs("ul", { className: "divide-y divide-border-secondary", children: [
1232
+ tokens.keys.map((key) => /* @__PURE__ */ jsxs(
1233
+ "li",
1234
+ {
1235
+ className: "flex flex-wrap items-center justify-between gap-2 py-2",
1236
+ children: [
1237
+ /* @__PURE__ */ jsxs("div", { children: [
1238
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-text-primary", children: [
1239
+ key.name,
1240
+ " ",
1241
+ /* @__PURE__ */ jsxs("span", { className: "font-mono text-xs text-text-secondary", children: [
1242
+ key.prefix,
1243
+ "\u2026"
1244
+ ] })
1245
+ ] }),
1246
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-text-secondary", children: [
1247
+ describeScopes(key.scopes),
1248
+ key.expiresAt ? ` \xB7 expires ${new Date(key.expiresAt).toLocaleDateString()}` : "",
1249
+ key.lastUsedAt ? ` \xB7 last used ${new Date(key.lastUsedAt).toLocaleString()}` : ""
1250
+ ] })
1251
+ ] }),
1252
+ /* @__PURE__ */ jsx(
1253
+ Button,
1254
+ {
1255
+ size: "sm",
1256
+ variant: "outline",
1257
+ onClick: () => void attempt(
1258
+ () => tokens.revoke(key.id),
1259
+ tr("fluidgrids.mcp.tokens.revoked", "Token revoked")
1260
+ ),
1261
+ children: tr("fluidgrids.mcp.tokens.revoke", "Revoke")
1262
+ }
1263
+ )
1264
+ ]
1265
+ },
1266
+ key.id
1267
+ )),
1268
+ !tokens.loading && tokens.keys.length === 0 && /* @__PURE__ */ jsx("li", { className: "py-2 text-sm text-text-secondary", children: tr("fluidgrids.mcp.tokens.empty", "No MCP tokens yet.") })
1269
+ ] })
1270
+ ] });
1271
+ }
1272
+ function CallRow({
1273
+ call,
1274
+ onDecide
1275
+ }) {
1276
+ const tr = useTr();
1277
+ return /* @__PURE__ */ jsxs(
1278
+ "tr",
1279
+ {
1280
+ className: "border-t border-border-secondary align-top",
1281
+ "data-testid": "mcp-call-row",
1282
+ children: [
1283
+ /* @__PURE__ */ jsx("td", { className: "py-2 pr-3 text-xs text-text-secondary whitespace-nowrap", children: new Date(call.createdAt).toLocaleString() }),
1284
+ /* @__PURE__ */ jsxs("td", { className: "py-2 pr-3 font-mono text-xs text-text-primary", children: [
1285
+ call.toolName,
1286
+ " ",
1287
+ /* @__PURE__ */ jsxs("span", { className: "text-text-secondary", children: [
1288
+ "v",
1289
+ call.toolSetVersion
1290
+ ] })
1291
+ ] }),
1292
+ /* @__PURE__ */ jsx("td", { className: "py-2 pr-3", children: /* @__PURE__ */ jsx(ToneBadge, { tone: callTone(call.status), children: call.status }) }),
1293
+ /* @__PURE__ */ jsxs("td", { className: "py-2 pr-3 text-xs text-text-secondary", children: [
1294
+ call.decision,
1295
+ call.policyName ? ` \xB7 ${call.policyName}` : "",
1296
+ call.errorMessage ? /* @__PURE__ */ jsx("div", { className: "text-status-error-text", children: call.errorMessage }) : null
1297
+ ] }),
1298
+ /* @__PURE__ */ jsxs("td", { className: "py-2 pr-3 text-xs text-text-secondary", children: [
1299
+ call.runId ? /* @__PURE__ */ jsx("span", { className: "font-mono", children: call.runId.slice(0, 8) }) : "\u2014",
1300
+ typeof call.latencyMs === "number" ? ` \xB7 ${call.latencyMs} ms` : ""
1301
+ ] }),
1302
+ /* @__PURE__ */ jsx("td", { className: "py-2 text-xs", children: canDecide(call) ? /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
1303
+ /* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => onDecide(call.id, true), children: tr("fluidgrids.mcp.calls.approve", "Approve") }),
1304
+ /* @__PURE__ */ jsx(
1305
+ Button,
1306
+ {
1307
+ size: "sm",
1308
+ variant: "outline",
1309
+ onClick: () => onDecide(call.id, false),
1310
+ children: tr("fluidgrids.mcp.calls.decline", "Decline")
1311
+ }
1312
+ )
1313
+ ] }) : /* @__PURE__ */ jsx(
1314
+ "span",
1315
+ {
1316
+ className: "font-mono text-text-secondary",
1317
+ title: call.argsSha256,
1318
+ children: call.argsSha256.slice(0, 10)
1319
+ }
1320
+ ) })
1321
+ ]
1322
+ }
1323
+ );
1324
+ }
1325
+ function CallLog() {
1326
+ const tr = useTr();
1327
+ const log = useMcpToolCalls();
1328
+ const decide = (id, approve) => void attempt(
1329
+ () => log.decide(id, approve),
1330
+ approve ? tr("fluidgrids.mcp.calls.approved", "Call approved") : tr("fluidgrids.mcp.calls.declined", "Call declined")
1331
+ );
1332
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", "data-testid": "mcp-call-log", children: [
1333
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1334
+ /* @__PURE__ */ jsxs(
1335
+ "select",
1336
+ {
1337
+ "aria-label": tr("fluidgrids.mcp.calls.filter", "Status"),
1338
+ className: FIELD,
1339
+ value: log.statusFilter ?? "",
1340
+ onChange: (e) => log.setStatusFilter(e.target.value || null),
1341
+ children: [
1342
+ /* @__PURE__ */ jsx("option", { value: "", children: tr("fluidgrids.mcp.calls.all", "All calls") }),
1343
+ ["PENDING_APPROVAL", "SUCCEEDED", "RUNNING", "FAILED", "DENIED"].map(
1344
+ (status) => /* @__PURE__ */ jsx("option", { value: status, children: status }, status)
1345
+ )
1346
+ ]
1347
+ }
1348
+ ),
1349
+ /* @__PURE__ */ jsx(
1350
+ Button,
1351
+ {
1352
+ size: "sm",
1353
+ variant: "ghost",
1354
+ onClick: () => void attempt(() => log.reload()),
1355
+ children: /* @__PURE__ */ jsx(Icon, { icon: "mdi:refresh", width: 16 })
1356
+ }
1357
+ )
1358
+ ] }),
1359
+ /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-left text-sm", children: [
1360
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "text-xs text-text-secondary", children: [
1361
+ /* @__PURE__ */ jsx("th", { className: "pb-1 pr-3 font-medium", children: tr("fluidgrids.mcp.calls.when", "When") }),
1362
+ /* @__PURE__ */ jsx("th", { className: "pb-1 pr-3 font-medium", children: tr("fluidgrids.mcp.calls.tool", "Tool") }),
1363
+ /* @__PURE__ */ jsx("th", { className: "pb-1 pr-3 font-medium", children: tr("fluidgrids.mcp.calls.status", "Status") }),
1364
+ /* @__PURE__ */ jsx("th", { className: "pb-1 pr-3 font-medium", children: tr("fluidgrids.mcp.calls.decision", "Decision") }),
1365
+ /* @__PURE__ */ jsx("th", { className: "pb-1 pr-3 font-medium", children: tr("fluidgrids.mcp.calls.run", "Run") }),
1366
+ /* @__PURE__ */ jsx("th", { className: "pb-1 font-medium", children: tr("fluidgrids.mcp.calls.args", "Arguments") })
1367
+ ] }) }),
1368
+ /* @__PURE__ */ jsx("tbody", { children: log.calls.map((call) => /* @__PURE__ */ jsx(CallRow, { call, onDecide: decide }, call.id)) })
1369
+ ] }) }),
1370
+ !log.loading && log.calls.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-sm text-text-secondary", children: tr("fluidgrids.mcp.calls.empty", "No calls yet.") }),
1371
+ log.hasMore && /* @__PURE__ */ jsx(
1372
+ Button,
1373
+ {
1374
+ size: "sm",
1375
+ variant: "outline",
1376
+ onClick: () => void attempt(() => log.loadMore()),
1377
+ children: tr("fluidgrids.mcp.calls.more", "Load more")
1378
+ }
1379
+ )
1380
+ ] });
1381
+ }
1382
+ function McpGatewayTab({
1383
+ workflowKey
1384
+ }) {
1385
+ const tr = useTr();
1386
+ const mcp = useMcpGateway();
1387
+ const [note, setNote] = useState("");
1388
+ const [busy, setBusy] = useState(false);
1389
+ const thisWorkflowTool = workflowKey ? mcp.tools.find(
1390
+ (tool) => tool.kind === "WORKFLOW" && tool.workflowKey === workflowKey
1391
+ ) : void 0;
1392
+ const latest = mcp.toolSets.find((set) => set.status === "PUBLISHED");
1393
+ const servedVersions = useMemo(
1394
+ () => mcp.toolSets.filter((set) => set.status === "PUBLISHED").map((set) => set.version),
1395
+ [mcp.toolSets]
1396
+ );
1397
+ const url = mcp.endpoint ? endpointUrl(getFluidGridsConfig().apiGatewayUrl, mcp.endpoint.path) : "";
1398
+ const toggleThisWorkflow = (on) => {
1399
+ if (!workflowKey) return;
1400
+ void attempt(
1401
+ () => thisWorkflowTool ? mcp.setEnabled(thisWorkflowTool.id, on) : mcp.exposeWorkflow({ workflowKey })
1402
+ );
1403
+ };
1404
+ const publish = async () => {
1405
+ setBusy(true);
1406
+ try {
1407
+ const outcome = await mcp.publish(note.trim());
1408
+ setNote("");
1409
+ toast.success(
1410
+ outcome.created ? tr(
1411
+ "fluidgrids.mcp.publish.created",
1412
+ "Published version {{version}}",
1413
+ { version: outcome.toolSet.version }
1414
+ ) : tr(
1415
+ "fluidgrids.mcp.publish.unchanged",
1416
+ "Nothing changed since version {{version}}",
1417
+ {
1418
+ version: outcome.toolSet.version
1419
+ }
1420
+ )
1421
+ );
1422
+ for (const skip of outcome.skipped)
1423
+ toast.warning(`${skip.toolName}: ${skip.reason}`);
1424
+ } catch (err) {
1425
+ toast.error(errorMessage(err));
1426
+ } finally {
1427
+ setBusy(false);
1428
+ }
1429
+ };
1430
+ if (mcp.error && !mcp.loading && mcp.tools.length === 0) {
1431
+ return /* @__PURE__ */ jsxs("div", { className: CARD, children: [
1432
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-status-error-text", children: errorMessage(mcp.error) }),
1433
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "outline", onClick: () => void mcp.reload(), children: tr("fluidgrids.mcp.retry", "Try again") })
1434
+ ] });
1435
+ }
1436
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", "data-testid": "mcp-gateway-tab", children: [
1437
+ /* @__PURE__ */ jsxs("section", { className: CARD, children: [
1438
+ /* @__PURE__ */ jsx("h3", { className: "text-base font-semibold text-text-primary", children: tr("fluidgrids.mcp.endpoint.title", "MCP endpoint") }),
1439
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-text-secondary", children: tr(
1440
+ "fluidgrids.mcp.endpoint.help",
1441
+ "External agents call the published tools here over Streamable HTTP with an MCP token. Every call passes your action policies and is recorded below."
1442
+ ) }),
1443
+ url && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
1444
+ /* @__PURE__ */ jsxs(
1445
+ "code",
1446
+ {
1447
+ className: "rounded bg-background-secondary px-2 py-1 text-xs text-text-primary break-all",
1448
+ "data-testid": "mcp-endpoint-url",
1449
+ children: [
1450
+ "POST ",
1451
+ url
1452
+ ]
1453
+ }
1454
+ ),
1455
+ /* @__PURE__ */ jsx(
1456
+ Button,
1457
+ {
1458
+ size: "sm",
1459
+ variant: "outline",
1460
+ onClick: () => void copy(
1461
+ url,
1462
+ tr("fluidgrids.mcp.endpoint.copied", "Endpoint copied")
1463
+ ),
1464
+ children: /* @__PURE__ */ jsx(Icon, { icon: "mdi:content-copy", width: 14 })
1465
+ }
1466
+ ),
1467
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-text-secondary", children: latest ? tr(
1468
+ "fluidgrids.mcp.endpoint.version",
1469
+ "Serving version {{version}}",
1470
+ { version: latest.version }
1471
+ ) : tr("fluidgrids.mcp.endpoint.none", "Nothing published yet") })
1472
+ ] }),
1473
+ workflowKey && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-t border-border-secondary pt-3", children: [
1474
+ /* @__PURE__ */ jsx(
1475
+ Switch,
1476
+ {
1477
+ id: "mcp-this-workflow",
1478
+ checked: thisWorkflowTool?.enabled ?? false,
1479
+ onCheckedChange: toggleThisWorkflow,
1480
+ "data-testid": "mcp-expose-this-workflow"
1481
+ }
1482
+ ),
1483
+ /* @__PURE__ */ jsxs(Label, { htmlFor: "mcp-this-workflow", className: "text-sm", children: [
1484
+ tr(
1485
+ "fluidgrids.mcp.thisWorkflow",
1486
+ "Expose this workflow as an MCP tool"
1487
+ ),
1488
+ thisWorkflowTool ? /* @__PURE__ */ jsx("span", { className: "ml-1 font-mono text-text-secondary", children: thisWorkflowTool.toolName }) : null
1489
+ ] })
1490
+ ] })
1491
+ ] }),
1492
+ /* @__PURE__ */ jsxs("section", { className: CARD, children: [
1493
+ /* @__PURE__ */ jsx("h3", { className: "text-base font-semibold text-text-primary", children: tr("fluidgrids.mcp.tools.title", "Tools") }),
1494
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-text-secondary", children: tr(
1495
+ "fluidgrids.mcp.tools.help",
1496
+ `A workflow tool runs the workflow's active version; its arguments are the fields its "When Executed by Another Workflow" trigger declares. Changes reach clients when you publish.`
1497
+ ) }),
1498
+ /* @__PURE__ */ jsxs(
1499
+ "ul",
1500
+ {
1501
+ className: "divide-y divide-border-secondary",
1502
+ "data-testid": "mcp-tools",
1503
+ children: [
1504
+ mcp.tools.map((tool) => /* @__PURE__ */ jsxs(
1505
+ "li",
1506
+ {
1507
+ className: "flex flex-wrap items-center justify-between gap-2 py-2",
1508
+ children: [
1509
+ /* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
1510
+ /* @__PURE__ */ jsx("p", { className: "font-mono text-sm text-text-primary", children: tool.toolName }),
1511
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-text-secondary", children: tool.kind === "WORKFLOW" ? tr("fluidgrids.mcp.tools.workflow", "Workflow {{key}}", {
1512
+ key: tool.workflowKey ?? ""
1513
+ }) : `${tool.nodeType ?? ""} \xB7 ${tool.operation ?? ""}` })
1514
+ ] }),
1515
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1516
+ /* @__PURE__ */ jsx(ToneBadge, { tone: tool.readOnly ? "success" : "warning", children: tool.readOnly ? tr("fluidgrids.mcp.tools.readOnly", "Read only") : tr("fluidgrids.mcp.tools.write", "Changes data") }),
1517
+ /* @__PURE__ */ jsx(
1518
+ Switch,
1519
+ {
1520
+ checked: tool.enabled,
1521
+ "aria-label": tr("fluidgrids.mcp.tools.enabled", "Enabled"),
1522
+ onCheckedChange: (on) => void attempt(() => mcp.setEnabled(tool.id, on))
1523
+ }
1524
+ ),
1525
+ /* @__PURE__ */ jsx(
1526
+ Button,
1527
+ {
1528
+ size: "sm",
1529
+ variant: "ghost",
1530
+ "aria-label": tr("fluidgrids.mcp.tools.remove", "Remove"),
1531
+ onClick: () => void attempt(
1532
+ () => mcp.removeTool(tool.id),
1533
+ tr("fluidgrids.mcp.tools.removed", "Tool removed")
1534
+ ),
1535
+ children: /* @__PURE__ */ jsx(Icon, { icon: "mdi:delete-outline", width: 16 })
1536
+ }
1537
+ )
1538
+ ] })
1539
+ ]
1540
+ },
1541
+ tool.id
1542
+ )),
1543
+ !mcp.loading && mcp.tools.length === 0 && /* @__PURE__ */ jsx("li", { className: "py-2 text-sm text-text-secondary", children: tr("fluidgrids.mcp.tools.empty", "No tools exposed yet.") })
1544
+ ]
1545
+ }
1546
+ ),
1547
+ /* @__PURE__ */ jsx(
1548
+ ConnectorToolForm,
1549
+ {
1550
+ onSave: (input) => attempt(
1551
+ () => mcp.exposeConnector(input),
1552
+ tr("fluidgrids.mcp.connector.added", "Tool added")
1553
+ )
1554
+ }
1555
+ )
1556
+ ] }),
1557
+ /* @__PURE__ */ jsxs("section", { className: CARD, children: [
1558
+ /* @__PURE__ */ jsx("h3", { className: "text-base font-semibold text-text-primary", children: tr("fluidgrids.mcp.versions.title", "Published versions") }),
1559
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
1560
+ /* @__PURE__ */ jsx(
1561
+ Input,
1562
+ {
1563
+ className: "max-w-sm",
1564
+ "aria-label": tr("fluidgrids.mcp.versions.note", "Release note"),
1565
+ placeholder: tr(
1566
+ "fluidgrids.mcp.versions.notePlaceholder",
1567
+ "What changed (optional)"
1568
+ ),
1569
+ value: note,
1570
+ onChange: (e) => setNote(e.target.value)
1571
+ }
1572
+ ),
1573
+ /* @__PURE__ */ jsx(
1574
+ Button,
1575
+ {
1576
+ size: "sm",
1577
+ onClick: () => void publish(),
1578
+ disabled: busy,
1579
+ "data-testid": "mcp-publish",
1580
+ children: tr("fluidgrids.mcp.versions.publish", "Publish tool set")
1581
+ }
1582
+ )
1583
+ ] }),
1584
+ /* @__PURE__ */ jsx(
1585
+ "ul",
1586
+ {
1587
+ className: "divide-y divide-border-secondary",
1588
+ "data-testid": "mcp-versions",
1589
+ children: mcp.toolSets.map((set) => /* @__PURE__ */ jsxs(
1590
+ "li",
1591
+ {
1592
+ className: "flex flex-wrap items-center justify-between gap-2 py-2",
1593
+ children: [
1594
+ /* @__PURE__ */ jsxs("div", { children: [
1595
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-text-primary", children: [
1596
+ "v",
1597
+ set.version,
1598
+ " \xB7",
1599
+ " ",
1600
+ set.tools.map((tool) => tool.name).join(", ")
1601
+ ] }),
1602
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-text-secondary", children: [
1603
+ new Date(set.publishedAt).toLocaleString(),
1604
+ set.note ? ` \xB7 ${set.note}` : ""
1605
+ ] })
1606
+ ] }),
1607
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1608
+ /* @__PURE__ */ jsx(
1609
+ ToneBadge,
1610
+ {
1611
+ tone: set.status === "PUBLISHED" ? "success" : "neutral",
1612
+ children: set.status
1613
+ }
1614
+ ),
1615
+ set.status === "PUBLISHED" && /* @__PURE__ */ jsx(
1616
+ Button,
1617
+ {
1618
+ size: "sm",
1619
+ variant: "outline",
1620
+ onClick: () => void attempt(
1621
+ () => mcp.deprecate(set.version),
1622
+ tr(
1623
+ "fluidgrids.mcp.versions.deprecated",
1624
+ "Version deprecated"
1625
+ )
1626
+ ),
1627
+ children: tr("fluidgrids.mcp.versions.deprecate", "Deprecate")
1628
+ }
1629
+ )
1630
+ ] })
1631
+ ]
1632
+ },
1633
+ set.id
1634
+ ))
1635
+ }
1636
+ )
1637
+ ] }),
1638
+ /* @__PURE__ */ jsxs("section", { className: CARD, children: [
1639
+ /* @__PURE__ */ jsx("h3", { className: "text-base font-semibold text-text-primary", children: tr("fluidgrids.mcp.tokens.title", "MCP tokens") }),
1640
+ /* @__PURE__ */ jsx(
1641
+ TokenSection,
1642
+ {
1643
+ toolNames: latest?.tools.map((tool) => tool.name) ?? [],
1644
+ versions: servedVersions
1645
+ }
1646
+ )
1647
+ ] }),
1648
+ /* @__PURE__ */ jsxs("section", { className: CARD, children: [
1649
+ /* @__PURE__ */ jsx("h3", { className: "text-base font-semibold text-text-primary", children: tr("fluidgrids.mcp.calls.title", "Call log") }),
1650
+ /* @__PURE__ */ jsx(CallLog, {})
1651
+ ] })
1652
+ ] });
1653
+ }
1654
+
373
1655
  // src/modules/settings/utils/workflow-settings-persistence.ts
374
1656
  var DATA_SAVING_KEYS = [
375
1657
  "saveSuccessExecutions",
@@ -380,11 +1662,16 @@ var RETENTION_KEYS = [
380
1662
  "maxAgeDays",
381
1663
  "maxRuns"
382
1664
  ];
1665
+ var DATA_PRIVACY_KEYS = [
1666
+ "doNotStoreRunData",
1667
+ "redactFields"
1668
+ ];
383
1669
  function mergeWorkflowExecutionSettings(existing, next) {
384
1670
  const merged = { ...existing, ...next };
385
1671
  for (const [key, editable] of [
386
1672
  ["dataSaving", DATA_SAVING_KEYS],
387
- ["retention", RETENTION_KEYS]
1673
+ ["retention", RETENTION_KEYS],
1674
+ ["dataPrivacy", DATA_PRIVACY_KEYS]
388
1675
  ]) {
389
1676
  const nested = mergeNested(existing[key], next[key], editable);
390
1677
  if (nested) merged[key] = nested;
@@ -583,7 +1870,15 @@ function ExecutionTab({ workflowKey }) {
583
1870
  return config ?? {};
584
1871
  }, [workflow?.definitionJson]);
585
1872
  const [form, setForm] = useState(persistedConfig);
586
- useEffect(() => setForm(persistedConfig), [persistedConfig]);
1873
+ const [redactFieldsText, setRedactFieldsText] = useState(
1874
+ () => formatRedactFields(persistedConfig.dataPrivacy?.redactFields)
1875
+ );
1876
+ useEffect(() => {
1877
+ setForm(persistedConfig);
1878
+ setRedactFieldsText(
1879
+ formatRedactFields(persistedConfig.dataPrivacy?.redactFields)
1880
+ );
1881
+ }, [persistedConfig]);
587
1882
  const runData = useRunDataSettings();
588
1883
  const selectedErrorWorkflowKey = form.errorWorkflowKey;
589
1884
  const { data: errorWorkflowData } = useWorkflowDefinitionQuery({
@@ -970,6 +2265,79 @@ function ExecutionTab({ workflowKey }) {
970
2265
  }
971
2266
  )
972
2267
  }
2268
+ ),
2269
+ /* @__PURE__ */ jsx(
2270
+ SettingRow,
2271
+ {
2272
+ label: tr(
2273
+ "fluidgrids.settings.execution.doNotStoreRunDataLabel",
2274
+ "Do not store run data"
2275
+ ),
2276
+ description: tr(
2277
+ "fluidgrids.settings.execution.doNotStoreRunDataDescription",
2278
+ "Keep only each run's status, timings, node statuses and errors. Trigger data and node inputs and outputs are deleted when the run finishes, and log lines keep their message only. Finished runs cannot be retried, replayed or reused."
2279
+ ),
2280
+ children: /* @__PURE__ */ jsx(
2281
+ Switch,
2282
+ {
2283
+ "data-testid": "fluidgrids-do-not-store-run-data",
2284
+ "aria-label": tr(
2285
+ "fluidgrids.settings.execution.doNotStoreRunDataLabel",
2286
+ "Do not store run data"
2287
+ ),
2288
+ checked: form.dataPrivacy?.doNotStoreRunData === true,
2289
+ onCheckedChange: (checked) => setForm((f) => ({
2290
+ ...f,
2291
+ dataPrivacy: compactOverrides({
2292
+ ...f.dataPrivacy,
2293
+ doNotStoreRunData: checked ? true : void 0
2294
+ })
2295
+ })),
2296
+ disabled: locked
2297
+ }
2298
+ )
2299
+ }
2300
+ ),
2301
+ /* @__PURE__ */ jsx(
2302
+ SettingRow,
2303
+ {
2304
+ label: tr(
2305
+ "fluidgrids.settings.execution.redactFieldsLabel",
2306
+ "Redacted fields"
2307
+ ),
2308
+ description: tr(
2309
+ "fluidgrids.settings.execution.redactFieldsDescription",
2310
+ "Comma-separated. A name (ssn) is hidden wherever it appears; a dotted path (customer.email) from the top of the data. Replaced with [REDACTED] in run logs and in stored run data, together with secret-looking keys such as tokens and passwords."
2311
+ ),
2312
+ children: /* @__PURE__ */ jsx(
2313
+ Input,
2314
+ {
2315
+ "data-testid": "fluidgrids-redact-fields",
2316
+ "aria-label": tr(
2317
+ "fluidgrids.settings.execution.redactFieldsLabel",
2318
+ "Redacted fields"
2319
+ ),
2320
+ value: redactFieldsText,
2321
+ placeholder: tr(
2322
+ "fluidgrids.settings.execution.redactFieldsPlaceholder",
2323
+ "ssn, customer.email"
2324
+ ),
2325
+ onChange: (e) => {
2326
+ const text = e.target.value;
2327
+ setRedactFieldsText(text);
2328
+ setForm((f) => ({
2329
+ ...f,
2330
+ dataPrivacy: compactOverrides({
2331
+ ...f.dataPrivacy,
2332
+ redactFields: parseRedactFields(text)
2333
+ })
2334
+ }));
2335
+ },
2336
+ disabled: locked,
2337
+ className: "w-[260px]"
2338
+ }
2339
+ )
2340
+ }
973
2341
  )
974
2342
  ] }),
975
2343
  !canEdit && /* @__PURE__ */ jsx("p", { className: "mt-4 text-xs text-text-secondary", role: "note", children: tr(
@@ -1253,6 +2621,30 @@ function SettingsPage({
1253
2621
  ]
1254
2622
  }
1255
2623
  ),
2624
+ /* @__PURE__ */ jsxs(
2625
+ TabsTrigger,
2626
+ {
2627
+ value: "agents",
2628
+ className: "gap-1.5",
2629
+ "data-tour": "fluidgrids-settings-agents-tab",
2630
+ children: [
2631
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:robot-outline", width: 16 }),
2632
+ tr("fluidgrids.settings.page.agentsTab", "Agents")
2633
+ ]
2634
+ }
2635
+ ),
2636
+ /* @__PURE__ */ jsxs(
2637
+ TabsTrigger,
2638
+ {
2639
+ value: "mcp",
2640
+ className: "gap-1.5",
2641
+ "data-tour": "fluidgrids-settings-mcp-tab",
2642
+ children: [
2643
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:connection", width: 16 }),
2644
+ tr("fluidgrids.settings.page.mcpTab", "MCP")
2645
+ ]
2646
+ }
2647
+ ),
1256
2648
  /* @__PURE__ */ jsxs(TabsTrigger, { value: "notifications", className: "gap-1.5", children: [
1257
2649
  /* @__PURE__ */ jsx(Icon, { icon: "mdi:bell-outline", width: 16 }),
1258
2650
  tr("fluidgrids.settings.page.notificationsTab", "Notifications")
@@ -1280,6 +2672,8 @@ function SettingsPage({
1280
2672
  /* @__PURE__ */ jsx(TabsContent, { value: "execution", children: /* @__PURE__ */ jsx(ExecutionTab, { workflowKey }) }),
1281
2673
  /* @__PURE__ */ jsx(TabsContent, { value: "run-data", children: /* @__PURE__ */ jsx(RunDataTab, {}) }),
1282
2674
  /* @__PURE__ */ jsx(TabsContent, { value: "policies", children: /* @__PURE__ */ jsx(ActionPoliciesTab, {}) }),
2675
+ /* @__PURE__ */ jsx(TabsContent, { value: "agents", children: /* @__PURE__ */ jsx(AgentsTab, {}) }),
2676
+ /* @__PURE__ */ jsx(TabsContent, { value: "mcp", children: /* @__PURE__ */ jsx(McpGatewayTab, { workflowKey }) }),
1283
2677
  /* @__PURE__ */ jsx(TabsContent, { value: "notifications", children: /* @__PURE__ */ jsx(NotificationsTab, {}) }),
1284
2678
  logStreamsEnabled ? /* @__PURE__ */ jsx(TabsContent, { value: "log-streaming", children: /* @__PURE__ */ jsx(LogStreamingTab, {}) }) : null,
1285
2679
  /* @__PURE__ */ jsx(TabsContent, { value: "api", children: /* @__PURE__ */ jsx(ApiTab, {}) })