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
@@ -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({