mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
feat(ui): connect-flow interlude banner on the MCP apps grid for gateway DCR sign-in
This commit is contained in:
parent
5df5fa50fa
commit
262e93ec27
3 changed files with 97 additions and 0 deletions
|
|
@ -4,6 +4,7 @@ import { Suspense, useEffect } from "react";
|
|||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useChatShell } from "@/contexts/ChatShellContext";
|
||||
import MCPAppsPanel from "@/components/chat/MCPAppsPanel";
|
||||
import ConnectFlowBanner from "@/components/chat/ConnectFlowBanner";
|
||||
|
||||
// useSearchParams() requires a Suspense boundary for static export.
|
||||
function IntegrationsPageContent() {
|
||||
|
|
@ -11,6 +12,13 @@ function IntegrationsPageContent() {
|
|||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const oauthReturn = searchParams.get("mcpOauthReturn");
|
||||
// Set by the gateway DCR authorize when a DCR client sends the user here to
|
||||
// authorize servers before finishing sign-in (see gateway_dcr_flow.py). The
|
||||
// handle keys the sealed per-flow cookie; connect_client is the client origin
|
||||
// for display only. connect_flow is NOT cleaned from the URL: the finish form
|
||||
// needs it, and the sealed cookie (not the URL) is the security boundary.
|
||||
const connectFlow = searchParams.get("connect_flow");
|
||||
const connectClient = searchParams.get("connect_client");
|
||||
|
||||
// Clean up the OAuth return param after it's been consumed — real routing means
|
||||
// we no longer need it to pick a tab, but it should not linger in the address bar.
|
||||
|
|
@ -24,6 +32,7 @@ function IntegrationsPageContent() {
|
|||
|
||||
return (
|
||||
<div className="flex-1 min-h-0 overflow-auto w-full py-8 px-8">
|
||||
{connectFlow && <ConnectFlowBanner flowHandle={connectFlow} clientOrigin={connectClient} />}
|
||||
<MCPAppsPanel accessToken={accessToken} selectedServers={selectedMCPServers} onChange={setSelectedMCPServers} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import ConnectFlowBanner from "./ConnectFlowBanner";
|
||||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
getProxyBaseUrl: () => "https://gateway.example.com",
|
||||
}));
|
||||
|
||||
describe("ConnectFlowBanner", () => {
|
||||
it("posts the flow handle to the proxy /authorize/complete as a full-page form", () => {
|
||||
const { container } = render(<ConnectFlowBanner flowHandle="flow-handle-123" clientOrigin="https://claude.ai" />);
|
||||
|
||||
const form = container.querySelector("form")!;
|
||||
expect(form.getAttribute("method")).toBe("POST");
|
||||
expect(form.getAttribute("action")).toBe("https://gateway.example.com/authorize/complete");
|
||||
|
||||
const hidden = form.querySelector('input[name="flow"]') as HTMLInputElement;
|
||||
expect(hidden.value).toBe("flow-handle-123");
|
||||
// No token, code, or secret is ever placed in the form; the sealed cookie carries them.
|
||||
expect(form.innerHTML).not.toContain("token");
|
||||
});
|
||||
|
||||
it("shows the client origin so the user knows what they are connecting to", () => {
|
||||
render(<ConnectFlowBanner flowHandle="h" clientOrigin="https://claude.ai" />);
|
||||
expect(screen.getAllByText(/claude\.ai/).length).toBeGreaterThan(0);
|
||||
expect(screen.getByRole("button", { name: /finish connecting/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("falls back to a generic label when the client origin is unknown", () => {
|
||||
render(<ConnectFlowBanner flowHandle="h" clientOrigin={null} />);
|
||||
expect(screen.getAllByText(/the application/).length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { CheckCircle } from "lucide-react";
|
||||
import { getProxyBaseUrl } from "@/components/networking";
|
||||
|
||||
interface Props {
|
||||
flowHandle: string;
|
||||
clientOrigin: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The interlude shown when a DCR client (Claude Desktop, MCP Inspector) sends the user
|
||||
* through the gateway sign-in and lands them on the apps grid to authorize servers. The
|
||||
* grid below authorizes individual servers into the per-user vault; this banner is the
|
||||
* deliberate finish step.
|
||||
*
|
||||
* "Finish connecting" is a native form POST to the proxy's /authorize/complete, not a
|
||||
* fetch: the endpoint 303-redirects the browser back to the DCR client's own redirect URI
|
||||
* with the gateway authorization code, and only a full-page navigation carries the
|
||||
* HttpOnly per-flow cookie and follows that cross-origin redirect. The flow handle is the
|
||||
* only field; the sealed flow cookie set at /authorize holds everything else.
|
||||
*/
|
||||
const ConnectFlowBanner: React.FC<Props> = ({ flowHandle, clientOrigin }) => {
|
||||
const action = `${getProxyBaseUrl()}/authorize/complete`;
|
||||
const clientLabel = clientOrigin ?? "the application";
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-primary/30 bg-primary/5 px-5 py-4">
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div className="flex items-start gap-3 min-w-0">
|
||||
<CheckCircle className="h-5 w-5 text-primary shrink-0 mt-0.5" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-semibold text-foreground">Connect your MCP servers to {clientLabel}</p>
|
||||
<p className="text-[13px] text-muted-foreground mt-0.5">
|
||||
Authorize the servers you want to use below. When you are ready, finish connecting and you will be
|
||||
returned to {clientLabel}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action={action} className="shrink-0">
|
||||
<input type="hidden" name="flow" value={flowHandle} />
|
||||
<button
|
||||
type="submit"
|
||||
className="h-[38px] rounded-md bg-primary px-4 text-sm font-semibold text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Finish connecting
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectFlowBanner;
|
||||
Loading…
Add table
Reference in a new issue