Merge branch 'main' into completion-support-default

This commit is contained in:
Ola Hungerford 2025-07-07 05:00:52 -07:00 • committed by GitHub
commit e106194b40
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 5383 additions and 929 deletions

View file

@ -3,6 +3,7 @@ import {
OAuthClientInformationFull,
OAuthClientInformation,
OAuthTokens,
OAuthProtectedResourceMetadata,
} from "@modelcontextprotocol/sdk/shared/auth.js";
// OAuth flow steps
@ -26,8 +27,11 @@ export interface StatusMessage {
export interface AuthDebuggerState {
isInitiatingAuth: boolean;
oauthTokens: OAuthTokens | null;
loading: boolean;
oauthStep: OAuthStep;
resourceMetadata: OAuthProtectedResourceMetadata | null;
resourceMetadataError: Error | null;
resource: URL | null;
authServerUrl: URL | null;
oauthMetadata: OAuthMetadata | null;
oauthClientInfo: OAuthClientInformationFull | OAuthClientInformation | null;
authorizationUrl: string | null;
@ -36,3 +40,20 @@ export interface AuthDebuggerState {
statusMessage: StatusMessage | null;
validationError: string | null;
}
export const EMPTY_DEBUGGER_STATE: AuthDebuggerState = {
isInitiatingAuth: false,
oauthTokens: null,
oauthStep: "metadata_discovery",
oauthMetadata: null,
resourceMetadata: null,
resourceMetadataError: null,
resource: null,
authServerUrl: null,
oauthClientInfo: null,
authorizationUrl: null,
authorizationCode: "",
latestError: null,
statusMessage: null,
validationError: null,
};

View file

@ -2,6 +2,7 @@ export type ConfigItem = {
label: string;
description: string;
value: string | number | boolean;
is_session_item: boolean;
};
/**
@ -33,4 +34,9 @@ export type InspectorConfig = {
* The full address of the MCP Proxy Server, in case it is running on a non-default address. Example: http://10.1.1.22:5577
*/
MCP_PROXY_FULL_ADDRESS: ConfigItem;
/**
* Session token for authenticating with the MCP Proxy Server. This token is displayed in the proxy server console on startup.
*/
MCP_PROXY_AUTH_TOKEN: ConfigItem;
};

View file

@ -7,6 +7,7 @@ export const SESSION_KEYS = {
TOKENS: "mcp_tokens",
CLIENT_INFORMATION: "mcp_client_information",
SERVER_METADATA: "mcp_server_metadata",
AUTH_DEBUGGER_STATE: "mcp_auth_debugger_state",
} as const;
// Generate server-specific session storage keys
@ -35,22 +36,33 @@ export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = {
label: "Request Timeout",
description: "Timeout for requests to the MCP server (ms)",
value: 10000,
is_session_item: false,
},
MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS: {
label: "Reset Timeout on Progress",
description: "Reset timeout on progress notifications",
value: true,
is_session_item: false,
},
MCP_REQUEST_MAX_TOTAL_TIMEOUT: {
label: "Maximum Total Timeout",
description:
"Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)",
value: 60000,
is_session_item: false,
},
MCP_PROXY_FULL_ADDRESS: {
label: "Inspector Proxy Address",
description:
"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
value: "",
is_session_item: false,
},
MCP_PROXY_AUTH_TOKEN: {
label: "Proxy Session Token",
description:
"Session token for authenticating with the MCP Proxy Server (displayed in proxy console on startup)",
value: "",
is_session_item: true,
},
} as const;

View file

