Skip to content
Merged
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
8 changes: 4 additions & 4 deletions mcpjam-inspector/.env.local
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
VITE_CONVEX_URL=https://proper-clownfish-150.convex.cloud
CONVEX_URL=https://proper-clownfish-150.convex.cloud
VITE_CONVEX_URL=https://tough-cassowary-291.convex.cloud
CONVEX_URL=https://tough-cassowary-291.convex.cloud
VITE_WORKOS_CLIENT_ID=client_01K4C1TVA6CMQ3G32F1P301A9G
VITE_WORKOS_REDIRECT_URI=mcpjam://oauth/callback
CONVEX_HTTP_URL=https://proper-clownfish-150.convex.site
CONVEX_HTTP_URL=https://tough-cassowary-291.convex.site
ENVIRONMENT=local
VITE_DISABLE_POSTHOG_LOCAL=true
VITE_DISABLE_POSTHOG_LOCAL=true
217 changes: 185 additions & 32 deletions mcpjam-inspector/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ChatTabV2 } from "./components/ChatTabV2";
import { EvalsTab } from "./components/EvalsTab";
import { CiEvalsTab } from "./components/CiEvalsTab";
import { ViewsTab } from "./components/ViewsTab";
import { SandboxesTab } from "./components/SandboxesTab";
import { SettingsTab } from "./components/SettingsTab";
import { TracingTab } from "./components/TracingTab";
import { AuthTab } from "./components/AuthTab";
Expand Down Expand Up @@ -54,23 +55,55 @@ import {
SharedServerChatPage,
getSharedPathTokenFromLocation,
} from "./components/hosted/SharedServerChatPage";
import {
SandboxChatPage,
getSandboxPathTokenFromLocation,
} from "./components/hosted/SandboxChatPage";
import { useHostedApiContext } from "./hooks/hosted/use-hosted-api-context";
import { HOSTED_MODE } from "./lib/config";
import { resolveHostedNavigation } from "./lib/hosted-navigation";
import { buildOAuthTokensByServerId } from "./lib/oauth/oauth-tokens";
import {
clearHostedOAuthPendingState,
getHostedOAuthCallbackContext,
resolveHostedOAuthReturnHash,
} from "./lib/hosted-oauth-callback";
import {
clearSandboxSignInReturnPath,
readSandboxSession,
readSandboxSignInReturnPath,
writeSandboxSignInReturnPath,
} from "./lib/sandbox-session";
import {
clearSharedSignInReturnPath,
hasActiveSharedSession,
readSharedServerSession,
readSharedSignInReturnPath,
slugify,
SHARED_OAUTH_PENDING_KEY,
writeSharedSignInReturnPath,
readPendingServerAdd,
clearPendingServerAdd,
} from "./lib/shared-server-session";
import {
sanitizeHostedOAuthErrorMessage,
writeHostedOAuthResumeMarker,
} from "./lib/hosted-oauth-resume";
import { handleOAuthCallback } from "./lib/oauth/mcp-oauth";

function getHostedOAuthCallbackErrorMessage(): string {
const params = new URLSearchParams(window.location.search);
const error = params.get("error");
const description = params.get("error_description");

if (error === "access_denied" && !description) {
return "Authorization was cancelled. Try again.";
}

return sanitizeHostedOAuthErrorMessage(
description || error,
"Authorization could not be completed. Try again.",
);
}

