Skip to content

Commit 759a361

Browse files
committed
fix(auth-debugger): show failed requests as steps instead of dismissing flow
Two key changes: 1. debug-middleware.ts: Catch fetch failures (CORS, network errors) and show them as steps with status 0 before re-throwing the error 2. AuthDebuggerFlow.tsx: Catch SDK errors (when all discovery fails) and show as a final 'Flow Error' step instead of calling onError() which dismisses the entire flow Now when testing with a CORS-restricted server: - Initial POST failure → shown as step with warning icon - Each failed discovery request → shown as step with warning icon - Final error → shown as step, flow stays visible
1 parent 985389a commit 759a361

6 files changed

Lines changed: 374 additions & 9 deletions

File tree

client/e2e/auth-cors-fail.spec.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
const APP_URL = "http://localhost:6275/";
4+
// This URL causes CORS preflight failures
5+
const CORS_FAIL_URL =
6+
"https://adb-7405613454306346.6.azuredatabricks.net/api/2.0/mcp/dbsql";
7+
8+
test.describe("Auth Debugger CORS Failure", () => {
9+
test("should show CORS failure as a step with warning icon", async ({
10+
page,
11+
}) => {
12+
page.on("console", (msg) => {
13+
console.log(`[Browser ${msg.type()}]: ${msg.text()}`);
14+
});
15+
16+
await page.goto(APP_URL);
17+
await expect(page.getByText("MCP Inspector")).toBeVisible();
18+
19+
// Select Streamable HTTP transport
20+
const transportDropdown = page.getByRole("combobox", {
21+
name: "Transport Type",
22+
});
23+
await transportDropdown.click();
24+
await page.getByRole("option", { name: "Streamable HTTP" }).click();
25+
26+
// Enter the CORS-failing URL
27+
const urlInput = page.getByRole("textbox", { name: "URL" });
28+
await expect(urlInput).toBeVisible();
29+
await urlInput.fill(CORS_FAIL_URL);
30+
31+
// Click Open Auth Settings
32+
await page.getByRole("button", { name: "Open Auth Settings" }).click();
33+
34+
// Wait for auth settings page
35+
await expect(
36+
page.getByRole("heading", { name: "Authentication Settings" }),
37+
).toBeVisible();
38+
39+
// Take screenshot before
40+
await page.screenshot({ path: "before-cors-test.png", fullPage: true });
41+
42+
// Click Debug Flow
43+
await page.getByRole("button", { name: "Debug Flow" }).click();
44+
45+
// Wait for CORS error to appear
46+
await page.waitForTimeout(5000);
47+
48+
// Take screenshot after
49+
await page.screenshot({ path: "after-cors-test.png", fullPage: true });
50+
51+
// Count steps
52+
const allSteps = await page
53+
.locator('[class*="flex items-center p-2"]')
54+
.count();
55+
console.log(`Total steps displayed: ${allSteps}`);
56+
57+
// Get full page content
58+
const html = await page.content();
59+
console.log(
60+
"Page has OAuth Debug Flow:",
61+
html.includes("OAuth Debug Flow"),
62+
);
63+
64+
// Look for error indicators
65+
const warningIcons = await page.locator("svg.text-yellow-500").count();
66+
console.log(`Warning icons: ${warningIcons}`);
67+
});
68+
});

