1+ import { createServer , type Server } from 'node:http' ;
2+ import { AddressInfo } from 'node:net' ;
3+ import { randomUUID } from 'node:crypto' ;
4+ import { Client } from '../client/index.js' ;
5+ import { StreamableHTTPClientTransport } from '../client/streamableHttp.js' ;
6+ import { McpServer } from '../server/mcp.js' ;
7+ import { StreamableHTTPServerTransport } from '../server/streamableHttp.js' ;
8+ import { CallToolResultSchema , ListToolsResultSchema , ListResourcesResultSchema , ListPromptsResultSchema } from '../types.js' ;
9+ import { z } from 'zod' ;
10+
11+ describe ( 'Streamable HTTP Transport Session Management' , ( ) => {
12+ // Function to set up the server with optional session management
13+ async function setupServer ( withSessionManagement : boolean ) {
14+ const server : Server = createServer ( ) ;
15+ const mcpServer = new McpServer (
16+ { name : 'test-server' , version : '1.0.0' } ,
17+ {
18+ capabilities : {
19+ logging : { } ,
20+ tools : { } ,
21+ resources : { } ,
22+ prompts : { }
23+ }
24+ }
25+ ) ;
26+
27+ // Add a simple resource
28+ mcpServer . resource (
29+ 'test-resource' ,
30+ '/test' ,
31+ { description : 'A test resource' } ,
32+ async ( ) => ( {
33+ contents : [ {
34+ uri : '/test' ,
35+ text : 'This is a test resource content'
36+ } ]
37+ } )
38+ ) ;
39+
40+ mcpServer . prompt (
41+ 'test-prompt' ,
42+ 'A test prompt' ,
43+ async ( ) => ( {
44+ messages : [ {
45+ role : 'user' ,
46+ content : {
47+ type : 'text' ,
48+ text : 'This is a test prompt'
49+ }
50+ } ]
51+ } )
52+ ) ;
53+
54+ mcpServer . tool (
55+ 'greet' ,
56+ 'A simple greeting tool' ,
57+ {
58+ name : z . string ( ) . describe ( 'Name to greet' ) . default ( 'World' ) ,
59+ } ,
60+ async ( { name } ) => {
61+ return {
62+ content : [ { type : 'text' , text : `Hello, ${ name } !` } ]
63+ } ;
64+ }
65+ ) ;
66+
67+ // Create transport with or without session management
68+ const serverTransport = new StreamableHTTPServerTransport ( {
69+ sessionIdGenerator : withSessionManagement
70+ ? ( ) => randomUUID ( ) // With session management, generate UUID
71+ : ( ) => undefined // Without session management, return undefined
72+ } ) ;
73+
74+ await mcpServer . connect ( serverTransport ) ;
75+
76+ server . on ( 'request' , async ( req , res ) => {
77+ await serverTransport . handleRequest ( req , res ) ;
78+ } ) ;
79+
80+ // Start the server on a random port
81+ const baseUrl = await new Promise < URL > ( ( resolve ) => {
82+ server . listen ( 0 , '127.0.0.1' , ( ) => {
83+ const addr = server . address ( ) as AddressInfo ;
84+ resolve ( new URL ( `http://127.0.0.1:${ addr . port } ` ) ) ;
85+ } ) ;
86+ } ) ;
87+
88+ return { server, mcpServer, serverTransport, baseUrl } ;
89+ }
90+
91+ describe ( 'Stateless Mode' , ( ) => {
92+ let server : Server ;
93+ let mcpServer : McpServer ;
94+ let serverTransport : StreamableHTTPServerTransport ;
95+ let baseUrl : URL ;
96+
97+ beforeEach ( async ( ) => {
98+ const setup = await setupServer ( false ) ;
99+ server = setup . server ;
100+ mcpServer = setup . mcpServer ;
101+ serverTransport = setup . serverTransport ;
102+ baseUrl = setup . baseUrl ;
103+ } ) ;
104+
105+ afterEach ( async ( ) => {
106+ // Clean up resources
107+ await mcpServer . close ( ) . catch ( ( ) => { } ) ;
108+ await serverTransport . close ( ) . catch ( ( ) => { } ) ;
109+ server . close ( ) ;
110+ } ) ;
111+
112+ it ( 'should operate without session management' , async ( ) => {
113+ // Create and connect a client
114+ const client = new Client ( {
115+ name : 'test-client' ,
116+ version : '1.0.0'
117+ } ) ;
118+
119+ const transport = new StreamableHTTPClientTransport ( baseUrl ) ;
120+ await client . connect ( transport ) ;
121+
122+ // Verify that no session ID was set
123+ expect ( transport . sessionId ) . toBeUndefined ( ) ;
124+
125+ // List available tools
126+ const toolsResult = await client . request ( {
127+ method : 'tools/list' ,
128+ params : { }
129+ } , ListToolsResultSchema ) ;
130+
131+ // Verify tools are accessible
132+ expect ( toolsResult . tools ) . toContainEqual ( expect . objectContaining ( {
133+ name : 'greet'
134+ } ) ) ;
135+
136+ // List available resources
137+ const resourcesResult = await client . request ( {
138+ method : 'resources/list' ,
139+ params : { }
140+ } , ListResourcesResultSchema ) ;
141+
142+ // Verify resources result structure
143+ expect ( resourcesResult ) . toHaveProperty ( 'resources' ) ;
144+
145+ // List available prompts
146+ const promptsResult = await client . request ( {
147+ method : 'prompts/list' ,
148+ params : { }
149+ } , ListPromptsResultSchema ) ;
150+
151+ // Verify prompts result structure
152+ expect ( promptsResult ) . toHaveProperty ( 'prompts' ) ;
153+ expect ( promptsResult . prompts ) . toContainEqual ( expect . objectContaining ( {
154+ name : 'test-prompt'
155+ } ) ) ;
156+
157+ // Call the greeting tool
158+ const greetingResult = await client . request ( {
159+ method : 'tools/call' ,
160+ params : {
161+ name : 'greet' ,
162+ arguments : {
163+ name : 'Stateless Transport'
164+ }
165+ }
166+ } , CallToolResultSchema ) ;
167+
168+ // Verify tool result
169+ expect ( greetingResult . content ) . toEqual ( [
170+ { type : 'text' , text : 'Hello, Stateless Transport!' }
171+ ] ) ;
172+
173+ // Clean up
174+ await transport . close ( ) ;
175+ } ) ;
176+ } ) ;
177+
178+ describe ( 'Stateful Mode' , ( ) => {
179+ let server : Server ;
180+ let mcpServer : McpServer ;
181+ let serverTransport : StreamableHTTPServerTransport ;
182+ let baseUrl : URL ;
183+
184+ beforeEach ( async ( ) => {
185+ const setup = await setupServer ( true ) ;
186+ server = setup . server ;
187+ mcpServer = setup . mcpServer ;
188+ serverTransport = setup . serverTransport ;
189+ baseUrl = setup . baseUrl ;
190+ } ) ;
191+
192+ afterEach ( async ( ) => {
193+ // Clean up resources
194+ await mcpServer . close ( ) . catch ( ( ) => { } ) ;
195+ await serverTransport . close ( ) . catch ( ( ) => { } ) ;
196+ server . close ( ) ;
197+ } ) ;
198+
199+ it ( 'should operate with session management' , async ( ) => {
200+ // Create and connect a client
201+ const client = new Client ( {
202+ name : 'test-client' ,
203+ version : '1.0.0'
204+ } ) ;
205+
206+ const transport = new StreamableHTTPClientTransport ( baseUrl ) ;
207+ await client . connect ( transport ) ;
208+
209+ // Verify that a session ID was set
210+ expect ( transport . sessionId ) . toBeDefined ( ) ;
211+ expect ( typeof transport . sessionId ) . toBe ( 'string' ) ;
212+
213+ // List available tools
214+ const toolsResult = await client . request ( {
215+ method : 'tools/list' ,
216+ params : { }
217+ } , ListToolsResultSchema ) ;
218+
219+ // Verify tools are accessible
220+ expect ( toolsResult . tools ) . toContainEqual ( expect . objectContaining ( {
221+ name : 'greet'
222+ } ) ) ;
223+
224+ // List available resources
225+ const resourcesResult = await client . request ( {
226+ method : 'resources/list' ,
227+ params : { }
228+ } , ListResourcesResultSchema ) ;
229+
230+ // Verify resources result structure
231+ expect ( resourcesResult ) . toHaveProperty ( 'resources' ) ;
232+
233+ // List available prompts
234+ const promptsResult = await client . request ( {
235+ method : 'prompts/list' ,
236+ params : { }
237+ } , ListPromptsResultSchema ) ;
238+
239+ // Verify prompts result structure
240+ expect ( promptsResult ) . toHaveProperty ( 'prompts' ) ;
241+ expect ( promptsResult . prompts ) . toContainEqual ( expect . objectContaining ( {
242+ name : 'test-prompt'
243+ } ) ) ;
244+
245+ // Call the greeting tool
246+ const greetingResult = await client . request ( {
247+ method : 'tools/call' ,
248+ params : {
249+ name : 'greet' ,
250+ arguments : {
251+ name : 'Stateful Transport'
252+ }
253+ }
254+ } , CallToolResultSchema ) ;
255+
256+ // Verify tool result
257+ expect ( greetingResult . content ) . toEqual ( [
258+ { type : 'text' , text : 'Hello, Stateful Transport!' }
259+ ] ) ;
260+
261+ // Clean up
262+ await transport . close ( ) ;
263+ } ) ;
264+ } ) ;
265+ } ) ;
0 commit comments