@ -3,6 +3,7 @@ import { useConnection } from "../useConnection";
import { z } from "zod";
import { ClientRequest } from "@modelcontextprotocol/sdk/types.js";
import { DEFAULT_INSPECTOR_CONFIG } from "../../constants";
import { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
// Mock fetch
global.fetch = jest.fn().mockResolvedValue({
@ -23,15 +24,48 @@ const mockClient = {
setRequestHandler: jest.fn(),
};
// Mock transport instances
const mockSSETransport: {
start: jest.Mock;
url: URL | undefined;
options: SSEClientTransportOptions | undefined;
} = {
start: jest.fn(),
url: undefined,
options: undefined,
};
const mockStreamableHTTPTransport: {
start: jest.Mock;
url: URL | undefined;
options: SSEClientTransportOptions | undefined;
} = {
start: jest.fn(),
url: undefined,
options: undefined,
};
jest.mock("@modelcontextprotocol/sdk/client/index.js", () => ({
Client: jest.fn().mockImplementation(() => mockClient),
}));
jest.mock("@modelcontextprotocol/sdk/client/sse.js", () => ({
SSEClientTransport: jest.fn(),
SSEClientTransport: jest.fn((url, options) => {
mockSSETransport.url = url;
mockSSETransport.options = options;
return mockSSETransport;
}),
SseError: jest.fn(),
}));
jest.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
StreamableHTTPClientTransport: jest.fn((url, options) => {
mockStreamableHTTPTransport.url = url;
mockStreamableHTTPTransport.options = options;
return mockStreamableHTTPTransport;
}),
}));
jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
auth: jest.fn().mockResolvedValue("AUTHORIZED"),
}));
@ -163,4 +197,269 @@ describe("useConnection", () => {
result.current.makeRequest(mockRequest, mockSchema),
).rejects.toThrow("MCP client not connected");
});
describe("URL Port Handling", () => {
const SSEClientTransport = jest.requireMock(
"@modelcontextprotocol/sdk/client/sse.js",
).SSEClientTransport;
const StreamableHTTPClientTransport = jest.requireMock(
"@modelcontextprotocol/sdk/client/streamableHttp.js",
).StreamableHTTPClientTransport;
beforeEach(() => {
jest.clearAllMocks();
});
test("preserves HTTPS port number when connecting", async () => {
const props = {
...defaultProps,
sseUrl: "https://example.com:8443/api",
transportType: "sse" as const,
};
const { result } = renderHook(() => useConnection(props));
await act(async () => {
await result.current.connect();
});
const call = SSEClientTransport.mock.calls[0][0];
expect(call.toString()).toContain(
"url=https%3A%2F%2Fexample.com%3A8443%2Fapi",
);
});
test("preserves HTTP port number when connecting", async () => {
const props = {
...defaultProps,
sseUrl: "http://localhost:3000/api",
transportType: "sse" as const,
};
const { result } = renderHook(() => useConnection(props));
await act(async () => {
await result.current.connect();
});
const call = SSEClientTransport.mock.calls[0][0];
expect(call.toString()).toContain(
"url=http%3A%2F%2Flocalhost%3A3000%2Fapi",
);
});
test("uses default port for HTTPS when not specified", async () => {
const props = {
...defaultProps,
sseUrl: "https://example.com/api",
transportType: "sse" as const,
};
const { result } = renderHook(() => useConnection(props));
await act(async () => {
await result.current.connect();
});
const call = SSEClientTransport.mock.calls[0][0];
expect(call.toString()).toContain("url=https%3A%2F%2Fexample.com%2Fapi");
expect(call.toString()).not.toContain("%3A443");
});
test("preserves port number in streamable-http transport", async () => {
const props = {
...defaultProps,
sseUrl: "https://example.com:8443/api",
transportType: "streamable-http" as const,
};
const { result } = renderHook(() => useConnection(props));
await act(async () => {
await result.current.connect();
});
const call = StreamableHTTPClientTransport.mock.calls[0][0];
expect(call.toString()).toContain(
"url=https%3A%2F%2Fexample.com%3A8443%2Fapi",
);
});
});
describe("Proxy Authentication Headers", () => {
beforeEach(() => {
jest.clearAllMocks();
// Reset the mock transport objects
mockSSETransport.url = undefined;
mockSSETransport.options = undefined;
mockStreamableHTTPTransport.url = undefined;
mockStreamableHTTPTransport.options = undefined;
});
test("sends X-MCP-Proxy-Auth header when proxy auth token is configured", async () => {
const propsWithProxyAuth = {
...defaultProps,
config: {
...DEFAULT_INSPECTOR_CONFIG,
MCP_PROXY_AUTH_TOKEN: {
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
value: "test-proxy-token",
},
},
};
const { result } = renderHook(() => useConnection(propsWithProxyAuth));
await act(async () => {
await result.current.connect();
});
// Check that the transport was created with the correct headers
expect(mockSSETransport.options).toBeDefined();
expect(mockSSETransport.options?.requestInit).toBeDefined();
expect(mockSSETransport.options?.requestInit?.headers).toHaveProperty(
"X-MCP-Proxy-Auth",
"Bearer test-proxy-token",
);
expect(mockSSETransport?.options?.eventSourceInit?.fetch).toBeDefined();
// Verify the fetch function includes the proxy auth header
const mockFetch = mockSSETransport.options?.eventSourceInit?.fetch;
const testUrl = "http://test.com";
await mockFetch?.(testUrl, {
headers: {
Accept: "text/event-stream",
},
cache: "no-store",
mode: "cors",
signal: new AbortController().signal,
redirect: "follow",
credentials: "include",
});
expect(global.fetch).toHaveBeenCalledTimes(2);
expect(
(global.fetch as jest.Mock).mock.calls[0][1].headers,
).toHaveProperty("X-MCP-Proxy-Auth", "Bearer test-proxy-token");
expect((global.fetch as jest.Mock).mock.calls[1][0]).toBe(testUrl);
expect(
(global.fetch as jest.Mock).mock.calls[1][1].headers,
).toHaveProperty("X-MCP-Proxy-Auth", "Bearer test-proxy-token");
});
test("does NOT send Authorization header for proxy auth", async () => {
const propsWithProxyAuth = {
...defaultProps,
config: {
...DEFAULT_INSPECTOR_CONFIG,
proxyAuthToken: "test-proxy-token",
},
};
const { result } = renderHook(() => useConnection(propsWithProxyAuth));
await act(async () => {
await result.current.connect();
});
// Check that Authorization header is NOT used for proxy auth
expect(mockSSETransport.options?.requestInit?.headers).not.toHaveProperty(
"Authorization",
"Bearer test-proxy-token",
);
});
test("preserves server Authorization header when proxy auth is configured", async () => {
const propsWithBothAuth = {
...defaultProps,
bearerToken: "server-auth-token",
config: {
...DEFAULT_INSPECTOR_CONFIG,
MCP_PROXY_AUTH_TOKEN: {
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
value: "test-proxy-token",
},
},
};
const { result } = renderHook(() => useConnection(propsWithBothAuth));
await act(async () => {
await result.current.connect();
});
// Check that both headers are present and distinct
const headers = mockSSETransport.options?.requestInit?.headers;
expect(headers).toHaveProperty(
"Authorization",
"Bearer server-auth-token",
);
expect(headers).toHaveProperty(
"X-MCP-Proxy-Auth",
"Bearer test-proxy-token",
);
});
test("sends X-MCP-Proxy-Auth in health check requests", async () => {
const fetchMock = global.fetch as jest.Mock;
fetchMock.mockClear();
const propsWithProxyAuth = {
...defaultProps,
config: {
...DEFAULT_INSPECTOR_CONFIG,
MCP_PROXY_AUTH_TOKEN: {
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
value: "test-proxy-token",
},
},
};
const { result } = renderHook(() => useConnection(propsWithProxyAuth));
await act(async () => {
await result.current.connect();
});
// Find the health check call
const healthCheckCall = fetchMock.mock.calls.find(
(call) => call[0].pathname === "/health",
);
expect(healthCheckCall).toBeDefined();
expect(healthCheckCall[1].headers).toHaveProperty(
"X-MCP-Proxy-Auth",
"Bearer test-proxy-token",
);
});
test("works correctly with streamable-http transport", async () => {
const propsWithStreamableHttp = {
...defaultProps,
transportType: "streamable-http" as const,
config: {
...DEFAULT_INSPECTOR_CONFIG,
MCP_PROXY_AUTH_TOKEN: {
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
value: "test-proxy-token",
},
},
};
const { result } = renderHook(() =>
useConnection(propsWithStreamableHttp),
);
await act(async () => {
await result.current.connect();
});
// Check that the streamable HTTP transport was created with the correct headers
expect(mockStreamableHTTPTransport.options).toBeDefined();
expect(
mockStreamableHTTPTransport.options?.requestInit?.headers,
).toHaveProperty("X-MCP-Proxy-Auth", "Bearer test-proxy-token");
});
});
});

