feat(ee): mcp (#1976)

* feat: MCP
* sync
* sync
This commit is contained in:
Philip Okugbe
2026-03-01 18:37:39 +00:00
committed by GitHub
parent 2309d1434b
commit 60848ea903
49 changed files with 781 additions and 154 deletions
+1 -1
View File
@@ -55,7 +55,7 @@
"semver": "^7.7.3",
"socket.io-client": "^4.8.3",
"tiptap-extension-global-drag-handle": "^0.1.18",
"zod": "^3.25.76"
"zod": "^4.3.6"
},
"devDependencies": {
"@eslint/js": "^9.16.0",
@@ -607,6 +607,25 @@
"Generative AI (Ask AI)": "Generative AI (Ask AI)",
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.",
"Toggle generative AI": "Toggle generative AI",
"Enterprise feature": "Enterprise feature",
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
"AI & MCP": "AI & MCP",
"AI": "AI",
"MCP": "MCP",
"Model Context Protocol (MCP)": "Model Context Protocol (MCP)",
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
"MCP documentation": "MCP documentation",
"MCP Server URL": "MCP Server URL",
"Use your API key for authentication. You can manage API keys in your account settings.": "Use your API key for authentication. You can manage API keys in your account settings.",
"Supported tools": "Supported tools",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "Your workspace has MCP enabled. Use your API key to connect AI assistants.",
"MCP server URL:": "MCP server URL:",
"Learn more": "Learn more",
"View the": "View the",
"for usage details.": "for usage details.",
"for setup instructions.": "for setup instructions.",
"API documentation": "API documentation",
"Sources": "Sources",
"AI Answers not available for attachments": "AI Answers not available for attachments",
"No answer available": "No answer available",
+1
View File
@@ -103,6 +103,7 @@ export default function App() {
<Route path={"sharing"} element={<Shares />} />
<Route path={"security"} element={<Security />} />
<Route path={"ai"} element={<AiSettings />} />
<Route path={"ai/mcp"} element={<AiSettings />} />
<Route path={"audit"} element={<AuditLogs />} />
{!isCloud() && <Route path={"license"} element={<License />} />}
{isCloud() && <Route path={"billing"} element={<Billing />} />}
@@ -113,7 +113,7 @@ const groupedData: DataGroup[] = [
showDisabledInNonEE: true,
},
{
label: "AI settings",
label: "AI",
icon: IconSparkles,
path: "/settings/ai",
isAdmin: true,
@@ -4,7 +4,7 @@ import { getAvatarUrl } from "@/lib/config.ts";
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
interface CustomAvatarProps {
avatarUrl: string;
avatarUrl?: string;
name: string;
color?: string;
size?: string | number;
@@ -0,0 +1,138 @@
import {
Anchor,
Group,
List,
Text,
Switch,
TextInput,
ActionIcon,
Tooltip,
Stack,
Alert,
} from "@mantine/core";
import { useAtom } from "jotai";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
import { notifications } from "@mantine/notifications";
import { useIsCloudEE } from "@/hooks/use-is-cloud-ee.tsx";
import { getAppUrl } from "@/lib/config.ts";
import { IconCheck, IconCopy, IconInfoCircle } from "@tabler/icons-react";
import { CopyButton } from "@/components/common/copy-button.tsx";
export default function McpSettings() {
const { t } = useTranslation();
const [workspace, setWorkspace] = useAtom(workspaceAtom);
const [checked, setChecked] = useState(workspace?.settings?.ai?.mcp);
const hasAccess = useIsCloudEE();
const mcpUrl = `${getAppUrl()}/api/mcp`;
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.currentTarget.checked;
try {
const updatedWorkspace = await updateWorkspace({ mcpEnabled: value });
setChecked(value);
setWorkspace(updatedWorkspace);
} catch (err) {
notifications.show({
message: err?.response?.data?.message,
color: "red",
});
}
};
return (
<Stack gap="lg">
{!hasAccess && (
<Alert
icon={<IconInfoCircle />}
title={t("Enterprise feature")}
color="blue"
>
{t(
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
)}
</Alert>
)}
<Group justify="space-between" wrap="nowrap" gap="xl">
<div>
<Text size="md">{t("Model Context Protocol (MCP)")}</Text>
<Text size="sm" c="dimmed">
{t(
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.",
)}{" "}
{t("View the")}{" "}
<Anchor
href="https://docmost.com/docs/user-guide/mcp"
target="_blank"
size="sm"
>
{t("MCP documentation")}
</Anchor>
.
</Text>
</div>
<Switch
defaultChecked={checked}
onChange={handleChange}
disabled={!hasAccess}
/>
</Group>
{checked && (
<div>
<Text size="sm" fw={500} mb={4}>
{t("MCP Server URL")}
</Text>
<Group gap="xs">
<TextInput
value={mcpUrl}
readOnly
style={{ flex: 1 }}
/>
<CopyButton value={mcpUrl} timeout={2000}>
{({ copied, copy }) => (
<Tooltip
label={copied ? t("Copied") : t("Copy")}
withArrow
position="right"
>
<ActionIcon
color={copied ? "teal" : "gray"}
variant="subtle"
onClick={copy}
>
{copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
</Group>
<Text size="sm" c="dimmed" mt="xs">
{t(
"Use your API key for authentication. You can manage API keys in your account settings.",
)}
</Text>
<div>
<Text size="sm" fw={500} mt="md" mb={4}>
{t("Supported tools")}
</Text>
<List size="sm" spacing={2}>
<List.Item><Text size="sm" c="dimmed" span>search_pages, get_page, create_page, update_page</Text></List.Item>
<List.Item><Text size="sm" c="dimmed" span>list_pages, list_child_pages, duplicate_page</Text></List.Item>
<List.Item><Text size="sm" c="dimmed" span>copy_page_to_space, move_page, move_page_to_space</Text></List.Item>
<List.Item><Text size="sm" c="dimmed" span>get_space, list_spaces, create_space, update_space</Text></List.Item>
<List.Item><Text size="sm" c="dimmed" span>get_comments, create_comment, update_comment</Text></List.Item>
<List.Item><Text size="sm" c="dimmed" span>search_attachments, list_workspace_members, get_current_user</Text></List.Item>
</List>
</div>
</div>
)}
</Stack>
);
}
+48 -17
View File
@@ -6,44 +6,75 @@ import useUserRole from "@/hooks/use-user-role.tsx";
import { useTranslation } from "react-i18next";
import EnableAiSearch from "@/ee/ai/components/enable-ai-search.tsx";
import EnableGenerativeAi from "@/ee/ai/components/enable-generative-ai.tsx";
import { Alert, Stack } from "@mantine/core";
import McpSettings from "@/ee/ai/components/mcp-settings.tsx";
import { Alert, Stack, Tabs } from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react";
import { useIsCloudEE } from "@/hooks/use-is-cloud-ee.tsx";
import { isCloud } from "@/lib/config.ts";
import { useLocation, useNavigate } from "react-router-dom";
export default function AiSettings() {
const { t } = useTranslation();
const { isAdmin } = useUserRole();
const hasAccess = useIsCloudEE();
const location = useLocation();
const navigate = useNavigate();
const activeTab = location.pathname.endsWith("/mcp") ? "mcp" : "ai";
if (!isAdmin) {
return null;
}
const handleTabChange = (value: string | null) => {
if (value === "mcp") {
navigate("/settings/ai/mcp");
} else {
navigate("/settings/ai");
}
};
return (
<>
<Helmet>
<title>AI - {getAppName()}</title>
</Helmet>
<SettingsTitle title={t("AI settings")} />
<SettingsTitle title={t("AI")} />
{!hasAccess && (
<Alert
icon={<IconInfoCircle />}
title={t("Enterprise feature")}
color="blue"
mb="lg"
>
{t(
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
<Tabs color="dark" value={activeTab} onChange={handleTabChange}>
<Tabs.List>
<Tabs.Tab fw={500} value="ai">
{t("AI")}
</Tabs.Tab>
<Tabs.Tab fw={500} value="mcp">
{t("MCP")}
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="ai" pt="md">
{!hasAccess && (
<Alert
icon={<IconInfoCircle />}
title={t("Enterprise feature")}
color="blue"
mb="lg"
>
{t(
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
)}
</Alert>
)}
</Alert>
)}
<Stack gap="md">
{!isCloud() && <EnableAiSearch />}
<EnableGenerativeAi />
</Stack>
<Stack gap="md">
{!isCloud() && <EnableAiSearch />}
<EnableGenerativeAi />
</Stack>
</Tabs.Panel>
<Tabs.Panel value="mcp" pt="md">
<McpSettings />
</Tabs.Panel>
</Tabs>
</>
);
}
@@ -1,8 +1,8 @@
import { lazy, Suspense, useState } from "react";
import { Modal, TextInput, Button, Group, Stack, Select } from "@mantine/core";
import { useForm } from "@mantine/form";
import { zodResolver } from "mantine-form-zod-resolver";
import { z } from "zod";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { z } from "zod/v4";
import { useTranslation } from "react-i18next";
import { useCreateApiKeyMutation } from "@/ee/api-key/queries/api-key-query";
import { IconCalendar } from "@tabler/icons-react";
@@ -36,7 +36,7 @@ export function CreateApiKeyModal({
const createApiKeyMutation = useCreateApiKeyMutation();
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
initialValues: {
name: "",
expiresAt: "",
@@ -1,7 +1,7 @@
import { Modal, TextInput, Button, Group, Stack } from "@mantine/core";
import { useForm } from "@mantine/form";
import { zodResolver } from "mantine-form-zod-resolver";
import { z } from "zod";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { z } from "zod/v4";
import { useTranslation } from "react-i18next";
import { useUpdateApiKeyMutation } from "@/ee/api-key/queries/api-key-query";
import { IApiKey } from "@/ee/api-key";
@@ -27,7 +27,7 @@ export function UpdateApiKeyModal({
const updateApiKeyMutation = useUpdateApiKeyMutation();
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
initialValues: {
name: "",
},
@@ -1,9 +1,9 @@
import React, { useState } from "react";
import { Button, Group, Space } from "@mantine/core";
import { Anchor, Alert, Button, Group, Space, Text } from "@mantine/core";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next";
import SettingsTitle from "@/components/settings/settings-title";
import { getAppName } from "@/lib/config";
import { getAppName, getAppUrl } from "@/lib/config";
import { ApiKeyTable } from "@/ee/api-key/components/api-key-table";
import { CreateApiKeyModal } from "@/ee/api-key/components/create-api-key-modal";
import { ApiKeyCreatedModal } from "@/ee/api-key/components/api-key-created-modal";
@@ -13,6 +13,8 @@ import Paginate from "@/components/common/paginate";
import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
import { useGetApiKeysQuery } from "@/ee/api-key/queries/api-key-query.ts";
import { IApiKey } from "@/ee/api-key";
import { useAtom } from "jotai";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
export default function UserApiKeys() {
const { t } = useTranslation();
@@ -23,6 +25,8 @@ export default function UserApiKeys() {
const [revokeModalOpened, setRevokeModalOpened] = useState(false);
const [selectedApiKey, setSelectedApiKey] = useState<IApiKey | null>(null);
const { data, isLoading } = useGetApiKeysQuery({ cursor });
const [workspace] = useAtom(workspaceAtom);
const mcpEnabled = workspace?.settings?.ai?.mcp === true;
const handleCreateSuccess = (response: IApiKey) => {
setCreatedApiKey(response);
@@ -48,6 +52,37 @@ export default function UserApiKeys() {
<SettingsTitle title={t("API keys")} />
<Text size="sm" c="dimmed" mb="md">
{t("View the")}{" "}
<Anchor href="https://docmost.com/api-docs" target="_blank" size="sm">
{t("API documentation")}
</Anchor>{" "}
{t("for usage details.")}
</Text>
{mcpEnabled && (
<Alert variant="light" color="blue" mb="md" p="sm">
<Text size="sm">
{t(
"Your workspace has MCP enabled. Use your API key to connect AI assistants.",
)}{" "}
<Anchor
href="https://docmost.com/docs/user-guide/mcp"
target="_blank"
size="sm"
>
{t("Learn more")}
</Anchor>
</Text>
<Text size="sm" mt={4}>
{t("MCP server URL:")}{" "}
<Text size="sm" fw={500} span ff="monospace">
{`${getAppUrl()}/api/mcp`}
</Text>
</Text>
</Alert>
)}
<Group justify="flex-end" mb="md">
<Button onClick={() => setCreateModalOpened(true)}>
{t("Create API Key")}
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { Button, Group, Space, Text } from "@mantine/core";
import { Anchor, Button, Group, Space, Text } from "@mantine/core";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next";
import SettingsTitle from "@/components/settings/settings-title";
@@ -54,8 +54,13 @@ export default function WorkspaceApiKeys() {
<SettingsTitle title={t("API management")} />
<Text size="md" c="dimmed" mb="md">
{t("Manage API keys for all users in the workspace")}
<Text size="sm" c="dimmed" mb="md">
{t("Manage API keys for all users in the workspace.")}{" "}
{t("View the")}{" "}
<Anchor href="https://docmost.com/api-docs" target="_blank" size="sm">
{t("API documentation")}
</Anchor>{" "}
{t("for usage details.")}
</Text>
<Group justify="flex-end" mb="md">
@@ -235,6 +235,7 @@ export default function AuditLogsTable({
{entry.actor ? (
<Group gap="sm" wrap="nowrap">
<CustomAvatar
avatarUrl={entry.actor.avatarUrl}
name={entry.actor.name}
size={36}
/>
@@ -18,6 +18,7 @@ export type IAuditLog = {
id: string;
name: string;
email: string;
avatarUrl?: string;
};
resource?: {
id: string;
@@ -1,5 +1,6 @@
import * as z from "zod";
import { useForm, zodResolver } from "@mantine/form";
import { z } from "zod/v4";
import { useForm } from "@mantine/form";
import { zod4Resolver } from "mantine-form-zod-resolver";
import {
Container,
Title,
@@ -30,7 +31,7 @@ export function CloudLoginForm() {
const { data: joinedWorkspaces } = useJoinedWorkspacesQuery();
const form = useForm<any>({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
initialValues: {
hostname: "",
},
@@ -1,8 +1,8 @@
import React, { useState } from "react";
import { Modal, TextInput, PasswordInput, Button, Stack } from "@mantine/core";
import { useForm } from "@mantine/form";
import { zodResolver } from "mantine-form-zod-resolver";
import { z } from "zod";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { z } from "zod/v4";
import { notifications } from "@mantine/notifications";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
@@ -34,7 +34,7 @@ export function LdapLoginModal({
const [error, setError] = useState<string | null>(null);
const form = useForm({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
initialValues: {
username: "",
password: "",
@@ -1,16 +1,17 @@
import { Button, Group, Text, Modal, TextInput } from "@mantine/core";
import * as z from "zod";
import { z } from "zod/v4";
import { useState } from "react";
import { useDisclosure } from "@mantine/hooks";
import * as React from "react";
import { useForm, zodResolver } from "@mantine/form";
import { useForm } from "@mantine/form";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next";
import { getSubdomainHost } from "@/lib/config.ts";
import { IWorkspace } from "@/features/workspace/types/workspace.types.ts";
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
import { getHostnameUrl } from "@/ee/utils.ts";
import { useAtom } from "jotai/index";
import { useAtom } from "jotai";
import {
currentUserAtom,
workspaceAtom,
@@ -66,7 +67,7 @@ function ChangeHostnameForm({ onClose }: ChangeHostnameFormProps) {
const [currentUser, setCurrentUser] = useAtom(currentUserAtom);
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
initialValues: {
hostname: currentUser?.workspace?.hostname,
},
@@ -1,7 +1,8 @@
import * as z from "zod";
import { z } from "zod/v4";
import React from "react";
import { Button, Group, Modal, Textarea } from "@mantine/core";
import { useForm, zodResolver } from "@mantine/form";
import { useForm } from "@mantine/form";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { useTranslation } from "react-i18next";
import { useActivateMutation } from "@/ee/licence/queries/license-query.ts";
import { useDisclosure } from "@mantine/hooks";
@@ -49,7 +50,7 @@ export function ActivateLicenseForm({ onClose }: ActivateLicenseFormProps) {
const activateLicenseMutation = useActivateMutation();
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
initialValues: {
licenseKey: "",
},
@@ -23,8 +23,8 @@ import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next";
import { regenerateBackupCodes } from "@/ee/mfa";
import { useForm } from "@mantine/form";
import { zodResolver } from "mantine-form-zod-resolver";
import { z } from "zod";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { z } from "zod/v4";
import useCurrentUser from "@/features/user/hooks/use-current-user";
interface MfaBackupCodesModalProps {
@@ -51,7 +51,7 @@ export function MfaBackupCodesModal({
});
const form = useForm({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
initialValues: {
confirmPassword: "",
},
@@ -12,7 +12,7 @@ import {
ThemeIcon,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { zodResolver } from "mantine-form-zod-resolver";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { IconDeviceMobile, IconLock } from "@tabler/icons-react";
import { useNavigate } from "react-router-dom";
import { notifications } from "@mantine/notifications";
@@ -20,7 +20,7 @@ import classes from "./mfa-challenge.module.css";
import { verifyMfa } from "@/ee/mfa";
import APP_ROUTE, { getPostLoginRedirect } from "@/lib/app-route";
import { useTranslation } from "react-i18next";
import * as z from "zod";
import { z } from "zod/v4";
import { MfaBackupCodeInput } from "./mfa-backup-code-input";
const formSchema = z.object({
@@ -43,7 +43,7 @@ export function MfaChallenge() {
const [useBackupCode, setUseBackupCode] = useState(false);
const form = useForm<MfaChallengeFormValues>({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
initialValues: {
code: "",
},
@@ -9,11 +9,11 @@ import {
} from "@mantine/core";
import { IconShieldOff, IconAlertTriangle } from "@tabler/icons-react";
import { useForm } from "@mantine/form";
import { zodResolver } from "mantine-form-zod-resolver";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { useMutation } from "@tanstack/react-query";
import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { z } from "zod/v4";
import { disableMfa } from "@/ee/mfa";
import useCurrentUser from "@/features/user/hooks/use-current-user";
@@ -41,7 +41,7 @@ export function MfaDisableModal({
});
const form = useForm({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
initialValues: {
confirmPassword: "",
},
@@ -63,7 +63,7 @@ export function MfaDisableModal({
const handleSubmit = async (values: { confirmPassword?: string }) => {
// Only send confirmPassword if it's required (non-SSO users)
const payload = requiresPassword
const payload = requiresPassword
? { confirmPassword: values.confirmPassword }
: {};
await disableMutation.mutateAsync(payload);
@@ -36,8 +36,8 @@ import { useMutation } from "@tanstack/react-query";
import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next";
import { setupMfa, enableMfa } from "@/ee/mfa";
import { zodResolver } from "mantine-form-zod-resolver";
import { z } from "zod";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { z } from "zod/v4";
interface MfaSetupModalProps {
opened: boolean;
@@ -71,7 +71,7 @@ export function MfaSetupModal({
const [manualEntryOpen, setManualEntryOpen] = useState(false);
const form = useForm({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
initialValues: {
verificationCode: "",
},
@@ -1,7 +1,7 @@
import { useAtom } from "jotai";
import * as z from "zod";
import { z } from "zod/v4";
import { useForm } from "@mantine/form";
import { zodResolver } from "mantine-form-zod-resolver";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
import React, { useState } from "react";
import { Button, Text, TagsInput } from "@mantine/core";
@@ -22,7 +22,7 @@ export default function AllowedDomains() {
const [, setDomains] = useState<string[]>([]);
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
initialValues: {
emailDomains: workspace?.emailDomains || [],
},
@@ -1,7 +1,7 @@
import React from "react";
import { z } from "zod";
import { z } from "zod/v4";
import { useForm } from "@mantine/form";
import { zodResolver } from "mantine-form-zod-resolver";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { Box, Button, Group, Stack, Switch, TextInput } from "@mantine/core";
import classes from "@/ee/security/components/sso.module.css";
import { IAuthProvider } from "@/ee/security/types/security.types.ts";
@@ -30,7 +30,7 @@ export function SsoGoogleForm({ provider, onClose }: SsoFormProps) {
isEnabled: provider.isEnabled,
allowSignup: provider.allowSignup,
},
validate: zodResolver(ssoSchema),
validate: zod4Resolver(ssoSchema),
});
const handleSubmit = async (values: SSOFormValues) => {
@@ -1,7 +1,7 @@
import React from "react";
import { z } from "zod";
import { z } from "zod/v4";
import { useForm } from "@mantine/form";
import { zodResolver } from "mantine-form-zod-resolver";
import { zod4Resolver } from "mantine-form-zod-resolver";
import {
Box,
Button,
@@ -59,7 +59,7 @@ export function SsoLDAPForm({ provider, onClose }: SsoFormProps) {
allowSignup: provider.allowSignup,
groupSync: provider.groupSync || false,
},
validate: zodResolver(ssoSchema),
validate: zod4Resolver(ssoSchema),
});
const handleSubmit = async (values: SSOFormValues) => {
@@ -1,6 +1,7 @@
import React from "react";
import { z } from "zod";
import { useForm, zodResolver } from "@mantine/form";
import { z } from "zod/v4";
import { useForm } from "@mantine/form";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { Box, Button, Group, Stack, Switch, TextInput } from "@mantine/core";
import { buildCallbackUrl } from "@/ee/security/sso.utils.ts";
import classes from "@/ee/security/components/sso.module.css";
@@ -39,7 +40,7 @@ export function SsoOIDCForm({ provider, onClose }: SsoFormProps) {
allowSignup: provider.allowSignup,
groupSync: provider.groupSync || false,
},
validate: zodResolver(ssoSchema),
validate: zod4Resolver(ssoSchema),
});
const callbackUrl = buildCallbackUrl({
@@ -1,7 +1,7 @@
import React from "react";
import { z } from "zod";
import { z } from "zod/v4";
import { useForm } from "@mantine/form";
import { zodResolver } from "mantine-form-zod-resolver";
import { zod4Resolver } from "mantine-form-zod-resolver";
import {
Box,
Button,
@@ -49,7 +49,7 @@ export function SsoSamlForm({ provider, onClose }: SsoFormProps) {
allowSignup: provider.allowSignup,
groupSync: provider.groupSync || false,
},
validate: zodResolver(ssoSchema),
validate: zod4Resolver(ssoSchema),
});
const callbackUrl = buildCallbackUrl({
@@ -1,8 +1,8 @@
import { useState } from "react";
import * as z from "zod";
import { useForm, zodResolver } from "@mantine/form";
import { z } from "zod/v4";
import { useForm } from "@mantine/form";
import { zod4Resolver } from "mantine-form-zod-resolver";
import useAuth from "@/features/auth/hooks/use-auth";
import { IForgotPassword } from "@/features/auth/types/auth.types";
import { Box, Button, Container, Text, TextInput, Title } from "@mantine/core";
import classes from "./auth.module.css";
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
@@ -10,10 +10,10 @@ import { useTranslation } from "react-i18next";
const formSchema = z.object({
email: z
.string()
.min(1, { message: "Email is required" })
.email({ message: "Invalid email address" }),
.email()
.min(1, { message: "Email is required" }),
});
type FormValues = z.infer<typeof formSchema>;
export function ForgotPasswordForm() {
const { t } = useTranslation();
@@ -21,14 +21,14 @@ export function ForgotPasswordForm() {
const [isTokenSent, setIsTokenSent] = useState<boolean>(false);
useRedirectIfAuthenticated();
const form = useForm<IForgotPassword>({
validate: zodResolver(formSchema),
const form = useForm<FormValues>({
validate: zod4Resolver(formSchema),
initialValues: {
email: "",
},
});
async function onSubmit(data: IForgotPassword) {
async function onSubmit(data: FormValues) {
if (await forgotPassword(data)) {
setIsTokenSent(true);
}
@@ -1,5 +1,5 @@
import * as React from "react";
import * as z from "zod";
import { z } from "zod/v4";
import { useForm } from "@mantine/form";
import {
@@ -11,9 +11,8 @@ import {
Box,
Stack,
} from "@mantine/core";
import { zodResolver } from "mantine-form-zod-resolver";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { useParams, useSearchParams } from "react-router-dom";
import { IRegister } from "@/features/auth/types/auth.types";
import useAuth from "@/features/auth/hooks/use-auth";
import classes from "@/features/auth/components/auth.module.css";
import { useGetInvitationQuery } from "@/features/workspace/queries/workspace-query.ts";
@@ -40,14 +39,14 @@ export function InviteSignUpForm() {
useRedirectIfAuthenticated();
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
initialValues: {
name: "",
password: "",
},
});
async function onSubmit(data: IRegister) {
async function onSubmit(data: FormValues) {
const invitationToken = searchParams.get("token");
await invitationSignup({
@@ -1,7 +1,7 @@
import * as z from "zod";
import { useForm, zodResolver } from "@mantine/form";
import { z } from "zod/v4";
import { useForm } from "@mantine/form";
import { zod4Resolver } from "mantine-form-zod-resolver";
import useAuth from "@/features/auth/hooks/use-auth";
import { ILogin } from "@/features/auth/types/auth.types";
import {
Container,
Title,
@@ -24,11 +24,11 @@ import React from "react";
const formSchema = z.object({
email: z
.string()
.min(1, { message: "email is required" })
.email({ message: "Invalid email address" }),
.email()
.min(1, { message: "email is required" }),
password: z.string().min(1, { message: "Password is required" }),
});
type FormValues = z.infer<typeof formSchema>;
export function LoginForm() {
const { t } = useTranslation();
@@ -41,15 +41,15 @@ export function LoginForm() {
error,
} = useWorkspacePublicDataQuery();
const form = useForm<ILogin>({
validate: zodResolver(formSchema),
const form = useForm<FormValues>({
validate: zod4Resolver(formSchema),
initialValues: {
email: "",
password: "",
},
});
async function onSubmit(data: ILogin) {
async function onSubmit(data: FormValues) {
await signIn(data);
}
@@ -1,7 +1,7 @@
import * as z from "zod";
import { useForm, zodResolver } from "@mantine/form";
import { z } from "zod/v4";
import { useForm } from "@mantine/form";
import { zod4Resolver } from "mantine-form-zod-resolver";
import useAuth from "@/features/auth/hooks/use-auth";
import { IPasswordReset } from "@/features/auth/types/auth.types";
import { Box, Button, Container, PasswordInput, Title } from "@mantine/core";
import classes from "./auth.module.css";
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
@@ -12,6 +12,7 @@ const formSchema = z.object({
.string()
.min(8, { message: "Password must contain at least 8 characters" }),
});
type FormValues = z.infer<typeof formSchema>;
interface PasswordResetFormProps {
resetToken?: string;
@@ -22,14 +23,14 @@ export function PasswordResetForm({ resetToken }: PasswordResetFormProps) {
const { passwordReset, isLoading } = useAuth();
useRedirectIfAuthenticated();
const form = useForm<IPasswordReset>({
validate: zodResolver(formSchema),
const form = useForm<FormValues>({
validate: zod4Resolver(formSchema),
initialValues: {
newPassword: "",
},
});
async function onSubmit(data: IPasswordReset) {
async function onSubmit(data: FormValues) {
await passwordReset({
token: resetToken,
newPassword: data.newPassword,
@@ -1,6 +1,7 @@
import * as React from "react";
import * as z from "zod";
import { useForm, zodResolver } from "@mantine/form";
import { z } from "zod/v4";
import { useForm } from "@mantine/form";
import { zod4Resolver } from "mantine-form-zod-resolver";
import {
Container,
Title,
@@ -11,7 +12,6 @@ import {
Anchor,
Text,
} from "@mantine/core";
import { ISetupWorkspace } from "@/features/auth/types/auth.types";
import useAuth from "@/features/auth/hooks/use-auth";
import classes from "@/features/auth/components/auth.module.css";
import { useTranslation } from "react-i18next";
@@ -24,19 +24,19 @@ const formSchema = z.object({
workspaceName: z.string().trim().max(50).optional(),
name: z.string().min(1).max(50),
email: z
.string()
.min(1, { message: "email is required" })
.email({ message: "Invalid email address" }),
.email()
.min(1, { message: "email is required" }),
password: z.string().min(8),
});
type FormValues = z.infer<typeof formSchema>;
export function SetupWorkspaceForm() {
const { t } = useTranslation();
const { setupWorkspace, isLoading } = useAuth();
// useRedirectIfAuthenticated();
const form = useForm<ISetupWorkspace>({
validate: zodResolver(formSchema),
const form = useForm<FormValues>({
validate: zod4Resolver(formSchema),
initialValues: {
workspaceName: "",
name: "",
@@ -45,7 +45,7 @@ export function SetupWorkspaceForm() {
},
});
async function onSubmit(data: ISetupWorkspace) {
async function onSubmit(data: FormValues) {
await setupWorkspace(data);
}
@@ -12,9 +12,9 @@ import {
TextInput,
} from "@mantine/core";
import { IconEdit } from "@tabler/icons-react";
import { z } from "zod";
import { z } from "zod/v4";
import { useForm } from "@mantine/form";
import { zodResolver } from "mantine-form-zod-resolver";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next";
import i18n from "i18next";
@@ -49,7 +49,7 @@ export default function EmbedView(props: NodeViewProps) {
initialValues: {
url: "",
},
validate: zodResolver(schema),
validate: zod4Resolver(schema),
});
const handleResize = useCallback(
@@ -2,11 +2,11 @@ import { Group, Box, Button, TextInput, Stack, Textarea } from "@mantine/core";
import React, { useState } from "react";
import { useCreateGroupMutation } from "@/features/group/queries/group-query.ts";
import { useForm } from "@mantine/form";
import * as z from "zod";
import { z } from "zod/v4";
import { useNavigate } from "react-router-dom";
import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx";
import { useTranslation } from "react-i18next";
import { zodResolver } from 'mantine-form-zod-resolver';
import { zod4Resolver } from 'mantine-form-zod-resolver';
const formSchema = z.object({
name: z.string().trim().min(2).max(100),
@@ -22,7 +22,7 @@ export function CreateGroupForm() {
const navigate = useNavigate();
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
initialValues: {
name: "",
description: "",
@@ -5,10 +5,10 @@ import {
useUpdateGroupMutation,
} from "@/features/group/queries/group-query.ts";
import { useForm } from "@mantine/form";
import * as z from "zod";
import { z } from "zod/v4";
import { useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { zodResolver } from "mantine-form-zod-resolver";
import { zod4Resolver } from "mantine-form-zod-resolver";
const formSchema = z.object({
name: z.string().min(2).max(100),
@@ -35,7 +35,7 @@ export function EditGroupForm({ onClose }: EditGroupFormProps) {
}, [isSuccess]);
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
initialValues: {
name: group?.name,
description: group?.description,
@@ -1,8 +1,8 @@
import { Group, Box, Button, TextInput, Stack, Textarea } from "@mantine/core";
import React, { useEffect } from "react";
import { useForm } from "@mantine/form";
import { zodResolver } from "mantine-form-zod-resolver";
import * as z from "zod";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { z } from "zod/v4";
import { useNavigate } from "react-router-dom";
import { useCreateSpaceMutation } from "@/features/space/queries/space-query.ts";
import { computeSpaceSlug } from "@/lib";
@@ -30,7 +30,7 @@ export function CreateSpaceForm() {
const navigate = useNavigate();
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
validateInputOnChange: ["slug"],
initialValues: {
name: "",
@@ -1,7 +1,8 @@
import { Group, Box, Button, TextInput, Stack, Textarea } from "@mantine/core";
import React from "react";
import { useForm, zodResolver } from "@mantine/form";
import * as z from "zod";
import { useForm } from "@mantine/form";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { z } from "zod/v4";
import { useUpdateSpaceMutation } from "@/features/space/queries/space-query.ts";
import { ISpace } from "@/features/space/types/space.types.ts";
import { useTranslation } from "react-i18next";
@@ -29,7 +30,7 @@ export function EditSpaceForm({ space, readOnly }: EditSpaceFormProps) {
const updateSpaceMutation = useUpdateSpaceMutation();
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
initialValues: {
name: space?.name,
description: space?.description || "",
@@ -1,7 +1,8 @@
import { useAtom } from "jotai";
import { focusAtom } from "jotai-optics";
import * as z from "zod";
import { useForm, zodResolver } from "@mantine/form";
import { z } from "zod/v4";
import { useForm } from "@mantine/form";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
import { updateUser } from "@/features/user/services/user-service.ts";
import { IUser } from "@/features/user/types/user.types.ts";
@@ -25,7 +26,7 @@ export default function AccountNameForm() {
const [, setUser] = useAtom(userAtom);
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
initialValues: {
name: currentUser?.user.name,
},
@@ -6,13 +6,14 @@ import {
Group,
PasswordInput,
} from "@mantine/core";
import * as z from "zod";
import { z } from "zod/v4";
import { useState } from "react";
import { useAtom } from "jotai";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
import { useDisclosure } from "@mantine/hooks";
import * as React from "react";
import { useForm, zodResolver } from "@mantine/form";
import { useForm } from "@mantine/form";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { useTranslation } from "react-i18next";
export default function ChangeEmail() {
@@ -48,9 +49,9 @@ export default function ChangeEmail() {
}
const formSchema = z.object({
email: z.string({ required_error: "New email is required" }).email(),
email: z.email({ error: "New email is required" }),
password: z
.string({ required_error: "your current password is required" })
.string({ error: "your current password is required" })
.min(8),
});
@@ -61,7 +62,7 @@ function ChangeEmailForm() {
const [isLoading, setIsLoading] = useState(false);
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
initialValues: {
password: "",
email: "",
@@ -1,9 +1,10 @@
import { Button, Group, Text, Modal, PasswordInput } from "@mantine/core";
import * as z from "zod";
import { z } from "zod/v4";
import { useState } from "react";
import { useDisclosure } from "@mantine/hooks";
import * as React from "react";
import { useForm, zodResolver } from "@mantine/form";
import { useForm } from "@mantine/form";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { changePassword } from "@/features/auth/services/auth-service.ts";
import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next";
@@ -42,9 +43,9 @@ export default function ChangePassword() {
const formSchema = z.object({
oldPassword: z
.string({ required_error: "your current password is required" })
.string({ error: "your current password is required" })
.min(8),
newPassword: z.string({ required_error: "New password is required" }).min(8),
newPassword: z.string({ error: "New password is required" }).min(8),
});
type FormValues = z.infer<typeof formSchema>;
@@ -57,7 +58,7 @@ function ChangePasswordForm({ onClose }: ChangePasswordFormProps) {
const [isLoading, setIsLoading] = useState(false);
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
initialValues: {
oldPassword: "",
newPassword: "",
@@ -26,7 +26,9 @@ export default function InviteActionMenu({ invitationId }: Props) {
const handleCopyLink = async (invitationId: string) => {
try {
const link = await getInviteLink({ invitationId });
clipboard.copy(link.inviteLink);
const url = new URL(link.inviteLink);
const inviteLink = `${window.location.origin}${url.pathname}${url.search}`;
clipboard.copy(inviteLink);
notifications.show({ message: t("Link copied") });
} catch (err) {
notifications.show({
@@ -1,12 +1,12 @@
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
import { useAtom } from "jotai";
import * as z from "zod";
import { z } from "zod/v4";
import { useState } from "react";
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
import { IWorkspace } from "@/features/workspace/types/workspace.types.ts";
import { TextInput, Button } from "@mantine/core";
import { useForm } from "@mantine/form";
import { zodResolver } from "mantine-form-zod-resolver";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { notifications } from "@mantine/notifications";
import useUserRole from "@/hooks/use-user-role.tsx";
import { useTranslation } from "react-i18next";
@@ -24,7 +24,7 @@ export default function WorkspaceNameForm() {
const { isAdmin } = useUserRole();
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
validate: zod4Resolver(formSchema),
initialValues: {
name: workspace?.name,
},
@@ -25,6 +25,7 @@ export interface IWorkspace {
aiSearch?: boolean;
generativeAi?: boolean;
disablePublicSharing?: boolean;
mcpEnabled?: boolean;
trashRetentionDays?: number;
}
@@ -36,6 +37,7 @@ export interface IWorkspaceSettings {
export interface IWorkspaceAiSettings {
search?: boolean;
generative?: boolean;
mcp?: boolean;
}
export interface IWorkspaceSharingSettings {
+1
View File
@@ -71,6 +71,7 @@ export const mantineCssResolver: CSSVariablesResolver = (theme) => ({
"--input-error-size": theme.fontSizes.sm,
},
light: {
"--mantine-color-dimmed": "#6b7280",
"--mantine-color-dark-light-color": "#4e5359",
"--mantine-color-dark-light-hover": "var(--mantine-color-gray-light-hover)",
},
+3 -1
View File
@@ -43,6 +43,7 @@
"@keyv/redis": "^5.1.6",
"@langchain/core": "1.1.18",
"@langchain/textsplitters": "1.0.1",
"@modelcontextprotocol/sdk": "^1.27.1",
"@nestjs-labs/nestjs-ioredis": "^11.0.4",
"@nestjs/bullmq": "^11.0.4",
"@nestjs/cache-manager": "^3.1.0",
@@ -110,7 +111,8 @@
"tseep": "^1.3.1",
"typesense": "^2.1.0",
"ws": "^8.19.0",
"yauzl": "^3.2.0"
"yauzl": "^3.2.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@eslint/js": "^9.20.0",
@@ -5,5 +5,6 @@ import { SearchService } from './search.service';
@Module({
controllers: [SearchController],
providers: [SearchService],
exports: [SearchService],
})
export class SearchModule {}
@@ -42,6 +42,10 @@ export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {
@IsBoolean()
disablePublicSharing: boolean;
@IsOptional()
@IsBoolean()
mcpEnabled: boolean;
@IsOptional()
@IsInt()
@Min(1)
@@ -326,7 +326,8 @@ export class WorkspaceService {
if (
typeof updateWorkspaceDto.disablePublicSharing !== 'undefined' ||
typeof updateWorkspaceDto.trashRetentionDays !== 'undefined'
typeof updateWorkspaceDto.trashRetentionDays !== 'undefined' ||
typeof updateWorkspaceDto.mcpEnabled !== 'undefined'
) {
const ws = await this.db
.selectFrom('workspaces')
@@ -424,10 +425,25 @@ export class WorkspaceService {
}
}
if (typeof updateWorkspaceDto.mcpEnabled !== 'undefined') {
const prev = settingsBefore?.ai?.mcp ?? false;
if (prev !== updateWorkspaceDto.mcpEnabled) {
before.mcpEnabled = prev;
after.mcpEnabled = updateWorkspaceDto.mcpEnabled;
}
await this.workspaceRepo.updateAiSettings(
workspaceId,
'mcp',
updateWorkspaceDto.mcpEnabled,
trx,
);
}
delete updateWorkspaceDto.restrictApiToAdmins;
delete updateWorkspaceDto.aiSearch;
delete updateWorkspaceDto.generativeAi;
delete updateWorkspaceDto.disablePublicSharing;
delete updateWorkspaceDto.mcpEnabled;
await this.workspaceRepo.updateWorkspace(
updateWorkspaceDto,