client/e2e/auth-cors-full.spec.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
const APP_URL = "http://localhost:6275/";
4+
const CORS_FAIL_URL =
5+
"https://adb-7405613454306346.6.azuredatabricks.net/api/2.0/mcp/dbsql";
6+
7+
test.describe("Auth Debugger CORS Full Flow", () => {
8+
test("should show all failed requests as steps, not dismiss the flow", async ({
9+
page,
10+
}) => {
11+
page.on("console", (msg) => {
12+
console.log(`[Browser ${msg.type()}]: ${msg.text()}`);
13+
});
14+
15+
await page.goto(APP_URL);
16+
await expect(page.getByText("MCP Inspector")).toBeVisible();
17+
18+
// Setup
19+
const transportDropdown = page.getByRole("combobox", {
20+
name: "Transport Type",
21+
});
22+
await transportDropdown.click();
23+
await page.getByRole("option", { name: "Streamable HTTP" }).click();
24+
25+
const urlInput = page.getByRole("textbox", { name: "URL" });
26+
await urlInput.fill(CORS_FAIL_URL);
27+
28+
await page.getByRole("button", { name: "Open Auth Settings" }).click();
29+
await expect(
30+
page.getByRole("heading", { name: "Authentication Settings" }),
31+
).toBeVisible();
32+
33+
// Start Debug Flow
34+
await page.getByRole("button", { name: "Debug Flow" }).click();
35+
36+
// Step through all failed requests
37+
for (let i = 1; i <= 10; i++) {
38+
await page.waitForTimeout(2000);
39+
40+
// Take screenshot
41+
await page.screenshot({ path: `cors-step-${i}.png`, fullPage: true });
42+
43+
// Count visible steps
44+
const stepCount = await page
45+
.locator('[class*="flex items-center p-2 rounded-md cursor-pointer"]')
46+
.count();
47+
console.log(`Iteration ${i}: ${stepCount} step(s) visible`);
48+
49+
// Check for warning icons
50+
const warningIcons = await page
51+
.locator("svg.lucide-alert-triangle")
52+
.count();
53+
console.log(`Iteration ${i}: ${warningIcons} warning icon(s)`);
54+
55+
// Check if there's a Continue button
56+
const continueButton = page.getByRole("button", { name: "Continue" });
57+
const isVisible = await continueButton
58+
.isVisible({ timeout: 500 })
59+
.catch(() => false);
60+
61+
if (isVisible) {
62+
const isEnabled = await continueButton.isEnabled();
63+
if (isEnabled) {
64+
console.log(`Iteration ${i}: Clicking Continue`);
65+
await continueButton.click();
66+
} else {
67+
console.log(`Iteration ${i}: Continue button disabled, stopping`);
68+
break;
69+
}
70+
} else {
71+
console.log(`Iteration ${i}: No Continue button visible`);
72+
73+
// Check if flow shows complete state
74+
const completeIndicator = await page
75+
.locator("text=/complete|success/i")
76+
.count();
77+
if (completeIndicator > 0) {
78+
console.log(`Iteration ${i}: Flow complete`);
79+
break;
80+
}
81+
82+
// Check for error message (old behavior we're fixing)
83+
const errorMessage = await page.locator(".bg-red-50").count();
84+
if (errorMessage > 0) {
85+
console.log(
86+
`Iteration ${i}: ERROR - Flow dismissed with error message (old behavior)`,
87+
);
88+
}
89+
break;
90+
}
91+
}
92+
93+
// Final screenshot
94+
await page.screenshot({ path: `cors-final.png`, fullPage: true });
95+
96+
// Check final state - should still show the flow with steps
97+
const finalStepCount = await page
98+
.locator('[class*="flex items-center p-2 rounded-md"]')
99+
.count();
100+
console.log(`Final: ${finalStepCount} step(s) visible`);
101+
102+
// Flow should NOT have been dismissed
103+
const flowHeading = await page.locator('text="OAuth Debug Flow"').count();
104+
console.log(`Final: OAuth Debug Flow heading visible: ${flowHeading > 0}`);
105+
});
106+
});

