From ca8e492d30957adadf450db1f07eeab79612440d Mon Sep 17 00:00:00 2001
From: Philipinho <16838612+Philipinho@users.noreply.github.com>
Date: Mon, 24 Aug 2026 02:15:38 +0100
Subject: [PATCH] feat: mcp oauth
---
.../public/locales/en-US/translation.json | 36 +-
apps/client/src/App.tsx | 6 +
.../src/ee/ai/components/mcp-settings.tsx | 152 +++++++-
.../src/ee/api-key/pages/user-api-keys.tsx | 119 ++++--
apps/client/src/ee/features.ts | 1 +
.../components/authorized-apps-panel.tsx | 19 +
.../oauth/components/oauth-grants-table.tsx | 178 +++++++++
.../src/ee/oauth/pages/oauth-consent.tsx | 353 ++++++++++++++++++
.../src/ee/oauth/queries/oauth-query.ts | 45 +++
.../src/ee/oauth/services/oauth-service.ts | 36 ++
apps/client/src/ee/oauth/types/oauth.types.ts | 24 ++
.../workspace/types/workspace.types.ts | 6 +
apps/client/src/lib/api-client.ts | 2 +
apps/server/package.json | 1 +
.../decorators/oauth-scope.decorator.ts | 8 +
.../require-session-auth.decorator.ts | 6 +
apps/server/src/common/events/audit-events.ts | 7 +
.../src/common/events/event.contants.ts | 2 +
apps/server/src/common/features.ts | 1 +
.../src/common/guards/jwt-auth.guard.spec.ts | 186 +++++++++
.../src/common/guards/jwt-auth.guard.ts | 43 +++
.../common/helpers/security-headers.spec.ts | 66 ++++
.../src/common/helpers/security-headers.ts | 16 +
.../core/attachment/attachment.controller.ts | 3 +
apps/server/src/core/auth/auth.controller.ts | 4 +-
apps/server/src/core/auth/dto/jwt-payload.ts | 12 +
.../src/core/auth/services/auth.service.ts | 19 +
.../src/core/auth/strategies/jwt.strategy.ts | 59 ++-
.../src/core/comment/comment.controller.ts | 4 +
apps/server/src/core/page/page.controller.ts | 9 +
.../src/core/search/search.controller.ts | 3 +
.../server/src/core/space/space.controller.ts | 5 +
apps/server/src/core/user/user.controller.ts | 2 +
.../controllers/workspace.controller.ts | 3 +
.../workspace/dto/update-workspace.dto.ts | 24 ++
.../workspace/services/workspace.service.ts | 27 +-
.../src/core/workspace/workspace.util.spec.ts | 49 +++
.../src/core/workspace/workspace.util.ts | 34 ++
.../migrations/20260816T190412-oauth.ts | 96 +++++
.../repos/workspace/workspace.repo.ts | 1 +
apps/server/src/database/types/db.d.ts | 62 +++
.../server/src/database/types/entity.types.ts | 11 +
apps/server/src/ee | 2 +-
.../integrations/throttle/throttle.module.ts | 11 +-
.../integrations/throttle/throttler-names.ts | 12 +
apps/server/src/main.ts | 48 ++-
pnpm-lock.yaml | 14 +
47 files changed, 1756 insertions(+), 71 deletions(-)
create mode 100644 apps/client/src/ee/oauth/components/authorized-apps-panel.tsx
create mode 100644 apps/client/src/ee/oauth/components/oauth-grants-table.tsx
create mode 100644 apps/client/src/ee/oauth/pages/oauth-consent.tsx
create mode 100644 apps/client/src/ee/oauth/queries/oauth-query.ts
create mode 100644 apps/client/src/ee/oauth/services/oauth-service.ts
create mode 100644 apps/client/src/ee/oauth/types/oauth.types.ts
create mode 100644 apps/server/src/common/decorators/oauth-scope.decorator.ts
create mode 100644 apps/server/src/common/decorators/require-session-auth.decorator.ts
create mode 100644 apps/server/src/common/guards/jwt-auth.guard.spec.ts
create mode 100644 apps/server/src/common/helpers/security-headers.spec.ts
create mode 100644 apps/server/src/core/workspace/workspace.util.spec.ts
create mode 100644 apps/server/src/database/migrations/20260816T190412-oauth.ts
diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json
index 03e10c53e..e93010c68 100644
--- a/apps/client/public/locales/en-US/translation.json
+++ b/apps/client/public/locales/en-US/translation.json
@@ -706,9 +706,8 @@
"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 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.",
+ "Connect with your Docmost account via OAuth when your client supports it, or use an API key from your account settings.": "Connect with your Docmost account via OAuth when your client supports it, or use an API key from 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",
"Manage API keys for all users in the workspace. View the API documentation for usage details.": "Manage API keys for all users in the workspace. View the API documentation for usage details.",
@@ -1302,5 +1301,36 @@
"Error loading attachments.": "Error loading attachments.",
"No attachments on this page yet.": "No attachments on this page yet.",
"Uploaded by {{name}}": "Uploaded by {{name}}",
- "Download {{name}}": "Download {{name}}"
+ "Download {{name}}": "Download {{name}}",
+ "Access revoked": "Access revoked",
+ "Authorize application": "Authorize application",
+ "{{name}} wants to access {{workspace}}": "{{name}} wants to access {{workspace}}",
+ "Not you? Switch account": "Not you? Switch account",
+ "This application will be able to:": "This application will be able to:",
+ "Write": "Write",
+ "Invalid authorization request": "Invalid authorization request",
+ "Authorize": "Authorize",
+ "Application": "Application",
+ "Permissions": "Permissions",
+ "Authorized": "Authorized",
+ "Revoke access": "Revoke access",
+ "Revoke access for {{name}}": "Revoke access for {{name}}",
+ "Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.",
+ "Something went wrong. Please try again.": "Something went wrong. Please try again.",
+ "Trusted applications": "Trusted applications",
+ "Remove {{name}}": "Remove {{name}}",
+ "This origin is already trusted.": "This origin is already trusted.",
+ "Trusted application name": "Trusted application name",
+ "Trusted application origin": "Trusted application origin",
+ "I recognize this application and want to continue": "I recognize this application and want to continue",
+ "You will be redirected to": "You will be redirected to",
+ "View content without making changes.": "View content without making changes.",
+ "Create and modify content.": "Create and modify content.",
+ "This application is not on your workspace's trusted list. Authorize only if you recognize it.": "This application is not on your workspace's trusted list. Authorize only if you recognize it.",
+ "Applications with these callback origins are trusted. Members will not see a warning when authorizing them.": "Applications with these callback origins are trusted. Members will not see a warning when authorizing them.",
+ "Enter the app's callback origin, e.g. https://app.yourcompany.com": "Enter the app's callback origin, e.g. https://app.yourcompany.com",
+ "Applications and AI assistants you have authorized to access your account.": "Applications and AI assistants you have authorized to access your account.",
+ "Your workspace has MCP enabled. Connect AI assistants with your Docmost account, or with an API key.": "Your workspace has MCP enabled. Connect AI assistants with your Docmost account, or with an API key.",
+ "Authorized apps": "Authorized apps",
+ "No authorized apps yet.": "No authorized apps yet."
}
diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx
index ab291ffea..8d12779cc 100644
--- a/apps/client/src/App.tsx
+++ b/apps/client/src/App.tsx
@@ -47,6 +47,7 @@ import FavoritesPage from "@/pages/favorites/favorites-page";
import AiChat from "@/ee/ai-chat/pages/ai-chat.tsx";
import VerifyEmail from "@/ee/pages/verify-email.tsx";
import LabelPage from "@/pages/label/label-page";
+import OAuthConsent from "@/ee/oauth/pages/oauth-consent.tsx";
export default function App() {
const { t } = useTranslation();
@@ -63,6 +64,7 @@ export default function App() {
} />
} />
} />
+ } />
{!isCloud() && (
} />
@@ -116,6 +118,10 @@ export default function App() {
element={}
/>
} />
+ }
+ />
} />
} />
} />
diff --git a/apps/client/src/ee/ai/components/mcp-settings.tsx b/apps/client/src/ee/ai/components/mcp-settings.tsx
index e7cc2234b..102da44e3 100644
--- a/apps/client/src/ee/ai/components/mcp-settings.tsx
+++ b/apps/client/src/ee/ai/components/mcp-settings.tsx
@@ -1,7 +1,9 @@
import {
Anchor,
+ Button,
Group,
List,
+ Table,
Text,
Switch,
TextInput,
@@ -15,14 +17,33 @@ import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
import React, { useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
+import { ITrustedOAuthClient } from "@/features/workspace/types/workspace.types.ts";
import { notifications } from "@mantine/notifications";
import { useHasFeature } from "@/ee/hooks/use-feature";
import { Feature } from "@/ee/features";
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
import { getAppUrl } from "@/lib/config.ts";
-import { IconCheck, IconCopy, IconInfoCircle } from "@tabler/icons-react";
+import {
+ IconCheck,
+ IconCopy,
+ IconInfoCircle,
+ IconTrash,
+} from "@tabler/icons-react";
import { CopyButton } from "@/components/common/copy-button.tsx";
+// Mirrors the server rule: an exact https origin, tolerating only a trailing slash.
+function parseTrustedOrigin(value: string): string | null {
+ const input = value.trim().toLowerCase();
+ try {
+ const url = new URL(input);
+ if (url.protocol !== "https:") return null;
+ if (input !== url.origin && input !== `${url.origin}/`) return null;
+ return url.origin;
+ } catch {
+ return null;
+ }
+}
+
export default function McpSettings() {
const { t } = useTranslation();
const [workspace, setWorkspace] = useAtom(workspaceAtom);
@@ -30,7 +51,14 @@ export default function McpSettings() {
const hasAccess = useHasFeature(Feature.MCP);
const upgradeLabel = useUpgradeLabel();
+ const [newClientName, setNewClientName] = useState("");
+ const [newClientOrigin, setNewClientOrigin] = useState("");
+
const mcpUrl = `${getAppUrl()}/mcp`;
+ const storedTrustedClients = workspace?.trustedOauthClients;
+ const trustedClients = Array.isArray(storedTrustedClients)
+ ? storedTrustedClients
+ : [];
const handleChange = async (event: React.ChangeEvent) => {
const value = event.currentTarget.checked;
@@ -46,6 +74,51 @@ export default function McpSettings() {
}
};
+ const saveTrustedClients = async (next: ITrustedOAuthClient[]) => {
+ try {
+ const updatedWorkspace = await updateWorkspace({
+ trustedOauthClients: next,
+ });
+ setWorkspace(updatedWorkspace);
+ return true;
+ } catch (err) {
+ notifications.show({
+ message: err?.response?.data?.message,
+ color: "red",
+ });
+ return false;
+ }
+ };
+
+ const handleAddTrustedClient = async () => {
+ const name = newClientName.trim();
+ const origin = parseTrustedOrigin(newClientOrigin);
+ if (!origin) {
+ notifications.show({
+ message: t("Enter the app's callback origin, e.g. https://app.yourcompany.com"),
+ color: "red",
+ });
+ return;
+ }
+ if (trustedClients.some((client) => client.origin.toLowerCase() === origin)) {
+ notifications.show({
+ message: t("This origin is already trusted."),
+ color: "red",
+ });
+ return;
+ }
+ if (await saveTrustedClients([...trustedClients, { origin, name }])) {
+ setNewClientName("");
+ setNewClientOrigin("");
+ }
+ };
+
+ const handleRemoveTrustedClient = (origin: string) => {
+ void saveTrustedClients(
+ trustedClients.filter((client) => client.origin !== origin),
+ );
+ };
+
return (
{!hasAccess && (
@@ -108,7 +181,7 @@ export default function McpSettings() {
{t(
- "Use your API key for authentication. You can manage API keys in your account settings.",
+ "Connect with your Docmost account via OAuth when your client supports it, or use an API key from your account settings.",
)}
@@ -149,6 +222,81 @@ export default function McpSettings() {
+
+
+
+ {t("Trusted applications")}
+
+
+ {t(
+ "Applications with these callback origins are trusted. Members will not see a warning when authorizing them.",
+ )}
+
+
+ {trustedClients.length > 0 && (
+