Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions mcpjam-inspector/client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useConvexAuth } from "convex/react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useAuth } from "@workos-inc/authkit-react";
import { toast } from "sonner";
import { ServersTab } from "./components/ServersTab";
import { ToolsTab } from "./components/ToolsTab";
Expand All @@ -25,11 +24,13 @@ import { ProfileTab } from "./components/ProfileTab";
import { OrganizationsTab } from "./components/OrganizationsTab";
import { SupportTab } from "./components/SupportTab";
import OAuthDebugCallback from "./components/oauth/OAuthDebugCallback";
import OAuthDesktopReturnNotice from "./components/oauth/OAuthDesktopReturnNotice";
import { MCPSidebar } from "./components/mcp-sidebar";
import { SidebarInset, SidebarProvider } from "./components/ui/sidebar";
import { useAppState } from "./hooks/use-app-state";
import { PreferencesStoreProvider } from "./stores/preferences/preferences-provider";
import { Toaster } from "./components/ui/sonner";
import { useElectronHostedAuth } from "./hooks/useElectronHostedAuth";
import { useElectronOAuth } from "./hooks/useElectronOAuth";
import { useEnsureDbUser } from "./hooks/useEnsureDbUser";
import { usePostHog, useFeatureFlagEnabled } from "posthog-js/react";
Expand Down Expand Up @@ -91,6 +92,7 @@ import {
writeHostedOAuthResumeMarker,
} from "./lib/hosted-oauth-resume";
import { handleOAuthCallback } from "./lib/oauth/mcp-oauth";
import { buildElectronMcpCallbackUrl } from "./hooks/use-server-state";

