feat(mcp): detect OAuth-protected remote MCP servers (#2363)

* feat(mcp): surface OAuth-required server tests

* fix(mcp): show connection failure details in status cards

---------

Co-authored-by: RockChinQ <rockchinq@gmail.com>
This commit is contained in:
huige66631
2026-09-09 16:13:33 +08:00
committed by GitHub
parent 485113ae43
commit ce6b647fe7
18 changed files with 433 additions and 37 deletions
@@ -8,7 +8,14 @@ import React, {
} from 'react';
import { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next';
import { Braces, Loader2, Trash2, Wrench, XCircle } from 'lucide-react';
import {
Braces,
Loader2,
ShieldAlert,
Trash2,
Wrench,
XCircle,
} from 'lucide-react';
import { Resolver, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
@@ -101,7 +108,7 @@ function StatusDisplay({
<div className="space-y-1">
<div className="flex items-center gap-2 text-red-600">
<XCircle className="size-5" />
<span className="font-medium">{t('mcp.connectionFailed')}</span>
<span className="font-medium">{t('mcp.connectionFailedStatus')}</span>
</div>
<div className="pl-7 text-sm text-red-500 space-y-0.5">
<div>
@@ -117,15 +124,41 @@ function StatusDisplay({
);
}
if (runtimeInfo.error_phase === 'oauth_required') {
return (
<div className="space-y-1">
<div className="flex items-center gap-2 text-amber-700 dark:text-amber-400">
<ShieldAlert className="size-5" />
<span className="font-medium">
{t('mcp.oauthAuthorizationRequired')}
</span>
</div>
<div className="pl-7 text-sm text-muted-foreground">
{t('mcp.oauthAuthorizationRequiredSuggestion')}
</div>
</div>
);
}
const httpStatus = runtimeInfo.error_code?.match(/^http_(\d{3})$/)?.[1];
const errorDetail =
runtimeInfo.error_code === 'connection_unreachable'
? t('mcp.connectionUnreachable')
: runtimeInfo.error_code === 'connection_timeout'
? t('mcp.connectionTimeout')
: httpStatus
? t('mcp.connectionHttpError', { status: httpStatus })
: runtimeInfo.error_message || t('mcp.unknownError');
return (
<div className="space-y-1">
<div className="flex items-center gap-2 text-red-600">
<XCircle className="size-5" />
<span className="font-medium">{t('mcp.connectionFailed')}</span>
<span className="font-medium">{t('mcp.connectionFailedStatus')}</span>
</div>
{runtimeInfo.error_message && (
<div className="pl-7 text-sm text-red-500">
{runtimeInfo.error_message}
{errorDetail && (
<div className="pl-7 whitespace-pre-wrap break-words text-sm text-muted-foreground">
{errorDetail}
</div>
)}
</div>
@@ -835,15 +868,31 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
async function testMcp() {
setMcpTesting(true);
const showConnectionFailure = (
message: string,
info?: MCPServerRuntimeInfo,
) => {
toast.error(t('mcp.connectionFailedStatus'));
setRuntimeInfo({
tool_count: 0,
tools: [],
resource_count: 0,
resources: [],
...info,
status: MCPSessionStatus.ERROR,
error_message: info?.error_message || message,
});
};
try {
const mode = form.getValues('mode');
if (mode === 'stdio' && !mcpStdioEnabled) {
toast.error(t('mcp.stdioDisabledByPolicy'));
showConnectionFailure(t('mcp.stdioDisabledByPolicy'));
setMcpTesting(false);
return;
}
if (mode === 'stdio' && !boxAvailable) {
toast.error(t('mcp.stdioBlockedByBoxToast'));
showConnectionFailure(t('mcp.stdioBlockedByBoxToast'));
setMcpTesting(false);
return;
}
@@ -914,15 +963,9 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
if (taskResp.runtime.exception) {
const errorMsg =
taskResp.runtime.exception || t('mcp.unknownError');
toast.error(`${t('mcp.testError')}: ${errorMsg}`);
setRuntimeInfo({
status: MCPSessionStatus.ERROR,
error_message: errorMsg,
tool_count: 0,
tools: [],
resource_count: 0,
resources: [],
});
const runtimeInfoFromTest = taskResp.task_context?.metadata
?.runtime_info as MCPServerRuntimeInfo | undefined;
showConnectionFailure(errorMsg, runtimeInfoFromTest);
if (shouldTestPersistedServer) {
await onPersistedTestComplete?.(serverName);
}
@@ -949,14 +992,19 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
clearInterval(interval);
setMcpTesting(false);
const errorMsg =
(err as CustomApiError).msg || t('mcp.getTaskFailed');
toast.error(`${t('mcp.testError')}: ${errorMsg}`);
(err as CustomApiError).msg ||
(err as Error).message ||
t('mcp.getTaskFailed');
showConnectionFailure(errorMsg);
}
}, 1000);
} catch (err) {
setMcpTesting(false);
const errorMsg = (err as Error).message || t('mcp.unknownError');
toast.error(`${t('mcp.testError')}: ${errorMsg}`);
const errorMsg =
(err as CustomApiError).msg ||
(err as Error).message ||
t('mcp.unknownError');
showConnectionFailure(errorMsg);
}
}
+1
View File
@@ -586,6 +586,7 @@ export enum MCPSessionStatus {
}
export interface MCPServerRuntimeInfo {
error_code?: string;
status: MCPSessionStatus;
error_message?: string;
/** Stage at which the session failed. Frontends key off this to render