Skip to content

Commit 8a76049

Browse files
chelojimenezclaude
andauthored
XAA debugger: surface AS capability preflight and error guidance (#1869)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 63c9121 commit 8a76049

8 files changed

Lines changed: 1456 additions & 12 deletions

File tree

mcpjam-inspector/client/src/components/xaa/XAAFlowLogger.tsx

Lines changed: 245 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEffect, useMemo, useState } from "react";
22
import {
33
AlertCircle,
4+
AlertTriangle,
45
CheckCircle2,
56
ChevronDown,
67
ChevronRight,
@@ -9,6 +10,8 @@ import {
910
Loader2,
1011
Pencil,
1112
RotateCcw,
13+
ShieldAlert,
14+
ShieldCheck,
1215
} from "lucide-react";
1316
import { Alert, AlertDescription } from "@mcpjam/design-system/alert";
1417
import { Badge } from "@mcpjam/design-system/badge";
@@ -29,10 +32,17 @@ import {
2932
getXAAStepIndex,
3033
XAA_STEP_ORDER,
3134
} from "@/lib/xaa/step-metadata";
35+
import type { XAAFlowState, XAAFlowStep } from "@/lib/xaa/types";
36+
import {
37+
getXAAErrorGuidance,
38+
latestErroredHttpEntry,
39+
type XAAErrorAction,
40+
type XAAErrorGuidance,
41+
} from "@/lib/xaa/error-guidance";
3242
import type {
33-
XAAFlowState,
34-
XAAFlowStep,
35-
} from "@/lib/xaa/types";
43+
XAACheckStatus,
44+
XAACompatibilityReport,
45+
} from "@/lib/xaa/capability-preflight";
3646
import {
3747
NEGATIVE_TEST_MODES,
3848
NEGATIVE_TEST_MODE_DETAILS,
@@ -63,6 +73,176 @@ interface XAAFlowLoggerProps {
6373
};
6474
}
6575

