1+ import { jest } from "@jest/globals" ;
2+ import mcpProxy from "../mcpProxy.js" ;
3+
4+ // Mock transport interface
5+ interface MockTransport {
6+ sessionId ?: string ;
7+ onmessage : ( ( message : any ) => void ) | null ;
8+ onclose : ( ( ) => void ) | null ;
9+ onerror : ( ( error : Error ) => void ) | null ;
10+ send : jest . Mock ;
11+ close : jest . Mock ;
12+ }
13+
14+ // Create mock transport
15+ function createMockTransport ( sessionId ?: string ) : MockTransport {
16+ return {
17+ sessionId,
18+ onmessage : null ,
19+ onclose : null ,
20+ onerror : null ,
21+ send : jest . fn ( ) . mockResolvedValue ( undefined ) ,
22+ close : jest . fn ( ) . mockResolvedValue ( undefined ) ,
23+ } ;
24+ }
25+
26+ describe ( "mcpProxy" , ( ) => {
27+ let mockClientTransport : MockTransport ;
28+ let mockServerTransport : MockTransport ;
29+ let mockCleanup : jest . Mock ;
30+
31+ beforeEach ( ( ) => {
32+ mockClientTransport = createMockTransport ( "client-session-123" ) ;
33+ mockServerTransport = createMockTransport ( "server-session-456" ) ;
34+ mockCleanup = jest . fn ( ) ;
35+ } ) ;
36+
37+ afterEach ( ( ) => {
38+ jest . clearAllMocks ( ) ;
39+ } ) ;
40+
41+ describe ( "message forwarding" , ( ) => {
42+ it ( "should forward messages from client to server" , async ( ) => {
43+ mcpProxy ( {
44+ transportToClient : mockClientTransport as any ,
45+ transportToServer : mockServerTransport as any ,
46+ onCleanup : mockCleanup ,
47+ } ) ;
48+
49+ const testMessage = {
50+ jsonrpc : "2.0" as const ,
51+ method : "test/method" ,
52+ params : { test : "data" } ,
53+ id : 1 ,
54+ } ;
55+
56+ // Simulate client message
57+ mockClientTransport . onmessage ! ( testMessage ) ;
58+
59+ expect ( mockServerTransport . send ) . toHaveBeenCalledWith ( testMessage ) ;
60+ } ) ;
61+
62+ it ( "should forward messages from server to client" , async ( ) => {
63+ mcpProxy ( {
64+ transportToClient : mockClientTransport as any ,
65+ transportToServer : mockServerTransport as any ,
66+ onCleanup : mockCleanup ,
67+ } ) ;
68+
69+ const testMessage = {
70+ jsonrpc : "2.0" as const ,
71+ result : { test : "response" } ,
72+ id : 1 ,
73+ } ;
74+
75+ // Simulate server message
76+ mockServerTransport . onmessage ! ( testMessage ) ;
77+
78+ expect ( mockClientTransport . send ) . toHaveBeenCalledWith ( testMessage ) ;
79+ } ) ;
80+ } ) ;
81+
82+ describe ( "error handling" , ( ) => {
83+ it ( "should send error response when server send fails for request" , async ( ) => {
84+ const serverError = new Error ( "Server send failed" ) ;
85+ mockServerTransport . send . mockRejectedValue ( serverError ) ;
86+
87+ mcpProxy ( {
88+ transportToClient : mockClientTransport as any ,
89+ transportToServer : mockServerTransport as any ,
90+ onCleanup : mockCleanup ,
91+ } ) ;
92+
93+ const testRequest = {
94+ jsonrpc : "2.0" as const ,
95+ method : "test/method" ,
96+ params : { test : "data" } ,
97+ id : 1 ,
98+ } ;
99+
100+ // Simulate client request that fails on server
101+ mockClientTransport . onmessage ! ( testRequest ) ;
102+
103+ // Wait for the async error handling
104+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
105+
106+ expect ( mockClientTransport . send ) . toHaveBeenCalledWith ( {
107+ jsonrpc : "2.0" ,
108+ id : 1 ,
109+ error : {
110+ code : - 32001 ,
111+ message : "Server send failed" ,
112+ data : serverError ,
113+ } ,
114+ } ) ;
115+ } ) ;
116+
117+ it ( "should not send error response when client transport is closed" , async ( ) => {
118+ const serverError = new Error ( "Server send failed" ) ;
119+ mockServerTransport . send . mockRejectedValue ( serverError ) ;
120+
121+ mcpProxy ( {
122+ transportToClient : mockClientTransport as any ,
123+ transportToServer : mockServerTransport as any ,
124+ onCleanup : mockCleanup ,
125+ } ) ;
126+
127+ // Close client transport first
128+ mockClientTransport . onclose ! ( ) ;
129+
130+ const testRequest = {
131+ jsonrpc : "2.0" as const ,
132+ method : "test/method" ,
133+ params : { test : "data" } ,
134+ id : 1 ,
135+ } ;
136+
137+ // Now try to send message
138+ mockClientTransport . onmessage ! ( testRequest ) ;
139+
140+ // Wait for the async error handling
141+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
142+
143+ // Should not send error response since client transport is closed
144+ expect ( mockClientTransport . send ) . toHaveBeenCalledTimes ( 0 ) ;
145+ } ) ;
146+
147+ it ( "should not send error response for notifications (no id)" , async ( ) => {
148+ const serverError = new Error ( "Server send failed" ) ;
149+ mockServerTransport . send . mockRejectedValue ( serverError ) ;
150+
151+ mcpProxy ( {
152+ transportToClient : mockClientTransport as any ,
153+ transportToServer : mockServerTransport as any ,
154+ onCleanup : mockCleanup ,
155+ } ) ;
156+
157+ const testNotification = {
158+ jsonrpc : "2.0" as const ,
159+ method : "test/notification" ,
160+ params : { test : "data" } ,
161+ } ;
162+
163+ // Simulate client notification that fails on server
164+ mockClientTransport . onmessage ! ( testNotification ) ;
165+
166+ // Wait for the async error handling
167+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
168+
169+ // Should not send error response for notifications
170+ expect ( mockClientTransport . send ) . toHaveBeenCalledTimes ( 0 ) ;
171+ } ) ;
172+ } ) ;
173+
174+ describe ( "connection cleanup" , ( ) => {
175+ it ( "should call cleanup when client transport closes" , ( ) => {
176+ mcpProxy ( {
177+ transportToClient : mockClientTransport as any ,
178+ transportToServer : mockServerTransport as any ,
179+ onCleanup : mockCleanup ,
180+ } ) ;
181+
182+ // Simulate client transport closing
183+ mockClientTransport . onclose ! ( ) ;
184+
185+ expect ( mockCleanup ) . toHaveBeenCalledTimes ( 1 ) ;
186+ expect ( mockServerTransport . close ) . toHaveBeenCalledTimes ( 1 ) ;
187+ } ) ;
188+
189+ it ( "should call cleanup when server transport closes" , ( ) => {
190+ mcpProxy ( {
191+ transportToClient : mockClientTransport as any ,
192+ transportToServer : mockServerTransport as any ,
193+ onCleanup : mockCleanup ,
194+ } ) ;
195+
196+ // Simulate server transport closing
197+ mockServerTransport . onclose ! ( ) ;
198+
199+ expect ( mockCleanup ) . toHaveBeenCalledTimes ( 1 ) ;
200+ expect ( mockClientTransport . close ) . toHaveBeenCalledTimes ( 1 ) ;
201+ } ) ;
202+
203+ it ( "should not call cleanup twice if both transports close" , ( ) => {
204+ mcpProxy ( {
205+ transportToClient : mockClientTransport as any ,
206+ transportToServer : mockServerTransport as any ,
207+ onCleanup : mockCleanup ,
208+ } ) ;
209+
210+ // Simulate both transports closing
211+ mockClientTransport . onclose ! ( ) ;
212+ mockServerTransport . onclose ! ( ) ;
213+
214+ expect ( mockCleanup ) . toHaveBeenCalledTimes ( 1 ) ;
215+ } ) ;
216+
217+ it ( "should work without cleanup callback" , ( ) => {
218+ expect ( ( ) => {
219+ mcpProxy ( {
220+ transportToClient : mockClientTransport as any ,
221+ transportToServer : mockServerTransport as any ,
222+ } ) ;
223+
224+ // Should not throw when cleanup is not provided
225+ mockClientTransport . onclose ! ( ) ;
226+ } ) . not . toThrow ( ) ;
227+ } ) ;
228+
229+ it ( "should handle cleanup callback errors gracefully" , ( ) => {
230+ const errorCleanup = jest . fn ( ) . mockImplementation ( ( ) => {
231+ throw new Error ( "Cleanup failed" ) ;
232+ } ) ;
233+
234+ expect ( ( ) => {
235+ mcpProxy ( {
236+ transportToClient : mockClientTransport as any ,
237+ transportToServer : mockServerTransport as any ,
238+ onCleanup : errorCleanup ,
239+ } ) ;
240+
241+ // Should not throw even if cleanup fails
242+ mockClientTransport . onclose ! ( ) ;
243+ } ) . not . toThrow ( ) ;
244+
245+ expect ( errorCleanup ) . toHaveBeenCalledTimes ( 1 ) ;
246+ } ) ;
247+ } ) ;
248+
249+ describe ( "transport close synchronization" , ( ) => {
250+ it ( "should not close server transport if already closed by server" , ( ) => {
251+ mcpProxy ( {
252+ transportToClient : mockClientTransport as any ,
253+ transportToServer : mockServerTransport as any ,
254+ onCleanup : mockCleanup ,
255+ } ) ;
256+
257+ // First, server transport closes
258+ mockServerTransport . onclose ! ( ) ;
259+
260+ // Reset mock to check if close is called again
261+ mockServerTransport . close . mockClear ( ) ;
262+
263+ // Then client transport tries to close
264+ mockClientTransport . onclose ! ( ) ;
265+
266+ // Server transport should not be closed again
267+ expect ( mockServerTransport . close ) . toHaveBeenCalledTimes ( 0 ) ;
268+ expect ( mockCleanup ) . toHaveBeenCalledTimes ( 1 ) ;
269+ } ) ;
270+
271+ it ( "should not close client transport if already closed by client" , ( ) => {
272+ mcpProxy ( {
273+ transportToClient : mockClientTransport as any ,
274+ transportToServer : mockServerTransport as any ,
275+ onCleanup : mockCleanup ,
276+ } ) ;
277+
278+ // First, client transport closes
279+ mockClientTransport . onclose ! ( ) ;
280+
281+ // Reset mock to check if close is called again
282+ mockClientTransport . close . mockClear ( ) ;
283+
284+ // Then server transport tries to close
285+ mockServerTransport . onclose ! ( ) ;
286+
287+ // Client transport should not be closed again
288+ expect ( mockClientTransport . close ) . toHaveBeenCalledTimes ( 0 ) ;
289+ expect ( mockCleanup ) . toHaveBeenCalledTimes ( 1 ) ;
290+ } ) ;
291+ } ) ;
292+
293+ describe ( "error handlers" , ( ) => {
294+ it ( "should set error handlers on both transports" , ( ) => {
295+ mcpProxy ( {
296+ transportToClient : mockClientTransport as any ,
297+ transportToServer : mockServerTransport as any ,
298+ onCleanup : mockCleanup ,
299+ } ) ;
300+
301+ expect ( mockClientTransport . onerror ) . toBeTruthy ( ) ;
302+ expect ( mockServerTransport . onerror ) . toBeTruthy ( ) ;
303+ } ) ;
304+
305+ it ( "should handle client errors without throwing" , ( ) => {
306+ mcpProxy ( {
307+ transportToClient : mockClientTransport as any ,
308+ transportToServer : mockServerTransport as any ,
309+ onCleanup : mockCleanup ,
310+ } ) ;
311+
312+ const testError = new Error ( "Client error" ) ;
313+ expect ( ( ) => {
314+ mockClientTransport . onerror ! ( testError ) ;
315+ } ) . not . toThrow ( ) ;
316+ } ) ;
317+
318+ it ( "should handle server errors without throwing" , ( ) => {
319+ mcpProxy ( {
320+ transportToClient : mockClientTransport as any ,
321+ transportToServer : mockServerTransport as any ,
322+ onCleanup : mockCleanup ,
323+ } ) ;
324+
325+ const testError = new Error ( "Server error" ) ;
326+ expect ( ( ) => {
327+ mockServerTransport . onerror ! ( testError ) ;
328+ } ) . not . toThrow ( ) ;
329+ } ) ;
330+ } ) ;
331+ } ) ;
0 commit comments