@@ -3,6 +3,7 @@ import { useConnection } from "../useConnection";
3
3
import { z } from "zod" ;
4
4
import { ClientRequest } from "@modelcontextprotocol/sdk/types.js" ;
5
5
import { DEFAULT_INSPECTOR_CONFIG } from "../../constants" ;
6
+ import { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js" ;
6
7
7
8
// Mock fetch
8
9
global . fetch = jest . fn ( ) . mockResolvedValue ( {
@@ -23,21 +24,46 @@ const mockClient = {
23
24
setRequestHandler : jest . fn ( ) ,
24
25
} ;
25
26
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
+
26
48
jest . mock ( "@modelcontextprotocol/sdk/client/index.js" , ( ) => ( {
27
49
Client : jest . fn ( ) . mockImplementation ( ( ) => mockClient ) ,
28
50
} ) ) ;
29
51
30
52
jest . 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
+ } ) ,
34
58
SseError : jest . fn ( ) ,
35
59
} ) ) ;
36
60
37
61
jest . 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
+ } ) ,
41
67
} ) ) ;
42
68
43
69
jest . mock ( "@modelcontextprotocol/sdk/client/auth.js" , ( ) => ( {
@@ -259,4 +285,172 @@ describe("useConnection", () => {
259
285
) ;
260
286
} ) ;
261
287
} ) ;
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
+ } ) ;
262
456
} ) ;
0 commit comments