All of the mid-session and step-up auth work
This commit is contained in:
parent
1f31e0f1f8
commit
562bdd00f1
6 changed files with 11437 additions and 1212 deletions
|
|
@ -8,12 +8,19 @@ import type {
|
|||
ConnectionStatus,
|
||||
} from "@inspector/core/mcp/index.js";
|
||||
import type { OAuthConnectionState } from "@inspector/core/auth/types.js";
|
||||
import type { AuthChallenge } from "@inspector/core/auth/challenge.js";
|
||||
import {
|
||||
formatAuthProtocol,
|
||||
formatClientRegistrationKind,
|
||||
formatIdpSession,
|
||||
formatScopes,
|
||||
} from "../utils/oauthDisplay.js";
|
||||
import {
|
||||
stepUpAuthorizeActionLabel,
|
||||
stepUpConfirmMessage,
|
||||
stepUpFollowUpMessage,
|
||||
stepUpModalTitle,
|
||||
} from "../utils/tuiOAuth.js";
|
||||
|
||||
interface AuthTabProps {
|
||||
serverName: string | null;
|
||||
|
|
@ -22,6 +29,13 @@ interface AuthTabProps {
|
|||
oauthStatus: "idle" | "authenticating" | "error";
|
||||
oauthMessage: string | null;
|
||||
oauthRevision: number;
|
||||
pendingStepUp?: {
|
||||
challenge: AuthChallenge;
|
||||
authorizationScopes?: string[];
|
||||
enterpriseManaged?: boolean;
|
||||
} | null;
|
||||
onAuthorizeStepUp?: () => void;
|
||||
onCancelStepUp?: () => void;
|
||||
width: number;
|
||||
height: number;
|
||||
focused?: boolean;
|
||||
|
|
@ -44,6 +58,9 @@ export function AuthTab({
|
|||
oauthStatus,
|
||||
oauthMessage,
|
||||
oauthRevision,
|
||||
pendingStepUp,
|
||||
onAuthorizeStepUp,
|
||||
onCancelStepUp,
|
||||
width,
|
||||
height,
|
||||
focused = false,
|
||||
|
|
@ -58,6 +75,7 @@ export function AuthTab({
|
|||
>(undefined);
|
||||
const [clearedConfirmation, setClearedConfirmation] = useState(false);
|
||||
const [lastClearDisconnected, setLastClearDisconnected] = useState(false);
|
||||
const [stepUpChoiceIndex, setStepUpChoiceIndex] = useState(0);
|
||||
|
||||
const refreshOAuthState = useCallback(async () => {
|
||||
if (!inspectorClient) {
|
||||
|
|
@ -70,13 +88,17 @@ export function AuthTab({
|
|||
|
||||
useEffect(() => {
|
||||
void refreshOAuthState();
|
||||
}, [refreshOAuthState, oauthRevision]);
|
||||
}, [refreshOAuthState, oauthRevision, connectionStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
setClearedConfirmation(false);
|
||||
setLastClearDisconnected(false);
|
||||
}, [oauthRevision]);
|
||||
|
||||
useEffect(() => {
|
||||
setStepUpChoiceIndex(0);
|
||||
}, [pendingStepUp]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inspectorClient) return;
|
||||
|
||||
|
|
@ -93,6 +115,34 @@ export function AuthTab({
|
|||
(input: string, key: Key) => {
|
||||
if (!focused) return;
|
||||
|
||||
if (pendingStepUp) {
|
||||
if (key.upArrow) {
|
||||
setStepUpChoiceIndex((i) => Math.max(0, i - 1));
|
||||
return;
|
||||
}
|
||||
if (key.downArrow) {
|
||||
setStepUpChoiceIndex((i) => Math.min(1, i + 1));
|
||||
return;
|
||||
}
|
||||
if (key.return) {
|
||||
if (stepUpChoiceIndex === 0) {
|
||||
onAuthorizeStepUp?.();
|
||||
} else {
|
||||
onCancelStepUp?.();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (input.toLowerCase() === "a") {
|
||||
onAuthorizeStepUp?.();
|
||||
return;
|
||||
}
|
||||
if (input.toLowerCase() === "c") {
|
||||
onCancelStepUp?.();
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.upArrow && scrollViewRef.current) {
|
||||
scrollViewRef.current.scrollBy(-1);
|
||||
} else if (key.downArrow && scrollViewRef.current) {
|
||||
|
|
@ -139,6 +189,66 @@ export function AuthTab({
|
|||
{oauthStatus === "error" && oauthMessage && (
|
||||
<Text color="red">{oauthMessage}</Text>
|
||||
)}
|
||||
{oauthStatus === "idle" && oauthMessage && (
|
||||
<Text color="cyan">{oauthMessage}</Text>
|
||||
)}
|
||||
|
||||
{pendingStepUp ? (
|
||||
<Box marginTop={1} flexDirection="column" gap={0}>
|
||||
<Text bold color="yellow">
|
||||
{stepUpModalTitle({
|
||||
enterpriseManaged: pendingStepUp.enterpriseManaged,
|
||||
})}
|
||||
</Text>
|
||||
<Text>
|
||||
{stepUpConfirmMessage(pendingStepUp.challenge, {
|
||||
enterpriseManaged: pendingStepUp.enterpriseManaged,
|
||||
})}{" "}
|
||||
{stepUpFollowUpMessage({
|
||||
enterpriseManaged: pendingStepUp.enterpriseManaged,
|
||||
})}
|
||||
</Text>
|
||||
{(() => {
|
||||
const scopes =
|
||||
pendingStepUp.authorizationScopes ??
|
||||
pendingStepUp.challenge.requiredScopes;
|
||||
if (!scopes?.length) return null;
|
||||
return (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text dimColor>
|
||||
{pendingStepUp.enterpriseManaged
|
||||
? "Permissions requested:"
|
||||
: "Scopes requested:"}
|
||||
</Text>
|
||||
{scopes.map((scope) => (
|
||||
<Text key={scope} dimColor>
|
||||
{" "}
|
||||
• {scope}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
})()}
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<SelectableItem isSelected={stepUpChoiceIndex === 0} bold>
|
||||
{(() => {
|
||||
const label = stepUpAuthorizeActionLabel({
|
||||
enterpriseManaged: pendingStepUp.enterpriseManaged,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Text underline>{label[0]}</Text>
|
||||
{label.slice(1)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</SelectableItem>
|
||||
<SelectableItem isSelected={stepUpChoiceIndex === 1}>
|
||||
<Text underline>C</Text>ancel
|
||||
</SelectableItem>
|
||||
</Box>
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
{oauthState ? (
|
||||
<Box flexDirection="column" marginTop={1} gap={0}>
|
||||
|
|
@ -229,7 +339,9 @@ export function AuthTab({
|
|||
backgroundColor="gray"
|
||||
>
|
||||
<Text bold color="white">
|
||||
S {isLiveConnection ? "clear+disconnect" : "clear"}, ↑/↓ scroll
|
||||
{pendingStepUp
|
||||
? "↑/↓ select, Enter confirm, A authorize, C cancel"
|
||||
: `S ${isLiveConnection ? "clear+disconnect" : "clear"}, ↑/↓ scroll`}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useState } from "react";
|
|||
import { Box, Text, useInput, type Key } from "ink";
|
||||
import { Form } from "ink-form";
|
||||
import { InspectorClient } from "@inspector/core/mcp/index.js";
|
||||
import { AuthRecoveryRequiredError } from "@inspector/core/auth/challenge.js";
|
||||
import type {
|
||||
Prompt,
|
||||
GetPromptResult,
|
||||
|
|
@ -29,6 +30,7 @@ interface PromptTestModalProps {
|
|||
width: number;
|
||||
height: number;
|
||||
onClose: () => void;
|
||||
onAuthRecoveryRequired?: (error: AuthRecoveryRequiredError) => void;
|
||||
}
|
||||
|
||||
type ModalState = "form" | "loading" | "results";
|
||||
|
|
@ -47,6 +49,7 @@ export function PromptTestModal({
|
|||
width,
|
||||
height,
|
||||
onClose,
|
||||
onAuthRecoveryRequired,
|
||||
}: PromptTestModalProps) {
|
||||
const [state, setState] = useState<ModalState>("form");
|
||||
const [result, setResult] = useState<PromptResult | null>(null);
|
||||
|
|
@ -144,6 +147,11 @@ export function PromptTestModal({
|
|||
});
|
||||
setState("results");
|
||||
} catch (error) {
|
||||
if (error instanceof AuthRecoveryRequiredError) {
|
||||
onAuthRecoveryRequired?.(error);
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
const duration = Date.now() - startTime;
|
||||
const errorMessage = getErrorMessage(error);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from "react";
|
|||
import { Box, Text, useInput, type Key } from "ink";
|
||||
import { ScrollView, type ScrollViewRef } from "ink-scroll-view";
|
||||
import type { InspectorClient } from "@inspector/core/mcp/index.js";
|
||||
import { AuthRecoveryRequiredError } from "@inspector/core/auth/challenge.js";
|
||||
import type {
|
||||
Prompt,
|
||||
PromptArgument,
|
||||
|
|
@ -18,6 +19,7 @@ interface PromptsTabProps {
|
|||
focusedPane?: "list" | "details" | null;
|
||||
onViewDetails?: (prompt: Prompt & { result?: GetPromptResult }) => void;
|
||||
onFetchPrompt?: (prompt: Prompt) => void;
|
||||
onAuthRecoveryRequired?: (error: AuthRecoveryRequiredError) => void;
|
||||
modalOpen?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -29,6 +31,7 @@ export function PromptsTab({
|
|||
focusedPane = null,
|
||||
onViewDetails,
|
||||
onFetchPrompt,
|
||||
onAuthRecoveryRequired,
|
||||
modalOpen = false,
|
||||
}: PromptsTabProps) {
|
||||
const visibleCount = Math.max(1, height - 7);
|
||||
|
|
@ -64,6 +67,10 @@ export function PromptsTab({
|
|||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof AuthRecoveryRequiredError) {
|
||||
onAuthRecoveryRequired?.(error);
|
||||
return;
|
||||
}
|
||||
setError(
|
||||
error instanceof Error ? error.message : "Failed to get prompt",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useState } from "react";
|
|||
import { Box, Text, useInput, type Key } from "ink";
|
||||
import { Form } from "ink-form";
|
||||
import { InspectorClient } from "@inspector/core/mcp/index.js";
|
||||
import { AuthRecoveryRequiredError } from "@inspector/core/auth/challenge.js";
|
||||
import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { uriTemplateToForm } from "../utils/uriTemplateToForm.js";
|
||||
import { ScrollView, type ScrollViewRef } from "ink-scroll-view";
|
||||
|
|
@ -30,6 +31,7 @@ interface ResourceTestModalProps {
|
|||
width: number;
|
||||
height: number;
|
||||
onClose: () => void;
|
||||
onAuthRecoveryRequired?: (error: AuthRecoveryRequiredError) => void;
|
||||
}
|
||||
|
||||
type ModalState = "form" | "loading" | "results";
|
||||
|
|
@ -49,6 +51,7 @@ export function ResourceTestModal({
|
|||
width,
|
||||
height,
|
||||
onClose,
|
||||
onAuthRecoveryRequired,
|
||||
}: ResourceTestModalProps) {
|
||||
const [state, setState] = useState<ModalState>("form");
|
||||
const [result, setResult] = useState<ResourceResult | null>(null);
|
||||
|
|
@ -150,6 +153,11 @@ export function ResourceTestModal({
|
|||
});
|
||||
setState("results");
|
||||
} catch (error) {
|
||||
if (error instanceof AuthRecoveryRequiredError) {
|
||||
onAuthRecoveryRequired?.(error);
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
const duration = Date.now() - startTime;
|
||||
const errorMessage = getErrorMessage(error);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef, useMemo } from "react";
|
|||
import { Box, Text, useInput, type Key } from "ink";
|
||||
import { ScrollView, type ScrollViewRef } from "ink-scroll-view";
|
||||
import type { InspectorClient } from "@inspector/core/mcp/index.js";
|
||||
import { AuthRecoveryRequiredError } from "@inspector/core/auth/challenge.js";
|
||||
import type {
|
||||
Resource,
|
||||
ReadResourceResult,
|
||||
|
|
@ -27,6 +28,7 @@ interface ResourcesTabProps {
|
|||
) => void;
|
||||
onFetchResource?: (resource: Resource) => void;
|
||||
onFetchTemplate?: (template: ResourceTemplate) => void;
|
||||
onAuthRecoveryRequired?: (error: AuthRecoveryRequiredError) => void;
|
||||
modalOpen?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -41,6 +43,7 @@ export function ResourcesTab({
|
|||
onViewDetails,
|
||||
onFetchResource,
|
||||
onFetchTemplate,
|
||||
onAuthRecoveryRequired,
|
||||
modalOpen = false,
|
||||
}: ResourcesTabProps) {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
@ -169,6 +172,10 @@ export function ResourcesTab({
|
|||
await inspectorClient.readResource(shouldFetchResource);
|
||||
setResourceContent(invocation.result);
|
||||
} catch (err) {
|
||||
if (err instanceof AuthRecoveryRequiredError) {
|
||||
onAuthRecoveryRequired?.(err);
|
||||
return;
|
||||
}
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Failed to read resource",
|
||||
);
|
||||
|
|
@ -180,7 +187,7 @@ export function ResourcesTab({
|
|||
};
|
||||
|
||||
fetchContent();
|
||||
}, [shouldFetchResource, inspectorClient]);
|
||||
}, [shouldFetchResource, inspectorClient, onAuthRecoveryRequired]);
|
||||
|
||||
const listWidth = Math.floor(width * 0.4);
|
||||
const detailWidth = width - listWidth;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useState } from "react";
|
|||
import { Box, Text, useInput, type Key } from "ink";
|
||||
import { Form } from "ink-form";
|
||||
import { InspectorClient } from "@inspector/core/mcp/index.js";
|
||||
import { AuthRecoveryRequiredError } from "@inspector/core/auth/challenge.js";
|
||||
import type { Tool, CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import type { JsonValue } from "@inspector/core/mcp/index.js";
|
||||
import { schemaToForm } from "../utils/schemaToForm.js";
|
||||
|
|
@ -13,6 +14,7 @@ interface ToolTestModalProps {
|
|||
width: number;
|
||||
height: number;
|
||||
onClose: () => void;
|
||||
onAuthRecoveryRequired?: (error: AuthRecoveryRequiredError) => void;
|
||||
}
|
||||
|
||||
type ModalState = "form" | "loading" | "results";
|
||||
|
|
@ -31,6 +33,7 @@ export function ToolTestModal({
|
|||
width,
|
||||
height,
|
||||
onClose,
|
||||
onAuthRecoveryRequired,
|
||||
}: ToolTestModalProps) {
|
||||
const [state, setState] = useState<ModalState>("form");
|
||||
const [result, setResult] = useState<ToolResult | null>(null);
|
||||
|
|
@ -150,6 +153,11 @@ export function ToolTestModal({
|
|||
}
|
||||
setState("results");
|
||||
} catch (error) {
|
||||
if (error instanceof AuthRecoveryRequiredError) {
|
||||
onAuthRecoveryRequired?.(error);
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
const duration = Date.now() - startTime;
|
||||
const errorObj =
|
||||
error instanceof Error
|
||||
|
|
|
|||
Loading…
Reference in a new issue