export default function App() {
const [activeTab, setActiveTab] = useState("servers");
const [activeOrganizationId, setActiveOrganizationId] = useState<
Expand All @@ -88,38 +121,128 @@ export default function App() {
isLoading: isWorkOsLoading,
} = useAuth();
const { isAuthenticated, isLoading: isAuthLoading } = useConvexAuth();
const [sharedOAuthHandling, setSharedOAuthHandling] = useState(false);
const [hostedOAuthHandling, setHostedOAuthHandling] = useState(() =>
HOSTED_MODE ? getHostedOAuthCallbackContext() !== null : false,
);
const [exitedSharedChat, setExitedSharedChat] = useState(false);
const [exitedSandboxChat, setExitedSandboxChat] = useState(false);
const sharedPathToken = HOSTED_MODE ? getSharedPathTokenFromLocation() : null;
const sandboxPathToken = HOSTED_MODE
? getSandboxPathTokenFromLocation()
: null;
const sharedSession = HOSTED_MODE ? readSharedServerSession() : null;
const sandboxSession = HOSTED_MODE ? readSandboxSession() : null;
const currentHashSlug = window.location.hash
.replace(/^#/, "")
.replace(/^\/+/, "")
.split("/")[0];
const hostedRouteKind = useMemo(() => {
if (!HOSTED_MODE) {
return null;
}

if (sharedPathToken) {
return "shared" as const;
}
if (sandboxPathToken) {
return "sandbox" as const;
}

if (sharedSession && sandboxSession) {
if (currentHashSlug === slugify(sharedSession.payload.serverName)) {
return "shared" as const;
}
if (currentHashSlug === slugify(sandboxSession.payload.name)) {
return "sandbox" as const;
}
return null;
}

if (sharedSession) {
return "shared" as const;
}
if (sandboxSession) {
return "sandbox" as const;
}

return null;
}, [
currentHashSlug,
sandboxPathToken,
sandboxSession,
sharedPathToken,
sharedSession,
]);
const isSharedChatRoute =
HOSTED_MODE &&
!exitedSharedChat &&
(!!sharedPathToken || hasActiveSharedSession());
HOSTED_MODE && !exitedSharedChat && hostedRouteKind === "shared";
const isSandboxChatRoute =
HOSTED_MODE && !exitedSandboxChat && hostedRouteKind === "sandbox";
const isHostedChatRoute = isSharedChatRoute || isSandboxChatRoute;

// Handle shared OAuth callback: detect code + pending flag before normal rendering
// Handle hosted OAuth callback: claim the callback before any hosted page renders.
useEffect(() => {
const callbackContext = getHostedOAuthCallbackContext();
if (!callbackContext) return;

const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
if (!code || !localStorage.getItem(SHARED_OAUTH_PENDING_KEY)) return;
const error = urlParams.get("error");

let cancelled = false;
setSharedOAuthHandling(true);
setHostedOAuthHandling(true);

const cleanupOAuth = () => {
const finalizeHostedOAuth = (errorMessage?: string | null) => {
if (cancelled) return;
localStorage.removeItem(SHARED_OAUTH_PENDING_KEY);
const storedSession = readSharedServerSession();
const sharedHash = storedSession
? slugify(storedSession.payload.serverName)
: "shared";
window.history.replaceState({}, "", `/#${sharedHash}`);
if (callbackContext.serverName) {
writeHostedOAuthResumeMarker({
surface: callbackContext.surface,
serverName: callbackContext.serverName,
serverUrl: callbackContext.serverUrl,
errorMessage:
errorMessage && errorMessage.trim() ? errorMessage : null,
});
}

clearHostedOAuthPendingState();
localStorage.removeItem("mcp-oauth-pending");
localStorage.removeItem("mcp-oauth-return-hash");
window.history.replaceState(
{},
"",
`/${resolveHostedOAuthReturnHash(callbackContext)}`,
);
};

if (error || !code) {
finalizeHostedOAuth(getHostedOAuthCallbackErrorMessage());
setHostedOAuthHandling(false);
return;
}

handleOAuthCallback(code)
.then(cleanupOAuth)
.catch(cleanupOAuth)
.then((result) => {
if (result.success) {
finalizeHostedOAuth(null);
return;
}

finalizeHostedOAuth(
sanitizeHostedOAuthErrorMessage(
result.error,
"Authorization could not be completed. Try again.",
),
);
})
.catch((callbackError) => {
finalizeHostedOAuth(
sanitizeHostedOAuthErrorMessage(
callbackError,
"Authorization could not be completed. Try again.",
),
);
})
.finally(() => {
if (!cancelled) setSharedOAuthHandling(false);
if (!cancelled) setHostedOAuthHandling(false);
});

return () => {
Expand Down Expand Up @@ -167,9 +290,15 @@ export default function App() {

// Let AuthKit + Convex auth settle before leaving /callback.
if (!isAuthLoading && isAuthenticated) {
const sandboxReturnPath = readSandboxSignInReturnPath();
const sharedReturnPath = readSharedSignInReturnPath();
clearSandboxSignInReturnPath();
clearSharedSignInReturnPath();
window.history.replaceState({}, "", sharedReturnPath ?? "/");
window.history.replaceState(
{},
"",
sandboxReturnPath ?? sharedReturnPath ?? "/",
);
setCallbackCompleted(true);
setCallbackRecoveryExpired(false);
return;
Expand Down Expand Up @@ -212,7 +341,7 @@ export default function App() {

// Auto-add a shared server when returning from SharedServerChatPage via "Open MCPJam"
useEffect(() => {
if (isSharedChatRoute) return;
if (isHostedChatRoute) return;
if (isLoadingRemoteWorkspaces) return;
if (isAuthLoading) return;

Expand All @@ -233,7 +362,7 @@ export default function App() {
oauthScopes: pending.oauthScopes ?? undefined,
});
}, [
isSharedChatRoute,
isHostedChatRoute,
isLoadingRemoteWorkspaces,
isAuthLoading,
workspaceServers,
Expand Down Expand Up @@ -306,7 +435,7 @@ export default function App() {
guestOauthTokensByServerName,
isAuthenticated,
serverConfigs: guestServerConfigs,
enabled: !isSharedChatRoute,
enabled: !isHostedChatRoute,
});

// Compute the set of server names that have saved views
Expand Down Expand Up @@ -337,6 +466,17 @@ export default function App() {
return;
}

if (isSandboxChatRoute) {
const storedSession = readSandboxSession();
if (storedSession) {
const expectedHash = slugify(storedSession.payload.name);
if (window.location.hash !== `#${expectedHash}`) {
window.location.hash = expectedHash;
}
}
return;
}

const resolved = resolveHostedNavigation(target, HOSTED_MODE);

if (
Expand Down Expand Up @@ -374,12 +514,16 @@ export default function App() {
}
setActiveTab(resolved.normalizedTab);
},
[isSharedChatRoute, setSelectedMultipleServersToAllServers],
[
isSandboxChatRoute,
isSharedChatRoute,
setSelectedMultipleServersToAllServers,
],
);

// Sync tab with hash on mount and when hash changes
useEffect(() => {
if (isSharedChatRoute) {
if (isHostedChatRoute) {
return;
}

Expand All @@ -390,7 +534,7 @@ export default function App() {
applyHash();
window.addEventListener("hashchange", applyHash);
return () => window.removeEventListener("hashchange", applyHash);
}, [applyNavigation, isSharedChatRoute]);
}, [applyNavigation, isHostedChatRoute]);

// Redirect away from tabs hidden by the ci-evals feature flag.
// Use strict equality to avoid redirecting while the flag is still loading (undefined).
Expand All @@ -410,7 +554,7 @@ export default function App() {
return <OAuthDebugCallback />;
}

if (sharedOAuthHandling) {
if (hostedOAuthHandling) {
return <LoadingScreen />;
}

Expand Down Expand Up @@ -447,7 +591,7 @@ export default function App() {
return <CompletingSignInLoading />;
}

if (isLoading && !isSharedChatRoute) {
if (isLoading && !isHostedChatRoute) {
return <LoadingScreen />;
}

Expand All @@ -459,7 +603,7 @@ export default function App() {
hasWorkOsUser: !!workOsUser,
isLoadingRemoteWorkspaces,
});
const sharedHostedShellGateState = resolveHostedShellGateState({
const hostedChatShellGateState = resolveHostedShellGateState({
hostedMode: HOSTED_MODE,
isConvexAuthLoading: isAuthLoading,
isConvexAuthenticated: isAuthenticated,
Expand Down Expand Up @@ -554,6 +698,9 @@ export default function App() {
onLeaveWorkspace={() => handleLeaveWorkspace(activeWorkspaceId)}
/>
)}
{activeTab === "sandboxes" && (
<SandboxesTab workspaceId={convexWorkspaceId} />
)}
{activeTab === "resources" && (
<div className="h-full overflow-hidden">
<ResourcesTab
Expand Down Expand Up @@ -647,14 +794,15 @@ export default function App() {
<Toaster />
<HostedShellGate
state={
isSharedChatRoute
? sharedHostedShellGateState
: hostedShellGateState
isHostedChatRoute ? hostedChatShellGateState : hostedShellGateState
}
onSignIn={() => {
if (sharedPathToken) {
writeSharedSignInReturnPath(window.location.pathname);
}
if (sandboxPathToken) {
writeSandboxSignInReturnPath(window.location.pathname);
}
signIn();
}}
>
Expand All @@ -663,6 +811,11 @@ export default function App() {
pathToken={sharedPathToken}
onExitSharedChat={() => setExitedSharedChat(true)}
/>
) : isSandboxChatRoute ? (
<SandboxChatPage
pathToken={sandboxPathToken}
onExitSandboxChat={() => setExitedSandboxChat(true)}
/>
) : (
appContent
)}
Expand Down
Loading