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
10 changes: 10 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ const App = () => {
);
},
);
const [includeCookies, setIncludeCookies] = useState<boolean>(() => {
return localStorage.getItem("lastIncludeCookies") === "true";
});
const [logLevel, setLogLevel] = useState<LoggingLevel>("debug");
const [notifications, setNotifications] = useState<ServerNotification[]>([]);
const [roots, setRoots] = useState<Root[]>([]);
Expand Down Expand Up @@ -401,6 +404,7 @@ const App = () => {
oauthScope,
config,
connectionType,
includeCookies,
onNotification: (notification) => {
setNotifications((prev) => [...prev, notification as ServerNotification]);

Expand Down Expand Up @@ -550,6 +554,10 @@ const App = () => {
localStorage.setItem("lastConnectionType", connectionType);
}, [connectionType]);

useEffect(() => {
localStorage.setItem("lastIncludeCookies", String(includeCookies));
}, [includeCookies]);

useEffect(() => {
if (bearerToken) {
localStorage.setItem("lastBearerToken", bearerToken);
Expand Down Expand Up @@ -1406,6 +1414,8 @@ const App = () => {
loggingSupported={!!serverCapabilities?.logging || false}
connectionType={connectionType}
setConnectionType={setConnectionType}
includeCookies={includeCookies}
setIncludeCookies={setIncludeCookies}
serverImplementation={serverImplementation}
/>
<div
Expand Down
37 changes: 37 additions & 0 deletions client/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import {
Select,
SelectContent,
Expand Down Expand Up @@ -75,6 +76,8 @@ interface SidebarProps {
setConfig: (config: InspectorConfig) => void;
connectionType: "direct" | "proxy";
setConnectionType: (type: "direct" | "proxy") => void;
includeCookies: boolean;
setIncludeCookies: (includeCookies: boolean) => void;
serverImplementation?:
| (WithIcons & { name?: string; version?: string; websiteUrl?: string })
| null;
Expand Down Expand Up @@ -109,6 +112,8 @@ const Sidebar = ({
setConfig,
connectionType,
setConnectionType,
includeCookies,
setIncludeCookies,
serverImplementation,
}: SidebarProps) => {
const [theme, setTheme] = useTheme();
Expand Down Expand Up @@ -361,6 +366,38 @@ const Sidebar = ({
</TooltipTrigger>
<TooltipContent>{connectionTypeTip}</TooltipContent>
</Tooltip>

{transportType === "streamable-http" &&
connectionType === "direct" && (
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-start space-x-2 rounded-md border p-3">
<Checkbox
id="include-cookies-checkbox"
checked={includeCookies}
onCheckedChange={(checked: boolean) =>
setIncludeCookies(Boolean(checked))
}
/>
<label
htmlFor="include-cookies-checkbox"
className="text-sm leading-snug"
>
Send cookies
<span className="block text-xs text-muted-foreground">
Uses credentials: include for direct Streamable HTTP
requests.
</span>
</label>
</div>
</TooltipTrigger>
<TooltipContent>
Target servers must allow credentials with a non-wildcard
Access-Control-Allow-Origin and
Access-Control-Allow-Credentials: true.
</TooltipContent>
</Tooltip>
)}
</>
)}

Expand Down
38 changes: 38 additions & 0 deletions client/src/components/__tests__/Sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ describe("Sidebar", () => {
setConfig: jest.fn(),
connectionType: "proxy" as const,
setConnectionType: jest.fn(),
includeCookies: false,
setIncludeCookies: jest.fn(),
};

const renderSidebar = (props = {}) => {
Expand Down Expand Up @@ -632,6 +634,42 @@ describe("Sidebar", () => {
});
});

describe("Connection settings", () => {
it("shows the cookie toggle for direct Streamable HTTP connections", () => {
renderSidebar({
transportType: "streamable-http",
connectionType: "direct",
});

expect(screen.getByText("Send cookies")).toBeInTheDocument();
expect(
screen.getByRole("checkbox", { name: /send cookies/i }),
).toHaveAttribute("aria-checked", "false");
});

it("does not show the cookie toggle for proxy connections", () => {
renderSidebar({
transportType: "streamable-http",
connectionType: "proxy",
});

expect(screen.queryByText("Send cookies")).not.toBeInTheDocument();
});

it("updates the cookie toggle when clicked", () => {
const setIncludeCookies = jest.fn();
renderSidebar({
transportType: "streamable-http",
connectionType: "direct",
setIncludeCookies,
});

fireEvent.click(screen.getByRole("checkbox", { name: /send cookies/i }));

expect(setIncludeCookies).toHaveBeenCalledWith(true);
});
});

describe("Authentication", () => {
const openAuthSection = () => {
const button = screen.getByTestId("auth-button");
Expand Down
53 changes: 53 additions & 0 deletions client/src/lib/hooks/__tests__/useConnection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1414,6 +1414,59 @@ describe("useConnection", () => {
expect(mockSSETransport.url?.toString()).toBe("http://localhost:8080/");
});

test("includes browser cookies for direct streamable-http when enabled", async () => {
const directProps = {
...defaultProps,
transportType: "streamable-http" as const,
connectionType: "direct" as const,
includeCookies: true,
};

const { result } = renderHook(() => useConnection(directProps));

await act(async () => {
await result.current.connect();
});

expect(mockStreamableHTTPTransport.options?.requestInit).toHaveProperty(
"credentials",
"include",
);

const mockFetch = mockStreamableHTTPTransport.options?.fetch;
await mockFetch?.("http://test.com/mcp", { cache: "no-store" });

expect((global.fetch as jest.Mock).mock.calls[0][1]).toHaveProperty(
"credentials",
"include",
);
});

test("does not include browser cookies for direct streamable-http by default", async () => {
const directProps = {
...defaultProps,
transportType: "streamable-http" as const,
connectionType: "direct" as const,
};

const { result } = renderHook(() => useConnection(directProps));

await act(async () => {
await result.current.connect();
});

expect(
mockStreamableHTTPTransport.options?.requestInit,
).not.toHaveProperty("credentials");

const mockFetch = mockStreamableHTTPTransport.options?.fetch;
await mockFetch?.("http://test.com/mcp", { cache: "no-store" });

expect((global.fetch as jest.Mock).mock.calls[0][1]).not.toHaveProperty(
"credentials",
);
});

test("uses proxy server URL when connectionType is 'proxy'", async () => {
const proxyProps = {
...defaultProps,
Expand Down
7 changes: 7 additions & 0 deletions client/src/lib/hooks/useConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ interface UseConnectionOptions {
oauthScope?: string;
config: InspectorConfig;
connectionType?: "direct" | "proxy";
includeCookies?: boolean;
onNotification?: (notification: Notification) => void;
onStdErrNotification?: (notification: Notification) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -116,6 +117,7 @@ export function useConnection({
oauthScope,
config,
connectionType = "proxy",
includeCookies = false,
onNotification,
onPendingRequest,
onElicitationRequest,
Expand Down Expand Up @@ -604,6 +606,9 @@ export function useConnection({
break;

case "streamable-http":
const credentialsInit = includeCookies
? { credentials: "include" as RequestCredentials }
: {};
transportOptions = {
authProvider: serverAuthProvider,
fetch: async (
Expand All @@ -616,6 +621,7 @@ export function useConnection({
const response = await fetch(url, {
headers: requestHeaders,
...init,
...credentialsInit,
});

// Capture protocol-related headers from response
Expand All @@ -625,6 +631,7 @@ export function useConnection({
},
requestInit: {
headers: requestHeaders,
...credentialsInit,
},
// TODO these should be configurable...
reconnectionOptions: {
Expand Down