mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 23:07:14 +00:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user