@@ -15,13 +15,79 @@ import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/cli
1515import { afterAll , beforeAll , describe , expect , it } from 'vitest' ;
1616
1717const PORT = 8787 ;
18+ const READINESS_TIMEOUT_MS = 60_000 ;
19+ const READINESS_POLL_INTERVAL_MS = 100 ;
1820
1921interface TestEnv {
2022 tempDir : string ;
2123 process : ChildProcess ;
2224 cleanup : ( ) => Promise < void > ;
2325}
2426
27+ /**
28+ * Wait until the worker can serve a real MCP `initialize` request.
29+ *
30+ * Wrangler's "Ready on …" stdout line is unreliable: miniflare can print it before the user
31+ * worker is actually wired, and subsequent POSTs come back as `500 Network connection lost` or
32+ * `ECONNREFUSED`. The only signal we can trust is "the server returned an MCP-shaped response
33+ * to a protocol request".
34+ *
35+ * Polls the configured port with an MCP `initialize` POST every {@link READINESS_POLL_INTERVAL_MS}ms
36+ * until either a JSON-RPC result body comes back, the wrangler process exits, or
37+ * {@link READINESS_TIMEOUT_MS} elapses.
38+ */
39+ async function waitForMcpReady ( proc : ChildProcess ) : Promise < void > {
40+ let stderrTail = '' ;
41+ proc . stderr ?. on ( 'data' , d => {
42+ stderrTail = ( stderrTail + d . toString ( ) ) . slice ( - 2048 ) ;
43+ } ) ;
44+
45+ let processExitedWithCode : number | null = null ;
46+ proc . on ( 'exit' , code => {
47+ processExitedWithCode = code ?? - 1 ;
48+ } ) ;
49+
50+ const deadline = Date . now ( ) + READINESS_TIMEOUT_MS ;
51+ let lastFailure = 'no attempts made' ;
52+
53+ while ( Date . now ( ) < deadline ) {
54+ if ( processExitedWithCode !== null ) {
55+ throw new Error ( `wrangler dev exited with code ${ processExitedWithCode } before becoming ready.\nstderr tail:\n${ stderrTail } ` ) ;
56+ }
57+
58+ try {
59+ const response = await fetch ( `http://127.0.0.1:${ PORT } /` , {
60+ method : 'POST' ,
61+ headers : {
62+ 'Content-Type' : 'application/json' ,
63+ Accept : 'application/json, text/event-stream'
64+ } ,
65+ body : JSON . stringify ( {
66+ jsonrpc : '2.0' ,
67+ id : 'readiness-probe' ,
68+ method : 'initialize' ,
69+ params : {
70+ protocolVersion : '2025-06-18' ,
71+ capabilities : { } ,
72+ clientInfo : { name : 'readiness-probe' , version : '0' }
73+ }
74+ } )
75+ } ) ;
76+ const body = await response . text ( ) ;
77+ if ( response . ok && body . includes ( '"jsonrpc"' ) && body . includes ( '"result"' ) ) {
78+ return ;
79+ }
80+ lastFailure = `status=${ response . status } body=${ body . slice ( 0 , 200 ) } ` ;
81+ } catch ( error ) {
82+ lastFailure = ( error as { cause ?: { code ?: string } ; message : string } ) . cause ?. code ?? ( error as Error ) . message ;
83+ }
84+
85+ await new Promise ( resolve => setTimeout ( resolve , READINESS_POLL_INTERVAL_MS ) ) ;
86+ }
87+
88+ throw new Error ( `Worker did not become ready within ${ READINESS_TIMEOUT_MS } ms.\nLast probe: ${ lastFailure } \nstderr tail:\n${ stderrTail } ` ) ;
89+ }
90+
2591describe ( 'Cloudflare Workers compatibility (no nodejs_compat)' , ( ) => {
2692 let env : TestEnv | null = null ;
2793
@@ -42,8 +108,7 @@ describe('Cloudflare Workers compatibility (no nodejs_compat)', () => {
42108 private : true ,
43109 type : 'module' ,
44110 dependencies : {
45- '@modelcontextprotocol/server' : `file:./${ tarballName } ` ,
46- '@cfworker/json-schema' : '^4.1.1'
111+ '@modelcontextprotocol/server' : `file:./${ tarballName } `
47112 } ,
48113 devDependencies : {
49114 wrangler : '^4.14.4'
@@ -84,48 +149,20 @@ export default {
84149 // Install dependencies
85150 execSync ( 'npm install' , { cwd : tempDir , stdio : 'pipe' , timeout : 60_000 } ) ;
86151
87- // Start wrangler dev server
152+ // Start wrangler dev server. Readiness is determined by probing the MCP endpoint, not by
153+ // parsing wrangler's stdout — see waitForMcpReady for the reasoning.
88154 const proc = spawn ( 'npx' , [ 'wrangler' , 'dev' , '--local' , '--port' , String ( PORT ) ] , {
89155 cwd : tempDir ,
90156 shell : true ,
91157 stdio : 'pipe'
92158 } ) ;
93159
94- // Wait for server to be ready
95- await new Promise < void > ( ( resolve , reject ) => {
96- const timeout = setTimeout ( ( ) => reject ( new Error ( 'Wrangler startup timeout' ) ) , 60_000 ) ;
97- let stderrData = '' ;
98-
99- proc . stdout ?. on ( 'data' , data => {
100- const output = data . toString ( ) ;
101- if ( / R e a d y o n | L i s t e n i n g o n / . test ( output ) ) {
102- clearTimeout ( timeout ) ;
103- // Extra delay for wrangler to fully initialize
104- setTimeout ( resolve , 1000 ) ;
105- }
106- } ) ;
107-
108- proc . stderr ?. on ( 'data' , data => {
109- stderrData += data . toString ( ) ;
110- // Check for fatal errors like missing node: modules
111- if ( / N o s u c h m o d u l e " n o d e : / . test ( stderrData ) ) {
112- clearTimeout ( timeout ) ;
113- reject ( new Error ( `Wrangler fatal error: ${ stderrData } ` ) ) ;
114- }
115- } ) ;
116-
117- proc . on ( 'error' , err => {
118- clearTimeout ( timeout ) ;
119- reject ( err ) ;
120- } ) ;
121-
122- proc . on ( 'close' , code => {
123- if ( code !== 0 && code !== null ) {
124- clearTimeout ( timeout ) ;
125- reject ( new Error ( `Wrangler exited with code ${ code } . stderr: ${ stderrData } ` ) ) ;
126- }
127- } ) ;
128- } ) ;
160+ try {
161+ await waitForMcpReady ( proc ) ;
162+ } catch ( error ) {
163+ proc . kill ( 'SIGTERM' ) ;
164+ throw error ;
165+ }
129166
130167 const cleanup = async ( ) => {
131168 proc . kill ( 'SIGTERM' ) ;
@@ -150,24 +187,9 @@ export default {
150187 it ( 'should handle MCP requests' , async ( ) => {
151188 expect ( env ) . not . toBeNull ( ) ;
152189
153- // Retry connection — wrangler may report "Ready" before it can handle requests
154- let client ! : Client ;
155- let lastError : unknown ;
156- for ( let attempt = 0 ; attempt < 5 ; attempt ++ ) {
157- try {
158- client = new Client ( { name : 'test-client' , version : '1.0.0' } ) ;
159- const transport = new StreamableHTTPClientTransport ( new URL ( `http://127.0.0.1:${ PORT } /` ) ) ;
160- await client . connect ( transport ) ;
161- lastError = undefined ;
162- break ;
163- } catch ( error ) {
164- lastError = error ;
165- await new Promise ( resolve => setTimeout ( resolve , 1000 ) ) ;
166- }
167- }
168- if ( lastError ) {
169- throw lastError ;
170- }
190+ const client = new Client ( { name : 'test-client' , version : '1.0.0' } ) ;
191+ const transport = new StreamableHTTPClientTransport ( new URL ( `http://127.0.0.1:${ PORT } /` ) ) ;
192+ await client . connect ( transport ) ;
171193
172194 const result = await client . callTool ( { name : 'greet' , arguments : { name : 'World' } } ) ;
173195 expect ( result . content ) . toEqual ( [ { type : 'text' , text : 'Hello, World!' } ] ) ;
0 commit comments