diff --git a/docs/content/Guides/Integrations/mcp-tool-integration.mdx b/docs/content/Guides/Integrations/mcp-tool-integration.mdx
index 4d89fafe..083bc1a3 100644
--- a/docs/content/Guides/Integrations/mcp-tool-integration.mdx
+++ b/docs/content/Guides/Integrations/mcp-tool-integration.mdx
@@ -26,7 +26,7 @@ If not set, it is derived from the host of `CONNECTOR_REDIRECT_BASE_URI`, then f
### Step 2: Add an MCP Server
-Go to **Settings** > **Connectors** and pick a preset (Notion, Linear, Atlassian, Sentry, Asana, Stripe), or choose **Add custom connector** > **MCP server**. The same form opens from **Settings** > **Tools** > **Add Tool**. Enter the server URL, select an auth type, and click **Test Connection** to verify, then **Save**.
+Go to **Settings** > **Connectors** and pick a preset (Notion, Linear, Atlassian, Sentry, Asana, Stripe): **Sign in to Notion** opens the service's sign-in, and its tools are ready when you come back. For any other server, choose **Add custom connector** > **MCP server** and enter its URL and authentication; scopes and the timeout are under **Show advanced**. Enter the server URL, select an auth type, and click **Test Connection** to verify, then **Save**.
### Step 3: Enable for Your Agent
@@ -34,7 +34,7 @@ In your agent configuration, enable the MCP tools you want the agent to use.
-Presets fill in the server URL and sign in with OAuth. Admins can turn presets off one by one, and turn off custom MCP servers, in **Admin > Connectors**. See [Connectors](/Guides/Connectors).
+Presets need no URL or form: they sign in with OAuth in one step. Admins can turn presets off one by one, and turn off custom MCP servers, in **Admin > Connectors**. See [Connectors](/Guides/Connectors).
## Authentication Types
diff --git a/docsgpt/api/user/tools/mcp.py b/docsgpt/api/user/tools/mcp.py
index 8fce355a..318d1b91 100644
--- a/docsgpt/api/user/tools/mcp.py
+++ b/docsgpt/api/user/tools/mcp.py
@@ -310,8 +310,14 @@ class MCPServerSave(Resource):
mcp_config = config.copy()
mcp_config["auth_credentials"] = auth_credentials
- if auth_type == "oauth":
- if not config.get("oauth_task_id"):
+ if auth_type == "oauth" and not config.get("oauth_task_id"):
+ # No new handshake: the caller signed in to this server
+ # before, so its stored tokens answer the discovery.
+ try:
+ mcp_tool = MCPTool(config=mcp_config, user_id=user)
+ mcp_tool.discover_tools()
+ actions_metadata = mcp_tool.get_actions_metadata()
+ except Exception:
return make_response(
jsonify(
{
@@ -321,6 +327,7 @@ class MCPServerSave(Resource):
),
400,
)
+ elif auth_type == "oauth":
redis_client = get_redis_instance()
manager = MCPOAuthManager(redis_client)
result = manager.get_oauth_status(
diff --git a/frontend/src/connectors/ConnectWizard.test.tsx b/frontend/src/connectors/ConnectWizard.test.tsx
index 18309962..472163e4 100644
--- a/frontend/src/connectors/ConnectWizard.test.tsx
+++ b/frontend/src/connectors/ConnectWizard.test.tsx
@@ -23,6 +23,15 @@ const service = vi.hoisted(() => ({
}));
vi.mock('../api/services/connectorsService', () => ({ default: service }));
+const mcpApi = vi.hoisted(() => ({
+ testMCPConnection: vi.fn(),
+ saveMCPServer: vi.fn(),
+}));
+vi.mock('../api/services/userService', async (importOriginal) => {
+ const original = await importOriginal<{ default: object }>();
+ return { default: { ...original.default, ...mcpApi } };
+});
+
// The OAuth popup is covered by ConnectorAuth's own tests; here it only has
// to report a finished sign-in.
vi.mock('../components/ConnectorAuth', () => ({
@@ -55,6 +64,9 @@ vi.mock('../components/FilePicker', () => ({
),
}));
+import notificationsReducer, {
+ sseEventReceived,
+} from '../notifications/notificationsSlice';
import connectorsReducer from './connectorsSlice';
import ConnectWizard from './ConnectWizard';
import type { ConnectorDefinition } from './types';
@@ -147,6 +159,7 @@ describe('ConnectWizard', () => {
const store = configureStore({
reducer: {
connectors: connectorsReducer,
+ notifications: notificationsReducer,
preference: (state = { token: null, selectedDocs: [] }) => state,
conversation: (state = {}) => state,
},
@@ -168,6 +181,7 @@ describe('ConnectWizard', () => {
,
);
});
+ return store;
};
const click = async (text: string) => {
@@ -292,4 +306,87 @@ describe('ConnectWizard', () => {
expect(onClose).toHaveBeenCalled();
expect(document.body.textContent).toContain('NEW_CHAT');
});
+
+ it('signs in to an MCP preset with one button and shows its tools', async () => {
+ const notion: ConnectorDefinition = {
+ ...base,
+ key: 'mcp:notion',
+ name: 'Notion',
+ icon: 'notion',
+ auth_kind: 'mcp_oauth',
+ credential_fields: [],
+ tool_templates: ['mcp_tool'],
+ mcp_url: 'https://mcp.notion.com/mcp',
+ oauth_scopes: [],
+ publisher: 'preset',
+ };
+ const popup = { closed: false, close: vi.fn(), location: { href: '' } };
+ const open = vi.spyOn(window, 'open').mockReturnValue(popup as never);
+ mcpApi.testMCPConnection.mockResolvedValue({
+ json: async () => ({ requires_oauth: true, task_id: 'task-1' }),
+ });
+ mcpApi.saveMCPServer.mockResolvedValue({
+ ok: true,
+ json: async () => ({ success: true, id: 'tool-9' }),
+ });
+ service.listConnections.mockResolvedValue({
+ success: true,
+ connections: [
+ { id: 'conn-9', connector_key: 'mcp:notion', updated_at: '2026-09-28' },
+ ],
+ });
+ service.getConnection.mockResolvedValue({
+ success: true,
+ connection: { tools: [{ ...TELEGRAM_TOOL, display_name: 'Notion' }] },
+ });
+ const store = await render(notion);
+ // No server URL or auth form: one sign-in button.
+ expect(document.body.querySelector('input')).toBeNull();
+ await click('settings.connectors.wizard.signIn');
+ // The pop-up opens inside the click, then follows the worker.
+ expect(open).toHaveBeenCalledWith(
+ 'about:blank',
+ 'mcpOAuth',
+ expect.any(String),
+ );
+ expect(document.body.textContent).toContain(
+ 'settings.connectors.wizard.waiting',
+ );
+ await act(async () => {
+ store.dispatch(
+ sseEventReceived({
+ id: 'ev-1',
+ type: 'mcp.oauth.awaiting_redirect',
+ scope: { kind: 'mcp_oauth', id: 'task-1' },
+ payload: { authorization_url: 'https://notion.example/authorize' },
+ }),
+ );
+ });
+ expect(popup.location.href).toBe('https://notion.example/authorize');
+ await act(async () => {
+ store.dispatch(
+ sseEventReceived({
+ id: 'ev-2',
+ type: 'mcp.oauth.completed',
+ scope: { kind: 'mcp_oauth', id: 'task-1' },
+ payload: { tools: [] },
+ }),
+ );
+ });
+ expect(mcpApi.saveMCPServer.mock.calls[0][0]).toMatchObject({
+ displayName: 'Notion',
+ config: {
+ server_url: 'https://mcp.notion.com/mcp',
+ auth_type: 'oauth',
+ oauth_task_id: 'task-1',
+ },
+ });
+ expect(document.body.textContent).toContain(
+ 'settings.connectors.wizard.doneTitle',
+ );
+ expect(document.body.textContent).toContain(
+ 'settings.connectors.wizard.toolsHeading:1',
+ );
+ open.mockRestore();
+ });
});
diff --git a/frontend/src/connectors/ConnectWizard.tsx b/frontend/src/connectors/ConnectWizard.tsx
index b3f648e1..547095f1 100644
--- a/frontend/src/connectors/ConnectWizard.tsx
+++ b/frontend/src/connectors/ConnectWizard.tsx
@@ -6,7 +6,9 @@ import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { envVar } from '@/env';
+import { baseURL } from '../api/client';
import connectorsService from '../api/services/connectorsService';
+import userService from '../api/services/userService';
import { useConnectorAuth } from '../components/ConnectorAuth';
import { FilePicker } from '../components/FilePicker';
import GoogleDrivePicker from '../components/GoogleDrivePicker';
@@ -17,6 +19,7 @@ import {
AccordionTrigger,
} from '../components/ui/accordion';
import { Alert, AlertDescription } from '../components/ui/alert';
+import { Button } from '../components/ui/button';
import { FormField } from '../components/ui/form-field';
import { Input } from '../components/ui/input';
import { Modal, ModalActions } from '../components/ui/modal';
@@ -43,6 +46,7 @@ import CredentialForm, { credentialsComplete } from './CredentialForm';
import { loadConnectors, selectConnections } from './connectorsSlice';
import { connectorDescription, connectorName } from './i18n';
import ToolPermissions from './ToolPermissions';
+import useMcpOAuth, { type McpOAuthConfig } from './useMcpOAuth';
import type { ConnectionTool, ConnectorDefinition } from './types';
export type WizardMode = 'connect' | 'reconnect' | 'sync' | 'done';
@@ -66,12 +70,15 @@ export default function ConnectWizard({
connector,
mode = 'connect',
connectionId: initialConnectionId = null,
+ mcpToolId,
onClose,
onFinished,
}: {
connector: ConnectorDefinition;
mode?: WizardMode;
connectionId?: string | null;
+ /** Reconnecting an MCP preset: the tool to update rather than add. */
+ mcpToolId?: string;
onClose: () => void;
onFinished?: () => void;
}) {
@@ -135,6 +142,73 @@ export default function ConnectWizard({
setStep(canSync ? 'setup' : 'done');
};
+ // MCP presets (Notion, Linear…) sign in over MCP OAuth: no URL or auth
+ // form, just "Sign in to Notion". The tool is saved on success, and the
+ // done step shows what it can do.
+ const isMcpPreset =
+ connector.auth_kind === 'mcp_oauth' && !!connector.mcp_url;
+ const mcp = useMcpOAuth();
+ const mcpConfig = (): McpOAuthConfig => ({
+ server_url: connector.mcp_url ?? '',
+ auth_type: 'oauth',
+ oauth_scopes: connector.oauth_scopes ?? [],
+ timeout: 30,
+ redirect_uri: `${baseURL.replace(/\/$/, '')}/api/mcp_server/callback`,
+ });
+
+ const saveMcp = async (config: McpOAuthConfig, taskId: string | null) => {
+ setPending(true);
+ try {
+ const response = await userService.saveMCPServer(
+ {
+ displayName: name,
+ config: { ...config, oauth_task_id: taskId ?? '' },
+ status: true,
+ ...(mcpToolId && { id: mcpToolId }),
+ },
+ token,
+ );
+ const result = await response.json();
+ if (!response.ok || !result.success) throw new Error(result.error);
+ const list = await connectorsService.listConnections(token);
+ const saved = (
+ (list?.connections ?? []) as {
+ id: string;
+ connector_key: string;
+ updated_at: string | null;
+ }[]
+ )
+ .filter((c) => c.connector_key === connector.key)
+ .sort((a, b) =>
+ (b.updated_at ?? '').localeCompare(a.updated_at ?? ''),
+ )[0];
+ refresh();
+ if (saved) {
+ setConnectionId(saved.id);
+ const detail = await connectorsService.getConnection(saved.id, token);
+ if (detail?.success) setTools(detail.connection.tools ?? []);
+ }
+ setStep('done');
+ } catch (err) {
+ setError(
+ (err instanceof Error && err.message) ||
+ t('settings.tools.mcp.errors.saveFailed'),
+ );
+ } finally {
+ setPending(false);
+ }
+ };
+
+ const startMcpSignIn = () => {
+ setError('');
+ const config = mcpConfig();
+ mcp.start(config, {
+ onDone: ({ taskId }) => saveMcp(config, taskId),
+ onError: (message) =>
+ setError(message || t('settings.tools.mcp.errors.oauthFailed')),
+ });
+ };
+
const startSignIn = useConnectorAuth({
provider: connector.key,
connectionId:
@@ -300,7 +374,25 @@ export default function ConnectWizard({
{error}
)}
- {connector.auth_kind === 'oauth' ? null : (
+ {isMcpPreset && mcp.pending && (
+
+ {t('settings.connectors.wizard.waiting', {
+ name,
+ interpolation: { escapeValue: false },
+ })}
+
+ )}
+ {isMcpPreset && mcp.blockedUrl && (
+
+ )}
+ {connector.auth_kind === 'oauth' || isMcpPreset ? null : (
{
+ mcp.cancel();
+ onClose();
+ }}
submitLabel={t('settings.connectors.wizard.signIn', {
name,
interpolation: { escapeValue: false },
})}
- onSubmit={startSignIn}
+ onSubmit={isMcpPreset ? startMcpSignIn : startSignIn}
+ pending={isMcpPreset && (mcp.pending || pending)}
/>
) : (
+ connector.auth_kind === 'mcp_oauth' && !!connector.mcp_url;
+
+/** Connectors whose sign-in the wizard redoes in place, with no form. */
+export const reconnectsInPlace = (connector: ConnectorDefinition) =>
+ connector.auth_kind === 'oauth' ||
+ connector.auth_kind === 'api_key' ||
+ isMcpPreset(connector);
diff --git a/frontend/src/connectors/useConnectorLauncher.tsx b/frontend/src/connectors/useConnectorLauncher.tsx
index 696bf86e..f461a12b 100644
--- a/frontend/src/connectors/useConnectorLauncher.tsx
+++ b/frontend/src/connectors/useConnectorLauncher.tsx
@@ -13,6 +13,7 @@ import { selectToken } from '../preferences/preferenceSlice';
import type { AppDispatch } from '../store';
import ConnectWizard, { type WizardMode } from './ConnectWizard';
import { loadConnectors } from './connectorsSlice';
+import { isMcpPreset } from './launchRules';
import type { ConnectorDefinition } from './types';
export type LaunchOptions = {
@@ -29,6 +30,7 @@ type Active =
connector: ConnectorDefinition;
mode: WizardMode;
connectionId?: string;
+ mcpToolId?: string;
}
| {
kind: 'mcp';
@@ -61,6 +63,19 @@ export default function useConnectorLauncher({
const launch = useCallback(
async (connector: ConnectorDefinition, options: LaunchOptions = {}) => {
+ if (isMcpPreset(connector)) {
+ setActive({
+ kind: 'wizard',
+ connector,
+ mode: options.mode === 'reconnect' ? 'reconnect' : 'connect',
+ connectionId: options.connectionId,
+ mcpToolId:
+ typeof options.mcpServer?.id === 'string'
+ ? options.mcpServer.id
+ : undefined,
+ });
+ return;
+ }
if (isMcp(connector)) {
setActive({
kind: 'mcp',
@@ -158,6 +173,7 @@ export default function useConnectorLauncher({
connector={active.connector}
mode={active.mode}
connectionId={active.connectionId}
+ mcpToolId={active.mcpToolId}
onClose={() => {
setActive(null);
refresh();
diff --git a/frontend/src/connectors/useMcpOAuth.ts b/frontend/src/connectors/useMcpOAuth.ts
new file mode 100644
index 00000000..705ef9ce
--- /dev/null
+++ b/frontend/src/connectors/useMcpOAuth.ts
@@ -0,0 +1,125 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { useSelector } from 'react-redux';
+
+import userService from '../api/services/userService';
+import { selectRecentEvents } from '../notifications/notificationsSlice';
+import { selectToken } from '../preferences/preferenceSlice';
+
+export type McpOAuthConfig = {
+ server_url: string;
+ auth_type: 'oauth';
+ oauth_scopes: string[];
+ timeout: number;
+ redirect_uri: string;
+};
+
+type Handlers = {
+ /** Signed in: `taskId` is the OAuth task to save with (null when the
+ * server already had a valid sign-in). */
+ onDone: (result: { taskId: string | null }) => void;
+ onError: (message: string) => void;
+};
+
+/**
+ * Sign in to an MCP server over OAuth from a click. The pop-up opens blank
+ * inside the click (so the browser allows it) and follows the worker's
+ * `mcp.oauth.*` events: pointed at the provider on `awaiting_redirect`,
+ * closed on `completed` or `failed`. `blockedUrl` is set when the browser
+ * blocked the pop-up anyway, so the caller can offer the link.
+ */
+export default function useMcpOAuth() {
+ const token = useSelector(selectToken);
+ const events = useSelector(selectRecentEvents);
+ const [taskId, setTaskId] = useState(null);
+ const [pending, setPending] = useState(false);
+ const [blockedUrl, setBlockedUrl] = useState(null);
+ const popupRef = useRef(null);
+ const handlersRef = useRef(null);
+ const handledRef = useRef>(new Set());
+
+ const closePopup = () => {
+ if (popupRef.current && !popupRef.current.closed) popupRef.current.close();
+ popupRef.current = null;
+ };
+
+ const finish = useCallback(() => {
+ closePopup();
+ setTaskId(null);
+ setPending(false);
+ handlersRef.current = null;
+ }, []);
+
+ useEffect(() => finish, [finish]);
+
+ const start = useCallback(
+ async (config: McpOAuthConfig, handlers: Handlers) => {
+ handlersRef.current = handlers;
+ handledRef.current = new Set();
+ setBlockedUrl(null);
+ setPending(true);
+ closePopup();
+ popupRef.current = window.open(
+ 'about:blank',
+ 'mcpOAuth',
+ 'width=600,height=700',
+ );
+ try {
+ const response = await userService.testMCPConnection({ config }, token);
+ const result = await response.json();
+ if (result.requires_oauth && result.task_id) {
+ setTaskId(result.task_id);
+ return;
+ }
+ const done = handlersRef.current;
+ finish();
+ if (result.success) done?.onDone({ taskId: null });
+ else done?.onError(result.message || result.error || '');
+ } catch {
+ const done = handlersRef.current;
+ finish();
+ done?.onError('');
+ }
+ },
+ [token, finish],
+ );
+
+ useEffect(() => {
+ if (!taskId) return;
+ // Newest first in the slice; walk oldest first so a redirect buffered
+ // before its completion still opens the provider.
+ for (let i = events.length - 1; i >= 0; i--) {
+ const event = events[i];
+ if (event.scope?.id !== taskId || !event.id) continue;
+ if (handledRef.current.has(event.id)) continue;
+ handledRef.current.add(event.id);
+ const payload = (event.payload ?? {}) as Record;
+ if (event.type === 'mcp.oauth.awaiting_redirect') {
+ const url = payload.authorization_url as string | undefined;
+ if (!url) continue;
+ if (popupRef.current && !popupRef.current.closed) {
+ popupRef.current.location.href = url;
+ } else {
+ popupRef.current = window.open(
+ url,
+ 'mcpOAuth',
+ 'width=600,height=700',
+ );
+ if (!popupRef.current) setBlockedUrl(url);
+ }
+ } else if (event.type === 'mcp.oauth.completed') {
+ const done = handlersRef.current;
+ const id = taskId;
+ finish();
+ done?.onDone({ taskId: id });
+ return;
+ } else if (event.type === 'mcp.oauth.failed') {
+ const done = handlersRef.current;
+ finish();
+ done?.onError(String(payload.error ?? ''));
+ return;
+ }
+ }
+ }, [events, taskId, finish]);
+
+ return { start, cancel: finish, pending, blockedUrl };
+}
diff --git a/frontend/src/conversation/ConnectToolCallBar.tsx b/frontend/src/conversation/ConnectToolCallBar.tsx
index eb12bd0e..f014c1bb 100644
--- a/frontend/src/conversation/ConnectToolCallBar.tsx
+++ b/frontend/src/conversation/ConnectToolCallBar.tsx
@@ -12,6 +12,7 @@ import {
selectConnectorCatalog,
selectConnectorsLoaded,
} from '../connectors/connectorsSlice';
+import { reconnectsInPlace } from '../connectors/launchRules';
import useConnectorLauncher from '../connectors/useConnectorLauncher';
import { selectToken } from '../preferences/preferenceSlice';
import type { AppDispatch } from '../store';
@@ -59,13 +60,13 @@ export default function ConnectToolCallBar({
const connect = () => {
// No account yet: connect one. The caller's own account that needs
// signing in again is reconnected right here (every tool and source on
- // it heals). MCP servers and anyone else's account go to the drawer.
+ // it heals). Custom MCP servers and anyone else's account go to the
+ // drawer.
if (connector && required?.status === 'missing') {
launch(connector);
return;
}
- const inPlace =
- connector?.auth_kind === 'oauth' || connector?.auth_kind === 'api_key';
+ const inPlace = !!connector && reconnectsInPlace(connector);
if (connector && required?.connection_id && inPlace) {
launch(connector, {
mode: 'reconnect',
diff --git a/frontend/src/locale/de.json b/frontend/src/locale/de.json
index 620e0348..d5523c49 100644
--- a/frontend/src/locale/de.json
+++ b/frontend/src/locale/de.json
@@ -1240,7 +1240,9 @@
"connectedWithKey": "Verbunden mit dem Schlüssel, der auf {{hint}} endet.",
"doneCounts": "{{sources}} werden synchronisiert, {{tools}} bereit.",
"doneSources": "{{sources}} werden synchronisiert.",
- "doneTools": "{{tools}} bereit."
+ "doneTools": "{{tools}} bereit.",
+ "waiting": "Schließe die Anmeldung bei {{name}} im geöffneten Fenster ab.",
+ "openSignIn": "Anmeldung bei {{name}} öffnen"
},
"remove": {
"title": "{{name}} entfernen?",
diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json
index ce555adf..75759a9f 100644
--- a/frontend/src/locale/en.json
+++ b/frontend/src/locale/en.json
@@ -1246,7 +1246,9 @@
"connectedWithKey": "Connected with the key ending {{hint}}.",
"doneCounts": "{{sources}} syncing, {{tools}} ready.",
"doneSources": "{{sources}} syncing.",
- "doneTools": "{{tools}} ready."
+ "doneTools": "{{tools}} ready.",
+ "waiting": "Finish signing in to {{name}} in the window that opened.",
+ "openSignIn": "Open the {{name}} sign-in"
},
"remove": {
"title": "Remove {{name}}?",
diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json
index a96888e4..0954f5f1 100644
--- a/frontend/src/locale/es.json
+++ b/frontend/src/locale/es.json
@@ -1240,7 +1240,9 @@
"connectedWithKey": "Conectado con la clave que termina en {{hint}}.",
"doneCounts": "{{sources}} sincronizando, {{tools}} listas.",
"doneSources": "{{sources}} sincronizando.",
- "doneTools": "{{tools}} listas."
+ "doneTools": "{{tools}} listas.",
+ "waiting": "Termina de iniciar sesión en {{name}} en la ventana que se abrió.",
+ "openSignIn": "Abrir el inicio de sesión de {{name}}"
},
"remove": {
"title": "¿Eliminar {{name}}?",
diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json
index a6123e25..95ff6834 100644
--- a/frontend/src/locale/jp.json
+++ b/frontend/src/locale/jp.json
@@ -1228,7 +1228,9 @@
"connectedWithKey": "末尾が {{hint}} のキーで接続しました。",
"doneCounts": "{{sources}}を同期中、{{tools}}を利用できます。",
"doneSources": "{{sources}}を同期中です。",
- "doneTools": "{{tools}}を利用できます。"
+ "doneTools": "{{tools}}を利用できます。",
+ "waiting": "開いたウィンドウで {{name}} へのサインインを完了してください。",
+ "openSignIn": "{{name}} のサインインを開く"
},
"remove": {
"title": "{{name}} を削除しますか?",
diff --git a/frontend/src/locale/ru.json b/frontend/src/locale/ru.json
index 56767fd3..09dc9661 100644
--- a/frontend/src/locale/ru.json
+++ b/frontend/src/locale/ru.json
@@ -1302,7 +1302,9 @@
"connectedWithKey": "Подключено ключом, оканчивающимся на {{hint}}.",
"doneCounts": "Синхронизируется: {{sources}}, готово: {{tools}}.",
"doneSources": "Синхронизируется: {{sources}}.",
- "doneTools": "Готово: {{tools}}."
+ "doneTools": "Готово: {{tools}}.",
+ "waiting": "Завершите вход в {{name}} в открывшемся окне.",
+ "openSignIn": "Открыть вход в {{name}}"
},
"remove": {
"title": "Удалить {{name}}?",
diff --git a/frontend/src/locale/zh-TW.json b/frontend/src/locale/zh-TW.json
index ef07510b..0cc3bfde 100644
--- a/frontend/src/locale/zh-TW.json
+++ b/frontend/src/locale/zh-TW.json
@@ -1228,7 +1228,9 @@
"connectedWithKey": "已使用結尾為 {{hint}} 的金鑰連線。",
"doneCounts": "{{sources}}同步中,{{tools}}已就緒。",
"doneSources": "{{sources}}同步中。",
- "doneTools": "{{tools}}已就緒。"
+ "doneTools": "{{tools}}已就緒。",
+ "waiting": "請在開啟的視窗中完成 {{name}} 登入。",
+ "openSignIn": "開啟 {{name}} 登入"
},
"remove": {
"title": "要移除 {{name}} 嗎?",
diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json
index 68a9173b..b9191446 100644
--- a/frontend/src/locale/zh.json
+++ b/frontend/src/locale/zh.json
@@ -1228,7 +1228,9 @@
"connectedWithKey": "已使用以 {{hint}} 结尾的密钥连接。",
"doneCounts": "{{sources}}正在同步,{{tools}}已就绪。",
"doneSources": "{{sources}}正在同步。",
- "doneTools": "{{tools}}已就绪。"
+ "doneTools": "{{tools}}已就绪。",
+ "waiting": "请在打开的窗口中完成 {{name}} 登录。",
+ "openSignIn": "打开 {{name}} 登录"
},
"remove": {
"title": "移除 {{name}}?",
diff --git a/frontend/src/modals/MCPServerModal.tsx b/frontend/src/modals/MCPServerModal.tsx
index d8a38ba8..d0c150a1 100644
--- a/frontend/src/modals/MCPServerModal.tsx
+++ b/frontend/src/modals/MCPServerModal.tsx
@@ -94,6 +94,7 @@ export default function MCPServerModal({
const popupOpenedRef = useRef(false);
const [oauthCompleted, setOAuthCompleted] = useState(false);
const [saveActive, setSaveActive] = useState(false);
+ const [showAdvanced, setShowAdvanced] = useState(false);
const cleanupOAuthListener = useCallback(() => {
setOauthTaskId(null);
@@ -526,19 +527,6 @@ export default function MCPServerModal({
);
- case 'oauth':
- return (
-
-
- handleInputChange('oauth_scopes', e.target.value)
- }
- placeholder="read, write"
- />
-
- );
default:
return null;
}
@@ -660,29 +648,59 @@ export default function MCPServerModal({
{renderAuthFields()}
-
+ {formData.auth_type === 'oauth' && (
+
+
+ handleInputChange('oauth_scopes', e.target.value)
+ }
+ placeholder="read, write"
+ />
+
+ )}
+
+ {
+ const value = e.target.value;
+ if (value === '') {
+ handleInputChange('timeout', '');
+ } else {
+ const numValue = parseInt(value);
+ if (!isNaN(numValue) && numValue >= 1) {
+ handleInputChange('timeout', numValue);
+ }
+ }
+ }}
+ placeholder="30"
+ min={1}
+ max={300}
+ />
+
+ >
+ ) : null}
+
+ {showAdvanced
+ ? t('modals.uploadDoc.hideAdvanced')
+ : t('modals.uploadDoc.showAdvanced')}
+
{testResult && (
diff --git a/frontend/src/notifications/ConnectionHealthToast.tsx b/frontend/src/notifications/ConnectionHealthToast.tsx
index 236509e0..2fa05ff7 100644
--- a/frontend/src/notifications/ConnectionHealthToast.tsx
+++ b/frontend/src/notifications/ConnectionHealthToast.tsx
@@ -18,6 +18,7 @@ import {
selectConnectorCatalog,
selectConnectorsLoaded,
} from '../connectors/connectorsSlice';
+import { reconnectsInPlace } from '../connectors/launchRules';
import useConnectorLauncher from '../connectors/useConnectorLauncher';
import { formatCount } from '../utils/dateTimeUtils';
import { selectToken } from '../preferences/preferenceSlice';
@@ -106,8 +107,7 @@ export default function ConnectionHealthToast() {
const reconnect = (payload: Record) => {
const connectionId = String(payload.connection_id ?? '');
const connector = catalog.find((c) => c.key === payload.connector_key);
- const inPlace =
- connector?.auth_kind === 'oauth' || connector?.auth_kind === 'api_key';
+ const inPlace = !!connector && reconnectsInPlace(connector);
if (connector && connectionId && inPlace) {
launch(connector, { mode: 'reconnect', connectionId });
return true;
@@ -137,7 +137,7 @@ export default function ConnectionHealthToast() {
- {/* Sign in again right here; MCP servers reconnect from the
+ {/* Sign in again right here; custom MCP servers reconnect from the
connector's drawer. The toast stays until it works. */}