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: 13 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 [credentialsInclude, setCredentialsInclude] = useState<boolean>(() => {
return localStorage.getItem("lastCredentialsInclude") === "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,
credentialsInclude,
onNotification: (notification) => {
setNotifications((prev) => [...prev, notification as ServerNotification]);

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

useEffect(() => {
localStorage.setItem(
"lastCredentialsInclude",
credentialsInclude ? "true" : "false",
);
}, [credentialsInclude]);

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

const connectionTypeTip =
"Connect to server directly (requires CORS config on server) or via MCP Inspector Proxy";
const credentialsIncludeTip =
"Send browser cookies with direct requests (credentials: 'include'). The target server must respond with Access-Control-Allow-Credentials: true and a non-wildcard Access-Control-Allow-Origin, and any cross-site cookies must use SameSite=None; Secure.";
// Reusable error reporter for copy actions
const reportError = useCallback(
(error: unknown) => {
Expand Down Expand Up @@ -361,6 +368,33 @@ const Sidebar = ({
</TooltipTrigger>
<TooltipContent>{connectionTypeTip}</TooltipContent>
</Tooltip>

{/* Send-cookies toggle - only applies to the direct transport */}
{connectionType === "direct" && (
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center space-x-2">
<Checkbox
id="credentials-include-checkbox"
checked={credentialsInclude}
onCheckedChange={(checked: boolean) =>
setCredentialsInclude(checked === true)
}
data-testid="credentials-include-checkbox"
/>
<label
className="text-sm font-medium cursor-pointer"
htmlFor="credentials-include-checkbox"
>
Send cookies (credentials: include)
</label>
</div>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
{credentialsIncludeTip}
</TooltipContent>
</Tooltip>
)}
</>
)}

Expand Down
58 changes: 58 additions & 0 deletions client/src/lib/hooks/__tests__/useConnection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,64 @@ describe("useConnection", () => {
});
});

describe("Credentials Include (direct connection)", () => {
beforeEach(() => {
jest.clearAllMocks();
mockSSETransport.url = undefined;
mockSSETransport.options = undefined;
});

test("passes credentials: 'include' to direct fetch when credentialsInclude is true", async () => {
const props = {
...defaultProps,
connectionType: "direct" as const,
credentialsInclude: true,
};

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

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

const mockFetch = mockSSETransport.options?.fetch;
expect(mockFetch).toBeDefined();

await mockFetch?.("http://test.com", {
headers: { Accept: "text/event-stream" },
});

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

test("omits credentials on direct fetch when credentialsInclude is unset", async () => {
const props = {
...defaultProps,
connectionType: "direct" as const,
};

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

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

const mockFetch = mockSSETransport.options?.fetch;
expect(mockFetch).toBeDefined();

await mockFetch?.("http://test.com", {
headers: { Accept: "text/event-stream" },
});

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

describe("Proxy Authentication Headers", () => {
beforeEach(() => {
jest.clearAllMocks();
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 @@ -102,6 +102,10 @@ interface UseConnectionOptions {
defaultLoggingLevel?: LoggingLevel;
serverImplementation?: Implementation;
metadata?: Record<string, string>;
// When true, pass `credentials: 'include'` on direct-connection fetches so
// the browser attaches cookies stored for the target origin. Applies to the
// direct transport only; proxy connections are unaffected.
credentialsInclude?: boolean;
}

export function useConnection({
Expand All @@ -116,6 +120,7 @@ export function useConnection({
oauthScope,
config,
connectionType = "proxy",
credentialsInclude = false,
onNotification,
onPendingRequest,
onElicitationRequest,
Expand Down Expand Up @@ -591,6 +596,7 @@ export function useConnection({
const response = await fetch(url, {
...init,
headers: requestHeaders,
...(credentialsInclude && { credentials: "include" }),
});

// Capture protocol-related headers from response
Expand All @@ -616,6 +622,7 @@ export function useConnection({
const response = await fetch(url, {
headers: requestHeaders,
...init,
...(credentialsInclude && { credentials: "include" }),
});

// Capture protocol-related headers from response
Expand Down