Improve error handling for MCP transport failures
Browse filesEnhances error reporting by preferring the original HTTP transport error when both HTTP and SSE transports fail, ensuring the UI displays the primary failure reason. Updates ServerCard to better display long error messages.
src/lib/components/mcp/ServerCard.svelte
CHANGED
|
@@ -139,7 +139,7 @@
|
|
| 139 |
{#if server.errorMessage}
|
| 140 |
<div class="mb-2 flex items-center gap-2">
|
| 141 |
<div
|
| 142 |
-
class="rounded bg-red-50 px-2 py-1 text-xs text-red-800 dark:bg-red-900/20 dark:text-red-200"
|
| 143 |
>
|
| 144 |
{server.errorMessage}
|
| 145 |
</div>
|
|
|
|
| 139 |
{#if server.errorMessage}
|
| 140 |
<div class="mb-2 flex items-center gap-2">
|
| 141 |
<div
|
| 142 |
+
class="line-clamp-6 break-words rounded bg-red-50 px-2 py-1 text-xs text-red-800 dark:bg-red-900/20 dark:text-red-200"
|
| 143 |
>
|
| 144 |
{server.errorMessage}
|
| 145 |
</div>
|
src/lib/server/mcp/clientPool.ts
CHANGED
|
@@ -18,19 +18,32 @@ export async function getClient(server: McpServerConfig, signal?: AbortSignal):
|
|
| 18 |
const existing = pool.get(key);
|
| 19 |
if (existing) return existing;
|
| 20 |
|
|
|
|
| 21 |
const client = new Client({ name: "chat-ui-mcp", version: "0.1.0" });
|
| 22 |
const url = new URL(server.url);
|
| 23 |
const requestInit: RequestInit = { headers: server.headers, signal };
|
| 24 |
try {
|
| 25 |
try {
|
| 26 |
await client.connect(new StreamableHTTPClientTransport(url, { requestInit }));
|
| 27 |
-
} catch {
|
|
|
|
|
|
|
|
|
|
| 28 |
await client.connect(new SSEClientTransport(url, { requestInit }));
|
| 29 |
}
|
| 30 |
} catch (err) {
|
| 31 |
try {
|
| 32 |
await client.close?.();
|
| 33 |
} catch {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
throw err;
|
| 35 |
}
|
| 36 |
|
|
|
|
| 18 |
const existing = pool.get(key);
|
| 19 |
if (existing) return existing;
|
| 20 |
|
| 21 |
+
let firstError: unknown;
|
| 22 |
const client = new Client({ name: "chat-ui-mcp", version: "0.1.0" });
|
| 23 |
const url = new URL(server.url);
|
| 24 |
const requestInit: RequestInit = { headers: server.headers, signal };
|
| 25 |
try {
|
| 26 |
try {
|
| 27 |
await client.connect(new StreamableHTTPClientTransport(url, { requestInit }));
|
| 28 |
+
} catch (httpErr) {
|
| 29 |
+
// Remember the original HTTP transport error so we can surface it if the fallback also fails.
|
| 30 |
+
// Today we always show the SSE message, which is misleading when the real failure was HTTP (e.g. 500).
|
| 31 |
+
firstError = httpErr;
|
| 32 |
await client.connect(new SSEClientTransport(url, { requestInit }));
|
| 33 |
}
|
| 34 |
} catch (err) {
|
| 35 |
try {
|
| 36 |
await client.close?.();
|
| 37 |
} catch {}
|
| 38 |
+
// Prefer the HTTP error if both transports fail; otherwise fall back to the last error.
|
| 39 |
+
if (firstError) {
|
| 40 |
+
const message =
|
| 41 |
+
"HTTP transport failed: " +
|
| 42 |
+
String(firstError instanceof Error ? firstError.message : firstError) +
|
| 43 |
+
"; SSE fallback failed: " +
|
| 44 |
+
String(err instanceof Error ? err.message : err);
|
| 45 |
+
throw new Error(message, { cause: err instanceof Error ? err : undefined });
|
| 46 |
+
}
|
| 47 |
throw err;
|
| 48 |
}
|
| 49 |
|
src/routes/api/mcp/health/+server.ts
CHANGED
|
@@ -83,6 +83,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|
| 83 |
signal,
|
| 84 |
};
|
| 85 |
|
|
|
|
| 86 |
let lastError: Error | undefined;
|
| 87 |
|
| 88 |
// Try Streamable HTTP transport first
|
|
@@ -137,7 +138,8 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|
| 137 |
return res;
|
| 138 |
}
|
| 139 |
} catch (error) {
|
| 140 |
-
|
|
|
|
| 141 |
console.log("Streamable HTTP failed, trying SSE transport...", lastError.message);
|
| 142 |
|
| 143 |
// Close failed client
|
|
@@ -200,6 +202,14 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|
| 200 |
}
|
| 201 |
} catch (sseError) {
|
| 202 |
lastError = sseError instanceof Error ? sseError : new Error(String(sseError));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
console.error("Both transports failed. Last error:", lastError);
|
| 204 |
}
|
| 205 |
}
|
|
|
|
| 83 |
signal,
|
| 84 |
};
|
| 85 |
|
| 86 |
+
let httpError: Error | undefined;
|
| 87 |
let lastError: Error | undefined;
|
| 88 |
|
| 89 |
// Try Streamable HTTP transport first
|
|
|
|
| 138 |
return res;
|
| 139 |
}
|
| 140 |
} catch (error) {
|
| 141 |
+
httpError = error instanceof Error ? error : new Error(String(error));
|
| 142 |
+
lastError = httpError;
|
| 143 |
console.log("Streamable HTTP failed, trying SSE transport...", lastError.message);
|
| 144 |
|
| 145 |
// Close failed client
|
|
|
|
| 202 |
}
|
| 203 |
} catch (sseError) {
|
| 204 |
lastError = sseError instanceof Error ? sseError : new Error(String(sseError));
|
| 205 |
+
// Prefer the HTTP error when both failed so UI shows the primary failure (e.g., HTTP 500) instead
|
| 206 |
+
// of the fallback SSE message.
|
| 207 |
+
if (httpError) {
|
| 208 |
+
lastError = new Error(
|
| 209 |
+
`HTTP transport failed: ${httpError.message}; SSE fallback failed: ${lastError.message}`,
|
| 210 |
+
{ cause: sseError instanceof Error ? sseError : undefined }
|
| 211 |
+
);
|
| 212 |
+
}
|
| 213 |
console.error("Both transports failed. Last error:", lastError);
|
| 214 |
}
|
| 215 |
}
|