1
+ import { StreamableHTTPClientTransport } from "./streamableHttp.js" ;
2
+ import { JSONRPCMessage } from "../types.js" ;
3
+
4
+
5
+ describe ( "StreamableHTTPClientTransport" , ( ) => {
6
+ let transport : StreamableHTTPClientTransport ;
7
+
8
+ beforeEach ( ( ) => {
9
+ transport = new StreamableHTTPClientTransport ( new URL ( "http://localhost:1234/mcp" ) ) ;
10
+ jest . spyOn ( global , "fetch" ) ;
11
+ } ) ;
12
+
13
+ afterEach ( ( ) => {
14
+ jest . clearAllMocks ( ) ;
15
+ } ) ;
16
+
17
+ it ( "should send JSON-RPC messages via POST" , async ( ) => {
18
+ const message : JSONRPCMessage = {
19
+ jsonrpc : "2.0" ,
20
+ method : "test" ,
21
+ params : { } ,
22
+ id : "test-id"
23
+ } ;
24
+
25
+ ( global . fetch as jest . Mock ) . mockResolvedValueOnce ( {
26
+ ok : true ,
27
+ status : 202 ,
28
+ headers : new Headers ( ) ,
29
+ } ) ;
30
+
31
+ await transport . send ( message ) ;
32
+
33
+ expect ( global . fetch ) . toHaveBeenCalledWith (
34
+ expect . anything ( ) ,
35
+ expect . objectContaining ( {
36
+ method : "POST" ,
37
+ headers : expect . any ( Headers ) ,
38
+ body : JSON . stringify ( message )
39
+ } )
40
+ ) ;
41
+ } ) ;
42
+
43
+ it ( "should send batch messages" , async ( ) => {
44
+ const messages : JSONRPCMessage [ ] = [
45
+ { jsonrpc : "2.0" , method : "test1" , params : { } , id : "id1" } ,
46
+ { jsonrpc : "2.0" , method : "test2" , params : { } , id : "id2" }
47
+ ] ;
48
+
49
+ ( global . fetch as jest . Mock ) . mockResolvedValueOnce ( {
50
+ ok : true ,
51
+ status : 200 ,
52
+ headers : new Headers ( { "content-type" : "text/event-stream" } ) ,
53
+ body : null
54
+ } ) ;
55
+
56
+ await transport . send ( messages ) ;
57
+
58
+ expect ( global . fetch ) . toHaveBeenCalledWith (
59
+ expect . anything ( ) ,
60
+ expect . objectContaining ( {
61
+ method : "POST" ,
62
+ headers : expect . any ( Headers ) ,
63
+ body : JSON . stringify ( messages )
64
+ } )
65
+ ) ;
66
+ } ) ;
67
+
68
+ it ( "should store session ID received during initialization" , async ( ) => {
69
+ const message : JSONRPCMessage = {
70
+ jsonrpc : "2.0" ,
71
+ method : "initialize" ,
72
+ params : {
73
+ clientInfo : { name : "test-client" , version : "1.0" } ,
74
+ protocolVersion : "2025-03-26"
75
+ } ,
76
+ id : "init-id"
77
+ } ;
78
+
79
+ ( global . fetch as jest . Mock ) . mockResolvedValueOnce ( {
80
+ ok : true ,
81
+ status : 200 ,
82
+ headers : new Headers ( { "mcp-session-id" : "test-session-id" } ) ,
83
+ } ) ;
84
+
85
+ await transport . send ( message ) ;
86
+
87
+ // Send a second message that should include the session ID
88
+ ( global . fetch as jest . Mock ) . mockResolvedValueOnce ( {
89
+ ok : true ,
90
+ status : 202 ,
91
+ headers : new Headers ( )
92
+ } ) ;
93
+
94
+ await transport . send ( { jsonrpc : "2.0" , method : "test" , params : { } } as JSONRPCMessage ) ;
95
+
96
+ // Check that second request included session ID header
97
+ const calls = ( global . fetch as jest . Mock ) . mock . calls ;
98
+ const lastCall = calls [ calls . length - 1 ] ;
99
+ expect ( lastCall [ 1 ] . headers ) . toBeDefined ( ) ;
100
+ expect ( lastCall [ 1 ] . headers . get ( "mcp-session-id" ) ) . toBe ( "test-session-id" ) ;
101
+ } ) ;
102
+
103
+ it ( "should handle 404 response when session expires" , async ( ) => {
104
+ const message : JSONRPCMessage = {
105
+ jsonrpc : "2.0" ,
106
+ method : "test" ,
107
+ params : { } ,
108
+ id : "test-id"
109
+ } ;
110
+
111
+ ( global . fetch as jest . Mock ) . mockResolvedValueOnce ( {
112
+ ok : false ,
113
+ status : 404 ,
114
+ statusText : "Not Found" ,
115
+ text : ( ) => Promise . resolve ( "Session not found" ) ,
116
+ headers : new Headers ( )
117
+ } ) ;
118
+
119
+ const errorSpy = jest . fn ( ) ;
120
+ transport . onerror = errorSpy ;
121
+
122
+ await expect ( transport . send ( message ) ) . rejects . toThrow ( "Error POSTing to endpoint (HTTP 404)" ) ;
123
+ expect ( errorSpy ) . toHaveBeenCalled ( ) ;
124
+ } ) ;
125
+
126
+ it ( "should handle session termination via DELETE request" , async ( ) => {
127
+ // First set the session ID by mocking initialization
128
+ ( global . fetch as jest . Mock ) . mockResolvedValueOnce ( {
129
+ ok : true ,
130
+ status : 200 ,
131
+ headers : new Headers ( { "mcp-session-id" : "session-to-terminate" } ) ,
132
+ } ) ;
133
+
134
+ await transport . send ( {
135
+ jsonrpc : "2.0" ,
136
+ method : "initialize" ,
137
+ params : {
138
+ clientInfo : { name : "test-client" , version : "1.0" } ,
139
+ protocolVersion : "2025-03-26"
140
+ } ,
141
+ id : "init-id"
142
+ } as JSONRPCMessage ) ;
143
+
144
+ // Mock DELETE request for session termination
145
+ ( global . fetch as jest . Mock ) . mockResolvedValueOnce ( {
146
+ ok : true ,
147
+ status : 200 ,
148
+ headers : new Headers ( )
149
+ } ) ;
150
+
151
+ const closeSpy = jest . fn ( ) ;
152
+ transport . onclose = closeSpy ;
153
+
154
+ await transport . close ( ) ;
155
+
156
+ // Check that DELETE request was sent
157
+ const calls = ( global . fetch as jest . Mock ) . mock . calls ;
158
+ const lastCall = calls [ calls . length - 1 ] ;
159
+ expect ( lastCall [ 1 ] . method ) . toBe ( "DELETE" ) ;
160
+ // The headers may be a plain object in tests
161
+ expect ( lastCall [ 1 ] . headers [ "mcp-session-id" ] ) . toBe ( "session-to-terminate" ) ;
162
+
163
+ expect ( closeSpy ) . toHaveBeenCalled ( ) ;
164
+ } ) ;
165
+
166
+ it ( "should handle non-streaming JSON response" , async ( ) => {
167
+ const message : JSONRPCMessage = {
168
+ jsonrpc : "2.0" ,
169
+ method : "test" ,
170
+ params : { } ,
171
+ id : "test-id"
172
+ } ;
173
+
174
+ const responseMessage : JSONRPCMessage = {
175
+ jsonrpc : "2.0" ,
176
+ result : { success : true } ,
177
+ id : "test-id"
178
+ } ;
179
+
180
+ ( global . fetch as jest . Mock ) . mockResolvedValueOnce ( {
181
+ ok : true ,
182
+ status : 200 ,
183
+ headers : new Headers ( { "content-type" : "application/json" } ) ,
184
+ json : ( ) => Promise . resolve ( responseMessage )
185
+ } ) ;
186
+
187
+ const messageSpy = jest . fn ( ) ;
188
+ transport . onmessage = messageSpy ;
189
+
190
+ await transport . send ( message ) ;
191
+
192
+ expect ( messageSpy ) . toHaveBeenCalledWith ( responseMessage ) ;
193
+ } ) ;
194
+ } ) ;
0 commit comments