View file

@ -42,9 +42,11 @@ import {
getMCPProxyAddress,
getMCPServerRequestMaxTotalTimeout,
resetRequestTimeoutOnProgress,
getMCPProxyAuthToken,
} from "@/utils/configUtils";
import { getMCPServerRequestTimeout } from "@/utils/configUtils";
import { InspectorConfig } from "../configurationTypes";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
interface UseConnectionOptions {
transportType: "stdio" | "sse" | "streamable-http";
@ -83,6 +85,9 @@ export function useConnection({
const [serverCapabilities, setServerCapabilities] =
useState<ServerCapabilities | null>(null);
const [mcpClient, setMcpClient] = useState<Client | null>(null);
const [clientTransport, setClientTransport] = useState<Transport | null>(
null,
);
const [requestHistory, setRequestHistory] = useState<
{ request: string; response?: string }[]
>([]);
@ -238,7 +243,13 @@ export function useConnection({
const checkProxyHealth = async () => {
try {
const proxyHealthUrl = new URL(`${getMCPProxyAddress(config)}/health`);
const proxyHealthResponse = await fetch(proxyHealthUrl);
const { token: proxyAuthToken, header: proxyAuthTokenHeader } =
getMCPProxyAuthToken(config);
const headers: HeadersInit = {};
if (proxyAuthToken) {
headers[proxyAuthTokenHeader] = `Bearer ${proxyAuthToken}`;
}
const proxyHealthResponse = await fetch(proxyHealthUrl, { headers });
const proxyHealth = await proxyHealthResponse.json();
if (proxyHealth?.status !== "ok") {
throw new Error("MCP Proxy Server is not healthy");
@ -257,6 +268,13 @@ export function useConnection({
);
};
const isProxyAuthError = (error: unknown): boolean => {
return (
error instanceof Error &&
error.message.includes("Authentication required. Use the session token")
);
};
const handleAuthError = async (error: unknown) => {
if (is401Error(error)) {
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
@ -304,7 +322,22 @@ export function useConnection({
bearerToken || (await serverAuthProvider.tokens())?.access_token;
if (token) {
const authHeaderName = headerName || "Authorization";
headers[authHeaderName] = `Bearer ${token}`;
// Add custom header name as a special request header to let the server know which header to pass through
if (authHeaderName.toLowerCase() !== "authorization") {
headers[authHeaderName] = token;
headers["x-custom-auth-header"] = authHeaderName;
} else {
headers[authHeaderName] = `Bearer ${token}`;
}
}
// Add proxy authentication
const { token: proxyAuthToken, header: proxyAuthTokenHeader } =
getMCPProxyAuthToken(config);
const proxyHeaders: HeadersInit = {};
if (proxyAuthToken) {
proxyHeaders[proxyAuthTokenHeader] = `Bearer ${proxyAuthToken}`;
}
// Create appropriate transport
@ -324,11 +357,15 @@ export function useConnection({
eventSourceInit: {
fetch: (
url: string | URL | globalThis.Request,
init: RequestInit | undefined,
) => fetch(url, { ...init, headers }),
init?: RequestInit,
) =>
fetch(url, {
...init,
headers: { ...headers, ...proxyHeaders },
}),
},
requestInit: {
headers,
headers: { ...headers, ...proxyHeaders },
},
};
break;
@ -340,11 +377,15 @@ export function useConnection({
eventSourceInit: {
fetch: (
url: string | URL | globalThis.Request,
init: RequestInit | undefined,
) => fetch(url, { ...init, headers }),
init?: RequestInit,
) =>
fetch(url, {
...init,
headers: { ...headers, ...proxyHeaders },
}),
},
requestInit: {
headers,
headers: { ...headers, ...proxyHeaders },
},
};
break;
@ -356,11 +397,15 @@ export function useConnection({
eventSourceInit: {
fetch: (
url: string | URL | globalThis.Request,
init: RequestInit | undefined,
) => fetch(url, { ...init, headers }),
init?: RequestInit,
) =>
fetch(url, {
...init,
headers: { ...headers, ...proxyHeaders },
}),
},
requestInit: {
headers,
headers: { ...headers, ...proxyHeaders },
},
// TODO these should be configurable...
reconnectionOptions: {
@ -377,14 +422,6 @@ export function useConnection({
transportType,
);
const clientTransport =
transportType === "streamable-http"
? new StreamableHTTPClientTransport(mcpProxyServerUrl as URL, {
sessionId: undefined,
...transportOptions,
})
: new SSEClientTransport(mcpProxyServerUrl as URL, transportOptions);
if (onNotification) {
[
CancelledNotificationSchema,
@ -414,7 +451,20 @@ export function useConnection({
let capabilities;
try {
await client.connect(clientTransport);
const transport =
transportType === "streamable-http"
? new StreamableHTTPClientTransport(mcpProxyServerUrl as URL, {
sessionId: undefined,
...transportOptions,
})
: new SSEClientTransport(
mcpProxyServerUrl as URL,
transportOptions,
);
await client.connect(transport as Transport);
setClientTransport(transport);
capabilities = client.getServerCapabilities();
const initializeRequest = {
@ -431,6 +481,18 @@ export function useConnection({
error,
);
// Check if it's a proxy auth error
if (isProxyAuthError(error)) {
toast({
title: "Proxy Authentication Required",
description:
"Please enter the session token from the proxy server console in the Configuration settings.",
variant: "destructive",
});
setConnectionStatus("error");
return;
}
const shouldRetry = await handleAuthError(error);
if (shouldRetry) {
return connect(undefined, retryCount + 1);
@ -468,10 +530,15 @@ export function useConnection({
};
const disconnect = async () => {
if (transportType === "streamable-http")
await (
clientTransport as StreamableHTTPClientTransport
).terminateSession();
await mcpClient?.close();
const authProvider = new InspectorOAuthClientProvider(sseUrl);
authProvider.clear();
setMcpClient(null);
setClientTransport(null);
setConnectionStatus("disconnected");
setCompletionsSupported(false);
setServerCapabilities(null);

View file

@ -51,3 +51,55 @@ export function useDraggablePane(initialHeight: number) {
handleDragStart,
};
}
export function useDraggableSidebar(initialWidth: number) {
const [width, setWidth] = useState(initialWidth);
const [isDragging, setIsDragging] = useState(false);
const dragStartX = useRef<number>(0);
const dragStartWidth = useRef<number>(0);
const handleDragStart = useCallback(
(e: React.MouseEvent) => {
setIsDragging(true);
dragStartX.current = e.clientX;
dragStartWidth.current = width;
document.body.style.userSelect = "none";
},
[width],
);
const handleDragMove = useCallback(
(e: MouseEvent) => {
if (!isDragging) return;
const deltaX = e.clientX - dragStartX.current;
const newWidth = Math.max(
200,
Math.min(600, dragStartWidth.current + deltaX),
);
setWidth(newWidth);
},
[isDragging],
);
const handleDragEnd = useCallback(() => {
setIsDragging(false);
document.body.style.userSelect = "";
}, []);
useEffect(() => {
if (isDragging) {
window.addEventListener("mousemove", handleDragMove);
window.addEventListener("mouseup", handleDragEnd);
return () => {
window.removeEventListener("mousemove", handleDragMove);
window.removeEventListener("mouseup", handleDragEnd);
};
}
}, [isDragging, handleDragMove, handleDragEnd]);
return {
width,
isDragging,
handleDragStart,
};
}

View file

@ -5,8 +5,13 @@ import {
registerClient,
startAuthorization,
exchangeAuthorization,
discoverOAuthProtectedResourceMetadata,
selectResourceURL,
} from "@modelcontextprotocol/sdk/client/auth.js";
import { OAuthMetadataSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
import {
OAuthMetadataSchema,
OAuthProtectedResourceMetadata,
} from "@modelcontextprotocol/sdk/shared/auth.js";
export interface StateMachineContext {
state: AuthDebuggerState;
@ -18,7 +23,6 @@ export interface StateMachineContext {
export interface StateTransition {
canTransition: (context: StateMachineContext) => Promise<boolean>;
execute: (context: StateMachineContext) => Promise<void>;
nextStep: OAuthStep;
}
// State machine transitions
@ -26,18 +30,47 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
metadata_discovery: {
canTransition: async () => true,
execute: async (context) => {
const metadata = await discoverOAuthMetadata(context.serverUrl);
// Default to discovering from the server's URL
let authServerUrl = new URL("/", context.serverUrl);
let resourceMetadata: OAuthProtectedResourceMetadata | null = null;
let resourceMetadataError: Error | null = null;
try {
resourceMetadata = await discoverOAuthProtectedResourceMetadata(
context.serverUrl,
);
if (resourceMetadata?.authorization_servers?.length) {
authServerUrl = new URL(resourceMetadata.authorization_servers[0]);
}
} catch (e) {
if (e instanceof Error) {
resourceMetadataError = e;
} else {
resourceMetadataError = new Error(String(e));
}
}
const resource: URL | undefined = await selectResourceURL(
context.serverUrl,
context.provider,
// we default to null, so swap it for undefined if not set
resourceMetadata ?? undefined,
);
const metadata = await discoverOAuthMetadata(authServerUrl);
if (!metadata) {
throw new Error("Failed to discover OAuth metadata");
}
const parsedMetadata = await OAuthMetadataSchema.parseAsync(metadata);
context.provider.saveServerMetadata(parsedMetadata);
context.updateState({
resourceMetadata,
resource,
resourceMetadataError,
authServerUrl,
oauthMetadata: parsedMetadata,
oauthStep: "client_registration",
});
},
nextStep: "client_registration",
},
client_registration: {
@ -46,9 +79,13 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
const metadata = context.state.oauthMetadata!;
const clientMetadata = context.provider.clientMetadata;
// Prefer scopes from resource metadata if available
const scopesSupported =
context.state.resourceMetadata?.scopes_supported ||
metadata.scopes_supported;
// Add all supported scopes to client registration
if (metadata.scopes_supported) {
clientMetadata.scope = metadata.scopes_supported.join(" ");
if (scopesSupported) {
clientMetadata.scope = scopesSupported.join(" ");
}
const fullInformation = await registerClient(context.serverUrl, {
@ -62,7 +99,6 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
oauthStep: "authorization_redirect",
});
},
nextStep: "authorization_redirect",
},
authorization_redirect: {
@ -84,6 +120,7 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
clientInformation,
redirectUrl: context.provider.redirectUrl,
scope,
resource: context.state.resource ?? undefined,
},
);
@ -93,7 +130,6 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
oauthStep: "authorization_code",
});
},
nextStep: "authorization_code",
},
authorization_code: {
@ -114,7 +150,6 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
oauthStep: "token_request",
});
},
nextStep: "token_request",
},
token_request: {
@ -136,6 +171,7 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
authorizationCode: context.state.authorizationCode,
codeVerifier,
redirectUri: context.provider.redirectUrl,
resource: context.state.resource ?? undefined,
});
context.provider.saveTokens(tokens);
@ -144,7 +180,6 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
oauthStep: "complete",
});
},
nextStep: "complete",
},
complete: {
@ -152,7 +187,6 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
execute: async () => {
// No-op for complete state
},
nextStep: "complete",
},
};