@@ -5,6 +5,7 @@ import { EventStore, StreamableHTTPServerTransport, EventId, StreamId } from "./
5
5
import { McpServer } from "./mcp.js" ;
6
6
import { CallToolResult , JSONRPCMessage } from "../types.js" ;
7
7
import { z } from "zod" ;
8
+ import { AuthInfo } from "./auth/types.js" ;
8
9
9
10
/**
10
11
* Test server configuration for StreamableHTTPServerTransport tests
@@ -70,6 +71,61 @@ async function createTestServer(config: TestServerConfig = { sessionIdGenerator:
70
71
return { server, transport, mcpServer, baseUrl } ;
71
72
}
72
73
74
+ /**
75
+ * Helper to create and start authenticated test HTTP server with MCP setup
76
+ */
77
+ async function createTestAuthServer ( config : TestServerConfig = { sessionIdGenerator : ( ( ) => randomUUID ( ) ) } ) : Promise < {
78
+ server : Server ;
79
+ transport : StreamableHTTPServerTransport ;
80
+ mcpServer : McpServer ;
81
+ baseUrl : URL ;
82
+ } > {
83
+ const mcpServer = new McpServer (
84
+ { name : "test-server" , version : "1.0.0" } ,
85
+ { capabilities : { logging : { } } }
86
+ ) ;
87
+
88
+ mcpServer . tool (
89
+ "profile" ,
90
+ "A user profile data tool" ,
91
+ { active : z . boolean ( ) . describe ( "Profile status" ) } ,
92
+ async ( { active } , { authInfo } ) : Promise < CallToolResult > => {
93
+ return { content : [ { type : "text" , text : `${ active ? 'Active' : 'Inactive' } profile from token: ${ authInfo ?. token } !` } ] } ;
94
+ }
95
+ ) ;
96
+
97
+ const transport = new StreamableHTTPServerTransport ( {
98
+ sessionIdGenerator : config . sessionIdGenerator ,
99
+ enableJsonResponse : config . enableJsonResponse ?? false ,
100
+ eventStore : config . eventStore
101
+ } ) ;
102
+
103
+ await mcpServer . connect ( transport ) ;
104
+
105
+ const server = createServer ( async ( req : IncomingMessage & { auth ?: AuthInfo } , res ) => {
106
+ try {
107
+ if ( config . customRequestHandler ) {
108
+ await config . customRequestHandler ( req , res ) ;
109
+ } else {
110
+ req . auth = { token : req . headers [ "authorization" ] ?. split ( " " ) [ 1 ] } as AuthInfo ;
111
+ await transport . handleRequest ( req , res ) ;
112
+ }
113
+ } catch ( error ) {
114
+ console . error ( "Error handling request:" , error ) ;
115
+ if ( ! res . headersSent ) res . writeHead ( 500 ) . end ( ) ;
116
+ }
117
+ } ) ;
118
+
119
+ const baseUrl = await new Promise < URL > ( ( resolve ) => {
120
+ server . listen ( 0 , "127.0.0.1" , ( ) => {
121
+ const addr = server . address ( ) as AddressInfo ;
122
+ resolve ( new URL ( `http://127.0.0.1:${ addr . port } ` ) ) ;
123
+ } ) ;
124
+ } ) ;
125
+
126
+ return { server, transport, mcpServer, baseUrl } ;
127
+ }
128
+
73
129
/**
74
130
* Helper to stop test server
75
131
*/
@@ -120,10 +176,11 @@ async function readSSEEvent(response: Response): Promise<string> {
120
176
/**
121
177
* Helper to send JSON-RPC request
122
178
*/
123
- async function sendPostRequest ( baseUrl : URL , message : JSONRPCMessage | JSONRPCMessage [ ] , sessionId ?: string ) : Promise < Response > {
179
+ async function sendPostRequest ( baseUrl : URL , message : JSONRPCMessage | JSONRPCMessage [ ] , sessionId ?: string , extraHeaders ?: Record < string , string > ) : Promise < Response > {
124
180
const headers : Record < string , string > = {
125
181
"Content-Type" : "application/json" ,
126
182
Accept : "application/json, text/event-stream" ,
183
+ ...extraHeaders
127
184
} ;
128
185
129
186
if ( sessionId ) {
@@ -673,6 +730,105 @@ describe("StreamableHTTPServerTransport", () => {
673
730
} ) ;
674
731
} ) ;
675
732
733
+ describe ( "StreamableHTTPServerTransport with AuthInfo" , ( ) => {
734
+ let server : Server ;
735
+ let transport : StreamableHTTPServerTransport ;
736
+ let baseUrl : URL ;
737
+ let sessionId : string ;
738
+
739
+ beforeEach ( async ( ) => {
740
+ const result = await createTestAuthServer ( ) ;
741
+ server = result . server ;
742
+ transport = result . transport ;
743
+ baseUrl = result . baseUrl ;
744
+ } ) ;
745
+
746
+ afterEach ( async ( ) => {
747
+ await stopTestServer ( { server, transport } ) ;
748
+ } ) ;
749
+
750
+ async function initializeServer ( ) : Promise < string > {
751
+ const response = await sendPostRequest ( baseUrl , TEST_MESSAGES . initialize ) ;
752
+
753
+ expect ( response . status ) . toBe ( 200 ) ;
754
+ const newSessionId = response . headers . get ( "mcp-session-id" ) ;
755
+ expect ( newSessionId ) . toBeDefined ( ) ;
756
+ return newSessionId as string ;
757
+ }
758
+
759
+ it ( "should call a tool with authInfo" , async ( ) => {
760
+ sessionId = await initializeServer ( ) ;
761
+
762
+ const toolCallMessage : JSONRPCMessage = {
763
+ jsonrpc : "2.0" ,
764
+ method : "tools/call" ,
765
+ params : {
766
+ name : "profile" ,
767
+ arguments : { active : true } ,
768
+ } ,
769
+ id : "call-1" ,
770
+ } ;
771
+
772
+ const response = await sendPostRequest ( baseUrl , toolCallMessage , sessionId , { 'authorization' : 'Bearer test-token' } ) ;
773
+ expect ( response . status ) . toBe ( 200 ) ;
774
+
775
+ const text = await readSSEEvent ( response ) ;
776
+ const eventLines = text . split ( "\n" ) ;
777
+ const dataLine = eventLines . find ( line => line . startsWith ( "data:" ) ) ;
778
+ expect ( dataLine ) . toBeDefined ( ) ;
779
+
780
+ const eventData = JSON . parse ( dataLine ! . substring ( 5 ) ) ;
781
+ expect ( eventData ) . toMatchObject ( {
782
+ jsonrpc : "2.0" ,
783
+ result : {
784
+ content : [
785
+ {
786
+ type : "text" ,
787
+ text : "Active profile from token: test-token!" ,
788
+ } ,
789
+ ] ,
790
+ } ,
791
+ id : "call-1" ,
792
+ } ) ;
793
+ } ) ;
794
+
795
+ it ( "should calls tool without authInfo when it is optional" , async ( ) => {
796
+ sessionId = await initializeServer ( ) ;
797
+
798
+ const toolCallMessage : JSONRPCMessage = {
799
+ jsonrpc : "2.0" ,
800
+ method : "tools/call" ,
801
+ params : {
802
+ name : "profile" ,
803
+ arguments : { active : false } ,
804
+ } ,
805
+ id : "call-1" ,
806
+ } ;
807
+
808
+ const response = await sendPostRequest ( baseUrl , toolCallMessage , sessionId ) ;
809
+ expect ( response . status ) . toBe ( 200 ) ;
810
+
811
+ const text = await readSSEEvent ( response ) ;
812
+ const eventLines = text . split ( "\n" ) ;
813
+ const dataLine = eventLines . find ( line => line . startsWith ( "data:" ) ) ;
814
+ expect ( dataLine ) . toBeDefined ( ) ;
815
+
816
+ const eventData = JSON . parse ( dataLine ! . substring ( 5 ) ) ;
817
+ expect ( eventData ) . toMatchObject ( {
818
+ jsonrpc : "2.0" ,
819
+ result : {
820
+ content : [
821
+ {
822
+ type : "text" ,
823
+ text : "Inactive profile from token: undefined!" ,
824
+ } ,
825
+ ] ,
826
+ } ,
827
+ id : "call-1" ,
828
+ } ) ;
829
+ } ) ;
830
+ } ) ;
831
+
676
832
// Test JSON Response Mode
677
833
describe ( "StreamableHTTPServerTransport with JSON Response Mode" , ( ) => {
678
834
let server : Server ;
0 commit comments