Skip to content

Commit 3bc4dd2

Browse files
committed
error messages improvements
Signed-off-by: Yi Cai <yicai@redhat.com>
1 parent 7910abf commit 3bc4dd2

2 files changed

Lines changed: 163 additions & 21 deletions

File tree

workspaces/lightspeed/plugins/lightspeed-backend/src/service/mcp-server-validator.ts

Lines changed: 80 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,80 @@ import { McpValidationResult } from './mcp-server-types';
2020

2121
const REQUEST_TIMEOUT_MS = 10_000;
2222

23+
const getEndpointLabel = (targetUrl: string): string => {
24+
try {
25+
const parsed = new URL(targetUrl);
26+
return parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname;
27+
} catch {
28+
return targetUrl;
29+
}
30+
};
31+
32+
const getNestedError = (error: unknown): Error | undefined => {
33+
if (
34+
error &&
35+
typeof error === 'object' &&
36+
'cause' in error &&
37+
(error as { cause?: unknown }).cause instanceof Error
38+
) {
39+
return (error as { cause: Error }).cause;
40+
}
41+
return undefined;
42+
};
43+
44+
const getNetworkErrorMessage = (url: string, error: unknown): string => {
45+
const endpoint = getEndpointLabel(url);
46+
const nestedError = getNestedError(error);
47+
const fullMessage = [
48+
error instanceof Error ? error.message : '',
49+
nestedError?.name ?? '',
50+
nestedError?.message ?? '',
51+
]
52+
.filter(Boolean)
53+
.join(' ')
54+
.toLowerCase();
55+
56+
if (
57+
fullMessage.includes('timeout') ||
58+
fullMessage.includes('aborterror') ||
59+
fullMessage.includes('aborted')
60+
) {
61+
return `Connection timed out while contacting ${endpoint}`;
62+
}
63+
if (
64+
fullMessage.includes('econnrefused') ||
65+
fullMessage.includes('connection refused')
66+
) {
67+
return `Connection refused by ${endpoint}`;
68+
}
69+
if (
70+
fullMessage.includes('enotfound') ||
71+
fullMessage.includes('getaddrinfo')
72+
) {
73+
return `Host not found for ${endpoint}`;
74+
}
75+
if (
76+
fullMessage.includes('econnreset') ||
77+
fullMessage.includes('socket hang up')
78+
) {
79+
return `Connection reset by ${endpoint}`;
80+
}
81+
if (
82+
fullMessage.includes('ehostunreach') ||
83+
fullMessage.includes('enetunreach')
84+
) {
85+
return `Host unreachable: ${endpoint}`;
86+
}
87+
if (fullMessage.includes('fetch failed')) {
88+
return `Unable to connect to ${endpoint}`;
89+
}
90+
91+
return (
92+
nestedError?.message ||
93+
(error instanceof Error ? error.message : String(error))
94+
);
95+
};
96+
2397
/**
2498
* Validates MCP server credentials using the Streamable HTTP transport.
2599
*
@@ -32,9 +106,13 @@ export class McpServerValidator {
32106
constructor(private readonly logger: LoggerService) {}
33107

34108
async validate(url: string, token: string): Promise<McpValidationResult> {
109+
const trimmedToken = token.trim();
110+
const authorizationHeader = /^Bearer\s+/i.test(trimmedToken)
111+
? trimmedToken
112+
: `Bearer ${trimmedToken}`;
35113
const headers: Record<string, string> = {
36114
'Content-Type': 'application/json',
37-
Authorization: `${token}`,
115+
Authorization: authorizationHeader,
38116
Accept: 'application/json, text/event-stream',
39117
};
40118

@@ -146,20 +224,7 @@ export class McpServerValidator {
146224
);
147225
return { valid: true, toolCount: 0, tools: [] };
148226
} catch (error: unknown) {
149-
const message = error instanceof Error ? error.message : String(error);
150-
151-
if (
152-
message.includes('TimeoutError') ||
153-
message.includes('AbortError') ||
154-
message.includes('abort')
155-
) {
156-
return {
157-
valid: false,
158-
toolCount: 0,
159-
tools: [],
160-
error: 'Connection timed out',
161-
};
162-
}
227+
const message = getNetworkErrorMessage(url, error);
163228

164229
this.logger.error(`MCP validation failed for ${url}: ${message}`);
165230
return {

workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type McpServer = {
4343
toolCount: number;
4444
hasToken: boolean;
4545
hasUserToken: boolean;
46+
validationError?: string;
4647
};
4748

4849
type McpServersSettingsProps = {
@@ -185,6 +186,18 @@ type McpServersListResponse = {
185186

186187
type McpServersPatchResponse = {
187188
server?: McpServerResponse;
189+
validation?: {
190+
error?: string;
191+
};
192+
};
193+
194+
type McpServersValidateResponse = {
195+
name: string;
196+
status: 'connected' | 'error' | 'unknown';
197+
toolCount: number;
198+
validation?: {
199+
error?: string;
200+
};
188201
};
189202

190203
const getStatusIcon = (status: ServerStatus, className: string) => {
@@ -217,14 +230,18 @@ const getDisplayDetail = (
217230
return 'Unknown';
218231
};
219232

220-
const toUiServer = (server: McpServerResponse): McpServer => ({
233+
const toUiServer = (
234+
server: McpServerResponse,
235+
validationError?: string,
236+
): McpServer => ({
221237
id: server.name,
222238
name: server.name,
223239
enabled: server.enabled,
224240
status: server.status,
225241
toolCount: server.toolCount,
226242
hasToken: server.hasToken,
227243
hasUserToken: server.hasUserToken,
244+
validationError: server.status === 'error' ? validationError : undefined,
228245
});
229246

230247
export const McpServersSettings = ({ onClose }: McpServersSettingsProps) => {
@@ -271,6 +288,35 @@ export const McpServersSettings = ({ onClose }: McpServersSettingsProps) => {
271288
[fetchApi],
272289
);
273290

291+
const validateServer = useCallback(
292+
async (serverName: string) => {
293+
const baseUrl = getBaseUrl();
294+
const data = await fetchJson<McpServersValidateResponse>(
295+
`${baseUrl}/mcp-servers/${encodeURIComponent(serverName)}/validate`,
296+
{
297+
method: 'POST',
298+
},
299+
);
300+
301+
setServers(prev =>
302+
prev.map(server =>
303+
server.name === serverName
304+
? {
305+
...server,
306+
status: data.status,
307+
toolCount: data.toolCount,
308+
validationError:
309+
data.status === 'error'
310+
? (data.validation?.error ?? 'Validation failed')
311+
: undefined,
312+
}
313+
: server,
314+
),
315+
);
316+
},
317+
[fetchJson, getBaseUrl],
318+
);
319+
274320
const loadServers = useCallback(async () => {
275321
setIsLoading(true);
276322
setError(null);
@@ -279,15 +325,33 @@ export const McpServersSettings = ({ onClose }: McpServersSettingsProps) => {
279325
const data = await fetchJson<McpServersListResponse>(
280326
`${baseUrl}/mcp-servers`,
281327
);
282-
setServers((data.servers ?? []).map(toUiServer));
328+
const uiServers = (data.servers ?? []).map(server => toUiServer(server));
329+
setServers(uiServers);
330+
331+
const serversToValidate = uiServers.filter(server => server.hasToken);
332+
void Promise.allSettled(
333+
serversToValidate.map(async server => {
334+
try {
335+
await validateServer(server.name);
336+
} catch (validationError) {
337+
setError(
338+
prev =>
339+
prev ??
340+
(validationError instanceof Error
341+
? validationError.message
342+
: `Failed to validate ${server.name}`),
343+
);
344+
}
345+
}),
346+
);
283347
} catch (e) {
284348
setError(
285349
e instanceof Error ? e.message : 'Failed to load MCP server settings',
286350
);
287351
} finally {
288352
setIsLoading(false);
289353
}
290-
}, [fetchJson, getBaseUrl]);
354+
}, [fetchJson, getBaseUrl, validateServer]);
291355

292356
useEffect(() => {
293357
loadServers();
@@ -313,7 +377,9 @@ export const McpServersSettings = ({ onClose }: McpServersSettingsProps) => {
313377
if (data.server) {
314378
setServers(prev =>
315379
prev.map(server =>
316-
server.name === serverName ? toUiServer(data.server!) : server,
380+
server.name === serverName
381+
? toUiServer(data.server!, data.validation?.error)
382+
: server,
317383
),
318384
);
319385
} else {
@@ -333,7 +399,13 @@ export const McpServersSettings = ({ onClose }: McpServersSettingsProps) => {
333399
);
334400

335401
const selectedCount = useMemo(
336-
() => servers.filter(server => server.enabled).length,
402+
() =>
403+
servers.filter(server => {
404+
const displayStatus = getDisplayStatus(server);
405+
const isUnavailable =
406+
displayStatus === 'failed' || displayStatus === 'tokenRequired';
407+
return server.enabled && !isUnavailable;
408+
}).length,
337409
[servers],
338410
);
339411

@@ -456,7 +528,12 @@ export const McpServersSettings = ({ onClose }: McpServersSettingsProps) => {
456528
<div className={classes.statusCell}>
457529
{getStatusIcon(displayStatus, statusClass)}
458530
{displayStatus === 'failed' ? (
459-
<Tooltip content="Token authentication failed, click edit to configure it again">
531+
<Tooltip
532+
content={
533+
server.validationError ??
534+
'Validation failed. Check server URL and token.'
535+
}
536+
>
460537
<Typography
461538
component="span"
462539
className={classes.statusValue}

0 commit comments

Comments
 (0)