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