@burdenoff/fluidgrids-fe-sdk 2026.1028.6 → 2026.1028.8

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