76+
function CompatibilityBanner({
77+
report,
78+
}: {
79+
report: XAACompatibilityReport;
80+
}) {
81+
const [expanded, setExpanded] = useState(report.overall !== "pass");
82+
useEffect(() => {
83+
setExpanded(report.overall !== "pass");
84+
}, [report.overall]);
85+
86+
const tone =
87+
report.overall === "pass"
88+
? {
89+
Icon: ShieldCheck,
90+
iconClass: "text-green-600 dark:text-green-400",
91+
borderClass: "border-green-500/40",
92+
bgClass: "bg-green-500/5",
93+
title: "Authorization server looks XAA-ready",
94+
}
95+
: report.overall === "warn"
96+
? {
97+
Icon: AlertTriangle,
98+
iconClass: "text-amber-500",
99+
borderClass: "border-amber-500/40",
100+
bgClass: "bg-amber-500/5",
101+
title: "Authorization server capabilities are ambiguous",
102+
}
103+
: {
104+
Icon: ShieldAlert,
105+
iconClass: "text-red-500",
106+
borderClass: "border-red-500/40",
107+
bgClass: "bg-red-500/5",
108+
title: "Authorization server isn't XAA-ready",
109+
};
110+
111+
const checkStatusClass = (status: XAACheckStatus) =>
112+
status === "pass"
113+
? "text-green-600 dark:text-green-400"
114+
: status === "fail"
115+
? "text-red-500"
116+
: "text-amber-500";
117+
118+
const checkStatusSymbol = (status: XAACheckStatus) =>
119+
status === "pass" ? "✓" : status === "fail" ? "✗" : "?";
120+
121+
return (
122+
<div
123+
className={cn(
124+
"rounded-md border px-3 py-2.5 text-xs",
125+
tone.borderClass,
126+
tone.bgClass,
127+
)}
128+
>
129+
<button
130+
type="button"
131+
onClick={() => setExpanded((prev) => !prev)}
132+
className="flex w-full items-start gap-2 text-left"
133+
>
134+
<tone.Icon className={cn("h-4 w-4 mt-0.5 shrink-0", tone.iconClass)} />
135+
<div className="flex-1 min-w-0 space-y-1">
136+
<div className="font-medium text-foreground">{tone.title}</div>
137+
{report.vendorHint && (
138+
<div className="text-muted-foreground">
139+
{report.vendorHint.note}
140+
</div>
141+
)}
142+
</div>
143+
{expanded ? (
144+
<ChevronDown className="h-3.5 w-3.5 mt-1 text-muted-foreground shrink-0" />
145+
) : (
146+
<ChevronRight className="h-3.5 w-3.5 mt-1 text-muted-foreground shrink-0" />
147+
)}
148+
</button>
149+
{expanded && (
150+
<ul className="mt-2 space-y-1 border-t border-border/50 pt-2">
151+
{report.checks.map((check) => (
152+
<li key={check.id} className="flex items-start gap-2">
153+
<span
154+
className={cn(
155+
"font-mono shrink-0 w-3",
156+
checkStatusClass(check.status),
157+
)}
158+
aria-hidden
159+
>
160+
{checkStatusSymbol(check.status)}
161+
</span>
162+
<div className="min-w-0 flex-1">
163+
<span className="font-medium text-foreground">
164+
{check.label}
165+
</span>
166+
<span className="text-muted-foreground">{check.detail}</span>
167+
</div>
168+
</li>
169+
))}
170+
</ul>
171+
)}
172+
</div>
173+
);
174+
}
175+
176+
function GuidanceCallout({
177+
guidance,
178+
onConfigure,
179+
onShowBootstrap,
180+
onReset,
181+
}: {
182+
guidance: XAAErrorGuidance;
183+
onConfigure?: () => void;
184+
onShowBootstrap?: () => void;
185+
onReset?: () => void;
186+
}) {
187+
const toneClass =
188+
guidance.severity === "error"
189+
? "border-red-500/40 bg-red-500/5"
190+
: "border-amber-500/40 bg-amber-500/5";
191+
const iconClass =
192+
guidance.severity === "error" ? "text-red-500" : "text-amber-500";
193+
194+
const handleAction = (action: XAAErrorAction) => {
195+
if (action.intent === "configure") onConfigure?.();
196+
else if (action.intent === "bootstrap") onShowBootstrap?.();
197+
else if (action.intent === "reset") onReset?.();
198+
else if (action.intent === "link" && action.href) {
199+
window.open(action.href, "_blank", "noopener,noreferrer");
200+
}
201+
};
202+
203+
const actionDisabled = (action: XAAErrorAction) => {
204+
if (action.intent === "configure") return !onConfigure;
205+
if (action.intent === "bootstrap") return !onShowBootstrap;
206+
if (action.intent === "reset") return !onReset;
207+
if (action.intent === "link") return !action.href;
208+
return true;
209+
};
210+
211+
return (
212+
<div className={cn("rounded-md border px-3 py-2.5 space-y-2", toneClass)}>
213+
<div className="flex items-start gap-2">
214+
<AlertCircle className={cn("h-4 w-4 mt-0.5 shrink-0", iconClass)} />
215+
<div className="flex-1 min-w-0 space-y-1">
216+
<div className="text-xs font-semibold text-foreground">
217+
{guidance.title}
218+
</div>
219+
<div className="text-xs text-muted-foreground">
220+
{guidance.explanation}
221+
</div>
222+
</div>
223+
</div>
224+
{guidance.actions.length > 0 && (
225+
<div className="flex flex-wrap gap-2 pl-6">
226+
{guidance.actions.map((action) => (
227+
<Button
228+
key={`${action.intent}-${action.label}`}
229+
type="button"
230+
variant="outline"
231+
size="sm"
232+
className="h-7 text-xs"
233+
onClick={() => handleAction(action)}
234+
disabled={actionDisabled(action)}
235+
>
236+
{action.label}
237+
</Button>
238+
))}
239+
</div>
240+
)}
241+
</div>
242+
);
243+
}
244+
245+
66246
export function XAAFlowLogger({
67247
flowState,
68248
hasProfile,
@@ -264,15 +444,46 @@ export function XAAFlowLogger({
264444
</div>
265445

266446
<div className="flex-1 overflow-auto bg-muted/30 p-4 space-y-4">
267-
{flowState.error && (
268-
<Alert variant="destructive" className="py-2">
269-
<AlertCircle className="h-4 w-4" />
270-
<AlertDescription className="text-xs">
271-
{flowState.error}
272-
</AlertDescription>
273-
</Alert>
447+
{hasProfile && flowState.compatibilityReport && (
448+
<CompatibilityBanner report={flowState.compatibilityReport} />
274449
)}
275450

451+
{(() => {
452+
const currentStepHttpEntries = (flowState.httpHistory || []).filter(
453+
(entry) => entry.step === flowState.currentStep,
454+
);
455+
const currentStepErroredEntry = latestErroredHttpEntry(
456+
currentStepHttpEntries,
457+
);
458+
if (!flowState.error && !currentStepErroredEntry) return null;
459+
const guidance = getXAAErrorGuidance({
460+
step: flowState.currentStep,
461+
stateError: flowState.error,
462+
httpEntry: currentStepErroredEntry,
463+
});
464+
if (guidance) {
465+
return (
466+
<GuidanceCallout
467+
guidance={guidance}
468+
onConfigure={actions.onConfigure}
469+
onShowBootstrap={actions.onShowBootstrap}
470+
onReset={actions.onReset}
471+
/>
472+
);
473+
}
474+
if (flowState.error) {
475+
return (
476+
<Alert variant="destructive" className="py-2">
477+
<AlertCircle className="h-4 w-4" />
478+
<AlertDescription className="text-xs">
479+
{flowState.error}
480+
</AlertDescription>
481+
</Alert>
482+
);
483+
}
484+
return null;
485+
})()}
486+
276487
{flowState.idJag && flowState.idJagDecoded && (
277488
<IdJagInspector
278489
rawJwt={flowState.idJag}
@@ -377,6 +588,30 @@ export function XAAFlowLogger({
377588
</div>
378589
) : null}
379590

591+
{(() => {
592+
if (group.step === flowState.currentStep) {
593+
// Top-level callout covers the current step.
594+
return null;
595+
}
596+
const erroredEntry = latestErroredHttpEntry(
597+
group.httpEntries,
598+
);
599+
if (!erroredEntry) return null;
600+
const guidance = getXAAErrorGuidance({
601+
step: group.step,
602+
httpEntry: erroredEntry,
603+
});
604+
if (!guidance) return null;
605+
return (
606+
<GuidanceCallout
607+
guidance={guidance}
608+
onConfigure={actions.onConfigure}
609+
onShowBootstrap={actions.onShowBootstrap}
610+
onReset={actions.onReset}
611+
/>
612+
);
613+
})()}
614+
380615
{group.infoEntries.map((entry) => (
381616
<InfoLogEntry
382617
key={entry.id}

mcpjam-inspector/client/src/components/xaa/XAASequenceDiagram.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { buildXAAActions } from "@/lib/xaa/sequence-actions";
77
import type { XAAFlowState, XAAFlowStep } from "@/lib/xaa/types";
88

99
const XAA_ACTORS = {
10-
client: { label: "MCP Client", color: "#10b981" },
11-
testIdp: { label: "MCPJam Issuer", color: "#ef4444" },
10+
client: { label: "Agent", color: "#10b981" },
11+
testIdp: { label: "IdP", color: "#ef4444" },
1212
mcpServer: { label: "MCP Server", color: "#f59e0b" },
1313
authServer: { label: "Authorization Server", color: "#3b82f6" },
1414
};

0 commit comments

Comments
 (0)