function getHostedOAuthCallbackErrorMessage(): string {
const params = new URLSearchParams(window.location.search);
Expand Down Expand Up @@ -120,7 +122,7 @@ export default function App() {
signIn,
user: workOsUser,
isLoading: isWorkOsLoading,
} = useAuth();
} = useElectronHostedAuth();
const { isAuthenticated, isLoading: isAuthLoading } = useConvexAuth();
const [hostedOAuthHandling, setHostedOAuthHandling] = useState(() =>
HOSTED_MODE ? getHostedOAuthCallbackContext() !== null : false,
Expand Down Expand Up @@ -279,6 +281,7 @@ export default function App() {
"/oauth/callback/debug",
);
const isOAuthCallback = window.location.pathname === "/callback";
const electronMcpCallbackUrl = buildElectronMcpCallbackUrl();

useEffect(() => {
if (!isOAuthCallback) {
Expand Down Expand Up @@ -593,6 +596,12 @@ export default function App() {
return <OAuthDebugCallback />;
}

if (electronMcpCallbackUrl) {
return (
<OAuthDesktopReturnNotice returnToElectronUrl={electronMcpCallbackUrl} />
);
}

if (hostedOAuthHandling) {
return <LoadingScreen />;
}
Expand Down
36 changes: 33 additions & 3 deletions mcpjam-inspector/client/src/__tests__/App.hosted-oauth.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,14 @@ vi.mock("../lib/theme-utils", () => ({
updateThemePreset: vi.fn(),
}));

vi.mock("../lib/oauth/mcp-oauth", () => ({
handleOAuthCallback: mockHandleOAuthCallback,
}));
vi.mock("../lib/oauth/mcp-oauth", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../lib/oauth/mcp-oauth")>();
return {
...actual,
handleOAuthCallback: mockHandleOAuthCallback,
};
});

vi.mock("../components/ServersTab", () => ({
ServersTab: () => <div>Servers Tab</div>,
Expand Down Expand Up @@ -328,4 +333,29 @@ describe("App hosted OAuth callback handling", () => {
});
expect(screen.queryByText("Servers Tab")).not.toBeInTheDocument();
});

it("shows a desktop return notice for Electron server OAuth browser callbacks", () => {
clearHostedOAuthPendingState();
clearSandboxSession();
localStorage.clear();
sessionStorage.clear();
window.history.replaceState(
{},
"",
"/oauth/callback?code=oauth-code&state=electron_mcp:test-state",
);

render(<App />);

expect(screen.getByText("Continue in MCPJam Desktop")).toBeInTheDocument();
expect(
screen.getByText(
/please close this page and continue in MCPJam Desktop/i,
),
).toBeInTheDocument();
expect(screen.getByRole("link", { name: /click here/i })).toHaveAttribute(
"href",
"mcpjam://oauth/callback?flow=mcp&code=oauth-code&state=electron_mcp%3Atest-state",
);
});
});
4 changes: 2 additions & 2 deletions mcpjam-inspector/client/src/components/ChatTabV2.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { FormEvent, useMemo, useState, useEffect, useCallback } from "react";
import { ArrowDown } from "lucide-react";
import { useAuth } from "@workos-inc/authkit-react";
import { useConvexAuth } from "convex/react";
import type { ContentBlock } from "@modelcontextprotocol/sdk/types.js";
import { ModelDefinition } from "@/shared/types";
Expand All @@ -20,6 +19,7 @@ import { MCPJamFreeModelsPrompt } from "@/components/chat-v2/mcpjam-free-models-
import { usePostHog } from "posthog-js/react";
import { detectEnvironment, detectPlatform } from "@/lib/PosthogUtils";
import { ErrorBox } from "@/components/chat-v2/error";
import { useElectronHostedAuth } from "@/hooks/useElectronHostedAuth";
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
import { type MCPPromptResult } from "@/components/chat-v2/chat-input/prompts/mcp-prompts-popover";
import type { SkillResult } from "@/components/chat-v2/chat-input/skills/skill-types";
Expand Down Expand Up @@ -100,7 +100,7 @@ export function ChatTabV2({
reasoningDisplayMode = "inline",
onOAuthRequired,
}: ChatTabProps) {
const { signUp } = useAuth();
const { signUp } = useElectronHostedAuth();
const { isAuthenticated: isConvexAuthenticated } = useConvexAuth();
const appState = useSharedAppState();
const { isVisible: isJsonRpcPanelVisible, toggle: toggleJsonRpcPanel } =
Expand Down
44 changes: 44 additions & 0 deletions mcpjam-inspector/client/src/components/OAuthFlowTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,42 @@ export const OAuthFlowTab = ({
}
};

const handleElectronOAuthCallback = (event: Event) => {
const callbackUrl = (event as CustomEvent<string>).detail;
if (!callbackUrl) {
return;
}

try {
const parsed = new URL(callbackUrl);
if (parsed.searchParams.get("flow") !== "debug") {
return;
}

const error = parsed.searchParams.get("error");
const errorDescription = parsed.searchParams.get("error_description");
if (error) {
if (exchangeTimeoutRef.current) {
clearTimeout(exchangeTimeoutRef.current);
exchangeTimeoutRef.current = null;
}

updateOAuthFlowState({
error: errorDescription ?? error,
});
return;
}

const code = parsed.searchParams.get("code");
const state = parsed.searchParams.get("state");
if (code) {
processOAuthCallback(code, state);
}
} catch (error) {
console.error("Failed to process Electron OAuth callback:", error);
}
};

let channel: BroadcastChannel | null = null;
try {
channel = new BroadcastChannel("oauth_callback_channel");
Expand All @@ -450,8 +486,16 @@ export const OAuthFlowTab = ({
}

window.addEventListener("message", handleMessage);
window.addEventListener(
"electron-oauth-callback",
handleElectronOAuthCallback as EventListener,
);
return () => {
window.removeEventListener("message", handleMessage);
window.removeEventListener(
"electron-oauth-callback",
handleElectronOAuthCallback as EventListener,
);
channel?.close();
};
}, [oauthStateMachine, updateOAuthFlowState]);
Expand Down
6 changes: 3 additions & 3 deletions mcpjam-inspector/client/src/components/OrganizationsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useState, useRef } from "react";
import { useConvexAuth } from "convex/react";
import { useAuth } from "@workos-inc/authkit-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { EditableText } from "@/components/ui/editable-text";
Expand Down Expand Up @@ -30,6 +29,7 @@ import {
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useElectronHostedAuth } from "@/hooks/useElectronHostedAuth";
import {
Organization,
OrganizationMember,
Expand All @@ -50,7 +50,7 @@ interface OrganizationsTabProps {
}

export function OrganizationsTab({ organizationId }: OrganizationsTabProps) {
const { user, signIn } = useAuth();
const { user, signIn } = useElectronHostedAuth();
const { isAuthenticated } = useConvexAuth();

const { sortedOrganizations, isLoading } = useOrganizationQueries({
Expand Down Expand Up @@ -136,7 +136,7 @@ interface OrganizationPageProps {

function OrganizationPage({ organization }: OrganizationPageProps) {
const { isAuthenticated } = useConvexAuth();
const { user } = useAuth();
const { user } = useElectronHostedAuth();
const currentUserEmail = user?.email;
const fileInputRef = useRef<HTMLInputElement>(null);

Expand Down
4 changes: 2 additions & 2 deletions mcpjam-inspector/client/src/components/ProfileTab.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { useRef, useState } from "react";
import { useAuth } from "@workos-inc/authkit-react";
import { useAction, useMutation, useQuery } from "convex/react";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { EditableText } from "@/components/ui/editable-text";
import { getInitials } from "@/lib/utils";
import { Camera, Loader2 } from "lucide-react";
import { useElectronHostedAuth } from "@/hooks/useElectronHostedAuth";
import { useProfilePicture } from "@/hooks/useProfilePicture";

export function ProfileTab() {
const { user, signIn } = useAuth();
const { user, signIn } = useElectronHostedAuth();
const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useAuth } from "@workos-inc/authkit-react";
import { useConvexAuth } from "convex/react";
import { usePostHog } from "posthog-js/react";
import { Button } from "@/components/ui/button";
Expand All @@ -10,6 +9,7 @@ import {
} from "@/components/ActiveServerSelector";
import { NotificationBell } from "@/components/notifications/NotificationBell";
import { detectEnvironment, detectPlatform } from "@/lib/PosthogUtils";
import { useElectronHostedAuth } from "@/hooks/useElectronHostedAuth";

interface AuthUpperAreaProps {
activeServerSelectorProps?: ActiveServerSelectorProps;
Expand All @@ -18,7 +18,7 @@ interface AuthUpperAreaProps {
export function AuthUpperArea({
activeServerSelectorProps,
}: AuthUpperAreaProps) {
const { user, signIn, signUp } = useAuth();
const { user, signIn, signUp } = useElectronHostedAuth();
const { isLoading } = useConvexAuth();
const posthog = usePostHog();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useAuth } from "@workos-inc/authkit-react";
import { useConvexAuth } from "convex/react";
import { Loader2, Link2Off, ShieldX } from "lucide-react";
import { toast } from "sonner";
Expand Down Expand Up @@ -28,6 +27,7 @@ import type { HostedOAuthRequiredDetails } from "@/lib/hosted-oauth-required";
import { slugify } from "@/lib/shared-server-session";
import { SandboxHostStyleProvider } from "@/contexts/sandbox-host-style-context";
import { getSandboxShellStyle } from "@/lib/sandbox-host-style";
import { useElectronHostedAuth } from "@/hooks/useElectronHostedAuth";

interface SandboxChatPageProps {
pathToken?: string | null;
Expand Down Expand Up @@ -248,7 +248,7 @@ export function SandboxChatPage({
pathToken,
onExitSandboxChat,
}: SandboxChatPageProps) {
const { getAccessToken, signIn } = useAuth();
const { getAccessToken, signIn } = useElectronHostedAuth();
const { isAuthenticated, isLoading: isAuthLoading } = useConvexAuth();
const themeMode = usePreferencesStore((s) => s.themeMode);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,35 @@ export const OAuthAuthorizationModal = ({
if (open && !hasOpenedRef.current) {
hasOpenedRef.current = true;

if (window.isElectron && window.electronAPI?.app?.openExternal) {
let cancelled = false;

void window.electronAPI.app
.openExternal(authorizationUrl)
.catch((error) => {
console.error(
"[OAuth Popup] Failed to open system browser:",
error,
);
})
.finally(() => {
if (cancelled) return;
onOpenChange(false);
hasOpenedRef.current = false;
});

return () => {
cancelled = true;
};
}

const width = 600;
const height = 700;
const left = window.screenX + (window.outerWidth - width) / 2;
const top = window.screenY + (window.outerHeight - height) / 2;

// Use unique window name each time to prevent reusing old popup with stale auth code
const uniqueWindowName = `oauth_authorization_${Date.now()}`;
console.log("authorizationUrl", authorizationUrl);
popupRef.current = window.open(
authorizationUrl,
uniqueWindowName,
Expand Down
Loading
Loading