@@ -35,6 +35,36 @@ const CLARIFICATION_HINT =
3535 "IMPORTANT: The user has typed a follow-up clarification while you were working." +
3636 " Call the getUserClarification tool to read it before proceeding." ;
3737
38+ // Per-tool safety-net budgets for the browser round-trip. The node connector
39+ // is reliable in practice, so these should never fire during normal use —
40+ // they exist so a stalled promise chain (live preview wedged, etc.) surfaces
41+ // a deterministic error to Claude instead of the handler hanging forever.
42+ // Tools whose runtime is bounded by user-supplied code (execJsInLivePreview)
43+ // intentionally have no timeout — the code is allowed to run as long as it takes.
44+ const EXEC_PEER_TIMEOUT_MS = {
45+ getEditorState : 5000 ,
46+ takeScreenshot : 15000 ,
47+ controlEditor : 5000 ,
48+ resizeLivePreview : 5000
49+ } ;
50+
51+ function _execPeerWithTimeout ( nodeConnector , fn , args , label ) {
52+ const ms = EXEC_PEER_TIMEOUT_MS [ fn ] ;
53+ const call = nodeConnector . execPeer ( fn , args ) ;
54+ if ( ! ms ) {
55+ return call ; // no timeout configured for this tool
56+ }
57+ let timer ;
58+ const timeout = new Promise ( function ( _resolve , reject ) {
59+ timer = setTimeout ( function ( ) {
60+ reject ( new Error ( label + " timed out after " + ms + "ms" ) ) ;
61+ } , ms ) ;
62+ } ) ;
63+ return Promise . race ( [ call , timeout ] ) . finally ( function ( ) {
64+ clearTimeout ( timer ) ;
65+ } ) ;
66+ }
67+
3868/**
3969 * Append a clarification hint to an MCP tool result if the user has queued a message.
4070 */
@@ -70,7 +100,7 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors)
70100 async function ( ) {
71101 let result ;
72102 try {
73- const state = await nodeConnector . execPeer ( "getEditorState" , { } ) ;
103+ const state = await _execPeerWithTimeout ( nodeConnector , "getEditorState" , { } , "getEditorState" ) ;
74104 result = {
75105 content : [ { type : "text" , text : JSON . stringify ( state ) } ]
76106 } ;
@@ -107,11 +137,11 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors)
107137 async function ( args ) {
108138 let toolResult ;
109139 try {
110- const result = await nodeConnector . execPeer ( "takeScreenshot" , {
140+ const result = await _execPeerWithTimeout ( nodeConnector , "takeScreenshot" , {
111141 selector : args . selector || undefined ,
112142 purePreview : args . purePreview || false ,
113143 filePath : args . filePath || undefined
114- } ) ;
144+ } , "takeScreenshot" ) ;
115145 if ( result . filePath ) {
116146 toolResult = {
117147 content : [ { type : "text" , text : "Screenshot saved to: " + result . filePath } ]
@@ -148,9 +178,9 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors)
148178 async function ( args ) {
149179 let toolResult ;
150180 try {
151- const result = await nodeConnector . execPeer ( "execJsInLivePreview" , {
181+ const result = await _execPeerWithTimeout ( nodeConnector , "execJsInLivePreview" , {
152182 code : args . code
153- } ) ;
183+ } , "execJsInLivePreview" ) ;
154184 if ( result . error ) {
155185 toolResult = {
156186 content : [ { type : "text" , text : "Error: " + result . error } ] ,
@@ -202,7 +232,7 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors)
202232 for ( const op of args . operations ) {
203233 console . log ( "[Phoenix AI] controlEditor:" , op . operation , op . filePath ) ;
204234 try {
205- const result = await nodeConnector . execPeer ( "controlEditor" , op ) ;
235+ const result = await _execPeerWithTimeout ( nodeConnector , "controlEditor" , op , "controlEditor:" + op . operation ) ;
206236 results . push ( result ) ;
207237 if ( ! result . success ) {
208238 hasError = true ;
@@ -234,9 +264,9 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors)
234264 async function ( args ) {
235265 let toolResult ;
236266 try {
237- const result = await nodeConnector . execPeer ( "resizeLivePreview" , {
267+ const result = await _execPeerWithTimeout ( nodeConnector , "resizeLivePreview" , {
238268 width : args . width
239- } ) ;
269+ } , "resizeLivePreview" ) ;
240270 if ( result . error ) {
241271 toolResult = {
242272 content : [ { type : "text" , text : "Error: " + result . error } ] ,
0 commit comments