@@ -3,6 +3,7 @@ import { useConnection } from "../useConnection";
33import { z } from "zod" ;
44import { ClientRequest } from "@modelcontextprotocol/sdk/types.js" ;
55import { DEFAULT_INSPECTOR_CONFIG } from "../../constants" ;
6+ import { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js" ;
67
78// Mock fetch
89global . fetch = jest . fn ( ) . mockResolvedValue ( {
@@ -23,21 +24,46 @@ const mockClient = {
2324 setRequestHandler : jest . fn ( ) ,
2425} ;
2526
27+ // Mock transport instances
28+ const mockSSETransport : {
29+ start : jest . Mock ;
30+ url : URL | undefined ;
31+ options : SSEClientTransportOptions | undefined ;
32+ } = {
33+ start : jest . fn ( ) ,
34+ url : undefined ,
35+ options : undefined ,
36+ } ;
37+
38+ const mockStreamableHTTPTransport : {
39+ start : jest . Mock ;
40+ url : URL | undefined ;
41+ options : SSEClientTransportOptions | undefined ;
42+ } = {
43+ start : jest . fn ( ) ,
44+ url : undefined ,
45+ options : undefined ,
46+ } ;
47+
2648jest . mock ( "@modelcontextprotocol/sdk/client/index.js" , ( ) => ( {
2749 Client : jest . fn ( ) . mockImplementation ( ( ) => mockClient ) ,
2850} ) ) ;
2951
3052jest . mock ( "@modelcontextprotocol/sdk/client/sse.js" , ( ) => ( {
31- SSEClientTransport : jest . fn ( ( url ) => ( {
32- toString : ( ) => url ,
33- } ) ) ,
53+ SSEClientTransport : jest . fn ( ( url , options ) => {
54+ mockSSETransport . url = url ;
55+ mockSSETransport . options = options ;
56+ return mockSSETransport ;
57+ } ) ,
3458 SseError : jest . fn ( ) ,
3559} ) ) ;
3660
3761jest . mock ( "@modelcontextprotocol/sdk/client/streamableHttp.js" , ( ) => ( {
38- StreamableHTTPClientTransport : jest . fn ( ( url ) => ( {
39- toString : ( ) => url ,
40- } ) ) ,
62+ StreamableHTTPClientTransport : jest . fn ( ( url , options ) => {
63+ mockStreamableHTTPTransport . url = url ;
64+ mockStreamableHTTPTransport . options = options ;
65+ return mockStreamableHTTPTransport ;
66+ } ) ,
4167} ) ) ;
4268
4369jest . mock ( "@modelcontextprotocol/sdk/client/auth.js" , ( ) => ( {
@@ -259,4 +285,172 @@ describe("useConnection", () => {
259285 ) ;
260286 } ) ;
261287 } ) ;
288+
289+ describe ( "Proxy Authentication Headers" , ( ) => {
290+ beforeEach ( ( ) => {
291+ jest . clearAllMocks ( ) ;
292+ // Reset the mock transport objects
293+ mockSSETransport . url = undefined ;
294+ mockSSETransport . options = undefined ;
295+ mockStreamableHTTPTransport . url = undefined ;
296+ mockStreamableHTTPTransport . options = undefined ;
297+ } ) ;
298+
299+ test ( "sends X-MCP-Proxy-Auth header when proxy auth token is configured" , async ( ) => {
300+ const propsWithProxyAuth = {
301+ ...defaultProps ,
302+ config : {
303+ ...DEFAULT_INSPECTOR_CONFIG ,
304+ MCP_PROXY_AUTH_TOKEN : {
305+ ...DEFAULT_INSPECTOR_CONFIG . MCP_PROXY_AUTH_TOKEN ,
306+ value : "test-proxy-token" ,
307+ } ,
308+ } ,
309+ } ;
310+
311+ const { result } = renderHook ( ( ) => useConnection ( propsWithProxyAuth ) ) ;
312+
313+ await act ( async ( ) => {
314+ await result . current . connect ( ) ;
315+ } ) ;
316+
317+ // Check that the transport was created with the correct headers
318+ expect ( mockSSETransport . options ) . toBeDefined ( ) ;
319+ expect ( mockSSETransport . options ?. requestInit ) . toBeDefined ( ) ;
320+
321+ expect ( mockSSETransport . options ?. requestInit ?. headers ) . toHaveProperty (
322+ "X-MCP-Proxy-Auth" ,
323+ "Bearer test-proxy-token" ,
324+ ) ;
325+ expect ( mockSSETransport ?. options ?. eventSourceInit ?. fetch ) . toBeDefined ( ) ;
326+
327+ // Verify the fetch function includes the proxy auth header
328+ const mockFetch = mockSSETransport . options ?. eventSourceInit ?. fetch ;
329+ const testUrl = "http://test.com" ;
330+ await mockFetch ?.( testUrl ) ;
331+
332+ expect ( global . fetch ) . toHaveBeenCalledTimes ( 2 ) ;
333+ expect (
334+ ( global . fetch as jest . Mock ) . mock . calls [ 0 ] [ 1 ] . headers ,
335+ ) . toHaveProperty ( "X-MCP-Proxy-Auth" , "Bearer test-proxy-token" ) ;
336+ expect ( ( global . fetch as jest . Mock ) . mock . calls [ 1 ] [ 0 ] ) . toBe ( testUrl ) ;
337+ expect (
338+ ( global . fetch as jest . Mock ) . mock . calls [ 1 ] [ 1 ] . headers ,
339+ ) . toHaveProperty ( "X-MCP-Proxy-Auth" , "Bearer test-proxy-token" ) ;
340+ } ) ;
341+
342+ test ( "does NOT send Authorization header for proxy auth" , async ( ) => {
343+ const propsWithProxyAuth = {
344+ ...defaultProps ,
345+ config : {
346+ ...DEFAULT_INSPECTOR_CONFIG ,
347+ proxyAuthToken : "test-proxy-token" ,
348+ } ,
349+ } ;
350+
351+ const { result } = renderHook ( ( ) => useConnection ( propsWithProxyAuth ) ) ;
352+
353+ await act ( async ( ) => {
354+ await result . current . connect ( ) ;
355+ } ) ;
356+
357+ // Check that Authorization header is NOT used for proxy auth
358+ expect ( mockSSETransport . options ?. requestInit ?. headers ) . not . toHaveProperty (
359+ "Authorization" ,
360+ "Bearer test-proxy-token" ,
361+ ) ;
362+ } ) ;
363+
364+ test ( "preserves server Authorization header when proxy auth is configured" , async ( ) => {
365+ const propsWithBothAuth = {
366+ ...defaultProps ,
367+ bearerToken : "server-auth-token" ,
368+ config : {
369+ ...DEFAULT_INSPECTOR_CONFIG ,
370+ MCP_PROXY_AUTH_TOKEN : {
371+ ...DEFAULT_INSPECTOR_CONFIG . MCP_PROXY_AUTH_TOKEN ,
372+ value : "test-proxy-token" ,
373+ } ,
374+ } ,
375+ } ;
376+
377+ const { result } = renderHook ( ( ) => useConnection ( propsWithBothAuth ) ) ;
378+
379+ await act ( async ( ) => {
380+ await result . current . connect ( ) ;
381+ } ) ;
382+
383+ // Check that both headers are present and distinct
384+ const headers = mockSSETransport . options ?. requestInit ?. headers ;
385+ expect ( headers ) . toHaveProperty (
386+ "Authorization" ,
387+ "Bearer server-auth-token" ,
388+ ) ;
389+ expect ( headers ) . toHaveProperty (
390+ "X-MCP-Proxy-Auth" ,
391+ "Bearer test-proxy-token" ,
392+ ) ;
393+ } ) ;
394+
395+ test ( "sends X-MCP-Proxy-Auth in health check requests" , async ( ) => {
396+ const fetchMock = global . fetch as jest . Mock ;
397+ fetchMock . mockClear ( ) ;
398+
399+ const propsWithProxyAuth = {
400+ ...defaultProps ,
401+ config : {
402+ ...DEFAULT_INSPECTOR_CONFIG ,
403+ MCP_PROXY_AUTH_TOKEN : {
404+ ...DEFAULT_INSPECTOR_CONFIG . MCP_PROXY_AUTH_TOKEN ,
405+ value : "test-proxy-token" ,
406+ } ,
407+ } ,
408+ } ;
409+
410+ const { result } = renderHook ( ( ) => useConnection ( propsWithProxyAuth ) ) ;
411+
412+ await act ( async ( ) => {
413+ await result . current . connect ( ) ;
414+ } ) ;
415+
416+ // Find the health check call
417+ const healthCheckCall = fetchMock . mock . calls . find (
418+ ( call ) => call [ 0 ] . pathname === "/health" ,
419+ ) ;
420+
421+ expect ( healthCheckCall ) . toBeDefined ( ) ;
422+ expect ( healthCheckCall [ 1 ] . headers ) . toHaveProperty (
423+ "X-MCP-Proxy-Auth" ,
424+ "Bearer test-proxy-token" ,
425+ ) ;
426+ } ) ;
427+
428+ test ( "works correctly with streamable-http transport" , async ( ) => {
429+ const propsWithStreamableHttp = {
430+ ...defaultProps ,
431+ transportType : "streamable-http" as const ,
432+ config : {
433+ ...DEFAULT_INSPECTOR_CONFIG ,
434+ MCP_PROXY_AUTH_TOKEN : {
435+ ...DEFAULT_INSPECTOR_CONFIG . MCP_PROXY_AUTH_TOKEN ,
436+ value : "test-proxy-token" ,
437+ } ,
438+ } ,
439+ } ;
440+
441+ const { result } = renderHook ( ( ) =>
442+ useConnection ( propsWithStreamableHttp ) ,
443+ ) ;
444+
445+ await act ( async ( ) => {
446+ await result . current . connect ( ) ;
447+ } ) ;
448+
449+ // Check that the streamable HTTP transport was created with the correct headers
450+ expect ( mockStreamableHTTPTransport . options ) . toBeDefined ( ) ;
451+ expect (
452+ mockStreamableHTTPTransport . options ?. requestInit ?. headers ,
453+ ) . toHaveProperty ( "X-MCP-Proxy-Auth" , "Bearer test-proxy-token" ) ;
454+ } ) ;
455+ } ) ;
262456} ) ;
0 commit comments