1- import { Deferred , Effect } from "effect" ;
1+ import { Effect } from "effect" ;
22import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" ;
33import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" ;
44import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js" ;
@@ -14,7 +14,8 @@ import {
1414 formatResumeAcknowledgement ,
1515 readElicitationMode ,
1616} from "@executor-js/host-mcp/browser-approval" ;
17- import type { ResumeResponse } from "@executor-js/execution" ;
17+ import { makeInProcessBrowserApprovalStore } from "@executor-js/host-mcp/browser-approval-store" ;
18+ import { formatPausedExecution , type ResumeResponse } from "@executor-js/execution" ;
1819
1920import { startIntegrationsRefresh } from "./integrations" ;
2021
@@ -24,6 +25,9 @@ import { startIntegrationsRefresh } from "./integrations";
2425
2526export type McpRequestHandler = {
2627 readonly handleRequest : ( request : Request ) => Promise < Response > ;
28+ /** GET `/api/mcp-sessions/:id/executions/:id` — paused detail for the console. */
29+ readonly handlePausedRequest : ( request : Request ) => Promise < Response > ;
30+ /** POST `/api/mcp-sessions/:id/executions/:id/resume` — record the decision. */
2731 readonly handleApprovalRequest : ( request : Request ) => Promise < Response > ;
2832 readonly close : ( ) => Promise < void > ;
2933} ;
@@ -52,6 +56,7 @@ const ignoreClose = (close: (() => Promise<void>) | undefined): Promise<void> =>
5256 )
5357 : Promise . resolve ( ) ;
5458
59+ const pausedRequestPattern = / ^ \/ a p i \/ m c p - s e s s i o n s \/ ( [ ^ / ? # ] + ) \/ e x e c u t i o n s \/ ( [ ^ / ? # ] + ) $ / ;
5560const approvalRequestPattern = / ^ \/ a p i \/ m c p - s e s s i o n s \/ ( [ ^ / ? # ] + ) \/ e x e c u t i o n s \/ ( [ ^ / ? # ] + ) \/ r e s u m e $ / ;
5661
5762const json = ( value : unknown , status = 200 ) : Response =>
@@ -77,16 +82,29 @@ const resumeApprovalResult = (executionId: string, response: ResumeResponse) =>
7782export const createMcpRequestHandler = ( config : ExecutorMcpServerConfig ) : McpRequestHandler => {
7883 const transports = new Map < string , WebStandardStreamableHTTPServerTransport > ( ) ;
7984 const servers = new Map < string , McpServer > ( ) ;
80- const approvalResponses = new Map < string , Map < string , ResumeResponse > > ( ) ;
81- const approvalWaiters = new Map < string , Map < string , Deferred . Deferred < ResumeResponse > > > ( ) ;
85+ const approvals = makeInProcessBrowserApprovalStore ( ) ;
86+ // Local runs one shared engine across every MCP session (main.ts builds it and
87+ // passes it in), so the paused-execution lookup for browser approval reads it
88+ // directly — there is no per-session engine to track.
89+ const engine = "engine" in config ? config . engine : null ;
90+
91+ const pausedDetail = (
92+ executionId : string ,
93+ ) : Promise < ReturnType < typeof formatPausedExecution > | null > =>
94+ engine
95+ ? Effect . runPromise (
96+ engine . getPausedExecution ( executionId ) . pipe (
97+ Effect . map ( ( paused ) => ( paused ? formatPausedExecution ( paused ) : null ) ) ,
98+ Effect . orElseSucceed ( ( ) => null ) ,
99+ ) ,
100+ )
101+ : Promise . resolve ( null ) ;
82102
83103 const dispose = async ( id : string , opts : { transport ?: boolean ; server ?: boolean } = { } ) => {
84104 const t = transports . get ( id ) ;
85105 const s = servers . get ( id ) ;
86106 transports . delete ( id ) ;
87107 servers . delete ( id ) ;
88- approvalResponses . delete ( id ) ;
89- approvalWaiters . delete ( id ) ;
90108 if ( opts . transport ) await ignoreClose ( t ? ( ) => t . close ( ) : undefined ) ;
91109 if ( opts . server ) await ignoreClose ( s ? ( ) => s . close ( ) : undefined ) ;
92110 } ;
@@ -125,48 +143,7 @@ export const createMcpRequestHandler = (config: ExecutorMcpServerConfig): McpReq
125143 created = await Effect . runPromise (
126144 createExecutorMcpServer ( {
127145 ...config ,
128- browserApprovalStore : {
129- takeResponse : ( executionId ) =>
130- Effect . sync ( ( ) => {
131- if ( ! createdSessionId ) return null ;
132- const sessionApprovals = approvalResponses . get ( createdSessionId ) ;
133- const response = sessionApprovals ?. get ( executionId ) ?? null ;
134- sessionApprovals ?. delete ( executionId ) ;
135- return response ;
136- } ) ,
137- waitForResponse : ( executionId ) =>
138- Effect . gen ( function * ( ) {
139- if ( ! createdSessionId ) return null ;
140- const sessionApprovals = approvalResponses . get ( createdSessionId ) ;
141- const response = sessionApprovals ?. get ( executionId ) ?? null ;
142- if ( response ) {
143- sessionApprovals ?. delete ( executionId ) ;
144- return response ;
145- }
146-
147- const sessionWaiters =
148- approvalWaiters . get ( createdSessionId ) ??
149- new Map < string , Deferred . Deferred < ResumeResponse > > ( ) ;
150- const waiter =
151- sessionWaiters . get ( executionId ) ?? ( yield * Deferred . make < ResumeResponse > ( ) ) ;
152- sessionWaiters . set ( executionId , waiter ) ;
153- approvalWaiters . set ( createdSessionId , sessionWaiters ) ;
154-
155- yield * Deferred . await ( waiter ) . pipe (
156- Effect . ensuring (
157- Effect . sync ( ( ) => {
158- if ( sessionWaiters . get ( executionId ) === waiter ) {
159- sessionWaiters . delete ( executionId ) ;
160- }
161- } ) ,
162- ) ,
163- ) ;
164- const approvals = approvalResponses . get ( createdSessionId ) ;
165- const approved = approvals ?. get ( executionId ) ?? null ;
166- approvals ?. delete ( executionId ) ;
167- return approved ;
168- } ) ,
169- } ,
146+ browserApprovalStore : approvals . store ,
170147 elicitationMode :
171148 elicitationMode === "browser"
172149 ? {
@@ -197,26 +174,29 @@ export const createMcpRequestHandler = (config: ExecutorMcpServerConfig): McpReq
197174 }
198175 } ,
199176
177+ handlePausedRequest : async ( request ) => {
178+ const match = pausedRequestPattern . exec ( new URL ( request . url ) . pathname ) ;
179+ if ( ! match ) return json ( { error : "Not found" } , 404 ) ;
180+ if ( request . method !== "GET" ) return json ( { error : "Method not allowed" } , 405 ) ;
181+
182+ const paused = await pausedDetail ( decodeURIComponent ( match [ 2 ] ) ) ;
183+ if ( ! paused ) return json ( { error : "Paused execution not found" } , 404 ) ;
184+ return json ( { text : paused . text , structured : paused . structured } ) ;
185+ } ,
186+
200187 handleApprovalRequest : async ( request ) => {
201- const url = new URL ( request . url ) ;
202- const match = approvalRequestPattern . exec ( url . pathname ) ;
188+ const match = approvalRequestPattern . exec ( new URL ( request . url ) . pathname ) ;
203189 if ( ! match ) return json ( { error : "Not found" } , 404 ) ;
204190 if ( request . method !== "POST" ) return json ( { error : "Method not allowed" } , 405 ) ;
205191
206- const sessionId = decodeURIComponent ( match [ 1 ] ) ;
207192 const executionId = decodeURIComponent ( match [ 2 ] ) ;
208- if ( ! servers . has ( sessionId ) ) return json ( { error : "MCP session not found" } , 404 ) ;
193+ // The shared engine must still hold the paused execution — guards stale ids.
194+ if ( ! ( await pausedDetail ( executionId ) ) ) return json ( { error : "MCP session not found" } , 404 ) ;
209195
210196 const response = await readResumeResponse ( request ) ;
211197 if ( ! response ) return json ( { error : "Invalid approval response" } , 400 ) ;
212198
213- const sessionApprovals =
214- approvalResponses . get ( sessionId ) ?? new Map < string , ResumeResponse > ( ) ;
215- sessionApprovals . set ( executionId , response ) ;
216- approvalResponses . set ( sessionId , sessionApprovals ) ;
217- const waiter = approvalWaiters . get ( sessionId ) ?. get ( executionId ) ;
218- if ( waiter ) await Effect . runPromise ( Deferred . succeed ( waiter , response ) ) ;
219-
199+ await Effect . runPromise ( approvals . recordResponse ( executionId , response ) ) ;
220200 return json ( resumeApprovalResult ( executionId , response ) ) ;
221201 } ,
222202
0 commit comments