client/e2e/auth-debug.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
const APP_URL = "http://localhost:6275/";
4+
const TEST_SERVER_URL = "https://mcp-oauth-ex3.val.run/mcp";
5+
6+
test.describe("Auth Debugger Debug", () => {
7+
test("should not double render steps", async ({ page }) => {
8+
// Enable console logging
9+
page.on("console", (msg) => {
10+
console.log(`[Browser ${msg.type()}]: ${msg.text()}`);
11+
});
12+
13+
await page.goto(APP_URL);
14+
await expect(page.getByText("MCP Inspector")).toBeVisible();
15+
16+
// Select Streamable HTTP transport
17+
const transportDropdown = page.getByRole("combobox", {
18+
name: "Transport Type",
19+
});
20+
await transportDropdown.click();
21+
await page.getByRole("option", { name: "Streamable HTTP" }).click();
22+
23+
// Enter URL
24+
const urlInput = page.getByRole("textbox", { name: "URL" });
25+
await expect(urlInput).toBeVisible();
26+
await urlInput.fill(TEST_SERVER_URL);
27+
28+
// Click Open Auth Settings
29+
const openAuthButton = page.getByRole("button", {
30+
name: "Open Auth Settings",
31+
});
32+
await expect(openAuthButton).toBeVisible();
33+
await openAuthButton.click();
34+
35+
// Wait for auth settings page
36+
await expect(
37+
page.getByRole("heading", { name: "Authentication Settings" }),
38+
).toBeVisible();
39+
40+
// Click Debug Flow
41+
await page.getByRole("button", { name: "Debug Flow" }).click();
42+
43+
// Wait for the first step to appear
44+
await page.waitForTimeout(3000);
45+
46+
// Take screenshot
47+
await page.screenshot({ path: "debug-flow-state.png", fullPage: true });
48+
49+
// Count how many times "1." appears in the step list
50+
const stepOnes = await page.locator("text=/^1\\.\\s/").count();
51+
console.log(`Number of "1." step elements: ${stepOnes}`);
52+
53+
// Get page content for debugging
54+
const content = await page.locator(".space-y-3").innerHTML();
55+
console.log("Step list content:", content.substring(0, 2000));
56+
});
57+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
const APP_URL = "http://localhost:6275/";
4+
const TEST_URL = "https://mcp-oauth-ex3.val.run/mcp";
5+
6+
test.describe("Auth Debugger Double Render Check", () => {
7+
test("step through and check for double rendering", async ({ page }) => {
8+
page.on("console", (msg) => {
9+
if (msg.type() === "log" || msg.type() === "info") {
10+
console.log(`[Browser]: ${msg.text()}`);
11+
}
12+
});
13+
14+
await page.goto(APP_URL);
15+
await expect(page.getByText("MCP Inspector")).toBeVisible();
16+
17+
// Setup
18+
const transportDropdown = page.getByRole("combobox", {
19+
name: "Transport Type",
20+
});
21+
await transportDropdown.click();
22+
await page.getByRole("option", { name: "Streamable HTTP" }).click();
23+
24+
const urlInput = page.getByRole("textbox", { name: "URL" });
25+
await urlInput.fill(TEST_URL);
26+
27+
await page.getByRole("button", { name: "Open Auth Settings" }).click();
28+
await expect(
29+
page.getByRole("heading", { name: "Authentication Settings" }),
30+
).toBeVisible();
31+
32+
// Start Debug Flow
33+
await page.getByRole("button", { name: "Debug Flow" }).click();
34+
await page.waitForTimeout(2000);
35+
36+
// Step through flow, taking screenshots and counting steps
37+
for (let i = 1; i <= 5; i++) {
38+
await page.screenshot({
39+
path: `step-${i}-before-continue.png`,
40+
fullPage: true,
41+
});
42+
43+
// Count step numbers visible
44+
const stepNumbers = await page
45+
.locator("text=/^\\d+\\.\\s/")
46+
.allTextContents();
47+
console.log(`Step ${i}: Visible step numbers:`, stepNumbers);
48+
49+
// Count unique step elements by their step number prefix
50+
const stepRows = await page.locator(".space-y-3 > div").count();
51+
console.log(`Step ${i}: Total step div count:`, stepRows);
52+
53+
const continueButton = page.getByRole("button", { name: "Continue" });
54+
if (
55+
await continueButton.isVisible({ timeout: 1000 }).catch(() => false)
56+
) {
57+
await continueButton.click();
58+
await page.waitForTimeout(1500);
59+
} else {
60+
console.log(`Step ${i}: No Continue button, stopping`);
61+
break;
62+
}
63+
}
64+
65+
await page.screenshot({ path: `final-state.png`, fullPage: true });
66+
});
67+
});

client/src/components/AuthDebuggerFlow.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,42 @@ export function AuthDebuggerFlow({
307307
}
308308
} catch (error) {
309309
console.error("OAuth debug flow error:", error);
310-
onError(error instanceof Error ? error : new Error(String(error)));
310+
311+
// Show the final error as a step instead of dismissing the flow
312+
const errorMessage =
313+
error instanceof Error ? error.message : String(error);
314+
const errorEntry: DebugRequestResponse = {
315+
id: crypto.randomUUID(),
316+
label: "Flow Error",
317+
request: {
318+
method: "N/A",
319+
url: serverUrl,
320+
headers: {},
321+
},
322+
response: {
323+
status: 0,
324+
statusText: "Error",
325+
headers: {},
326+
body: {
327+
error: errorMessage,
328+
note: "The OAuth flow could not complete. This may be due to CORS restrictions or server configuration.",
329+
},
330+
},
331+
};
332+
333+
if (quickMode) {
334+
setCompletedSteps((prev) => [...prev, errorEntry]);
335+
setFlowState("complete");
336+
} else {
337+
setCurrentStep(errorEntry);
338+
setFlowState("waiting_continue");
339+
await new Promise<void>((resolve) => {
340+
continueResolverRef.current = resolve;
341+
});
342+
setCompletedSteps((prev) => [...prev, errorEntry]);
343+
setCurrentStep(null);
344+
setFlowState("complete");
345+
}
311346
}
312347
}
313348

0 commit comments

Comments
 (0)