@@ -127,17 +127,37 @@ export async function createAdapterServer(
127127 // protocol handshake (initialize) can happen independently per client.
128128 // All sessions share the same adapter, lock, rateLimiter, and browser.
129129
130+ // ── Zod schema shape extractor ────────────────────────────────────────────
131+ // Uses string-based typeName check instead of instanceof to avoid false
132+ // negatives when adapter and core have separate Zod installations (common
133+ // with file: deps where each package resolves its own node_modules).
134+ // Also unwraps ZodEffects (from .refine() / .transform()) to reach the
135+ // underlying ZodObject shape.
136+ function extractZodShape ( schema : z . ZodTypeAny ) : Record < string , z . ZodTypeAny > {
137+ const def = ( schema as { _def ?: { typeName ?: string ; schema ?: z . ZodTypeAny } } ) . _def ;
138+ if ( ! def ) return { } ;
139+ if ( def . typeName === "ZodObject" ) return ( schema as z . ZodObject < z . ZodRawShape > ) . shape ;
140+ // ZodEffects wraps .refine() and .transform() — unwrap to get the base object
141+ if ( def . typeName === "ZodEffects" && def . schema ) return extractZodShape ( def . schema ) ;
142+ return { } ;
143+ }
144+
130145 function createMcpSession ( transport : StreamableHTTPServerTransport ) : McpServer {
131146 const mcp = new McpServer ( { name : `browserkit-${ site } ` , version : "0.1.0" } ) ;
132147
133148 // ── Adapter tools ───────────────────────────────────────────────────────
134149 for ( const tool of adapter . tools ( ) ) {
135150 const toolName = tool . name ;
136151 const inputShape = tool . inputSchema ;
152+ const annotations = tool . annotations ?? {
153+ readOnlyHint : true ,
154+ openWorldHint : true ,
155+ } ;
137156 mcp . tool (
138157 toolName ,
139158 tool . description ,
140- inputShape instanceof z . ZodObject ? inputShape . shape : { } ,
159+ extractZodShape ( inputShape ) ,
160+ annotations ,
141161 async ( input : unknown ) => wrapToolCall ( toolName , input )
142162 ) ;
143163 }
@@ -156,6 +176,12 @@ export async function createAdapterServer(
156176 " screenshot — capture current page as an inline image" ,
157177 " page_state — current URL, title, mode, CDP endpoint" ,
158178 " set_mode — switch headless/watch/paused; requires mode param" ,
179+ "" ,
180+ "Actions:" ,
181+ " health_check — login status, current mode, selector validity report" ,
182+ " screenshot — capture current page as an inline image" ,
183+ " page_state — current URL, title, mode, CDP endpoint" ,
184+ " set_mode — switch headless/watch/paused; requires mode param" ,
159185 " navigate — navigate to a URL; requires url param (use in watch/paused mode)" ,
160186 "" ,
161187 "Params:" ,
@@ -407,6 +433,29 @@ export async function createAdapterServer(
407433 }
408434 ) ;
409435
436+ // ── close_session ────────────────────────────────────────────────────────
437+ // Closes the browser session for this adapter — useful when the session has
438+ // gone stale and you want a fresh start without restarting the whole daemon.
439+ // The next tool call will automatically relaunch the browser.
440+ mcp . tool (
441+ "close_session" ,
442+ `Close the ${ site } browser session and release all browser resources. ` +
443+ "The next tool call will automatically reopen the browser. " +
444+ "Use this when the session has gone stale, you want to force a fresh login, " +
445+ "or you want to free memory without stopping the daemon." ,
446+ { } ,
447+ { title : "Close Browser Session" , destructiveHint : true } ,
448+ async ( ) => {
449+ await sessionManager . closeSite ( site ) ;
450+ return {
451+ content : [ {
452+ type : "text" as const ,
453+ text : `Browser session for "${ site } " closed. The next tool call will relaunch it.` ,
454+ } ] ,
455+ } ;
456+ }
457+ ) ;
458+
410459 mcp . connect ( transport ) . catch ( ( err ) => {
411460 log . error ( { site, err } , "failed to connect McpServer to transport" ) ;
412461 } ) ;
0 commit comments