1
1
import { Stagehand } from "@browserbasehq/stagehand" ;
2
2
import { Server } from "@modelcontextprotocol/sdk/server/index.js" ;
3
3
import type { Config } from "../config.js" ;
4
- import { CallToolResult } from "@modelcontextprotocol/sdk/types.js" ;
4
+ import { CallToolResult , TextContent , ImageContent } from "@modelcontextprotocol/sdk/types.js" ;
5
5
import { handleToolCall } from "./tools/tools.js" ;
6
6
import { listResources , readResource } from "./resources.js" ;
7
7
import {
@@ -12,11 +12,23 @@ import {
12
12
setServerInstance ,
13
13
log
14
14
} from "./logging.js" ;
15
+ import {
16
+ getSession ,
17
+ getSessionReadOnly ,
18
+ defaultSessionId ,
19
+ type BrowserSession
20
+ } from "./sessionManager.js" ;
21
+
22
+ export type ToolActionResult =
23
+ | { content ?: ( ImageContent | TextContent ) [ ] }
24
+ | undefined
25
+ | void ;
15
26
16
27
export class Context {
17
- private stagehand : Stagehand ;
28
+ private stagehands = new Map < string , Stagehand > ( ) ;
29
+ public readonly config : Config ;
18
30
private server : Server ;
19
- private config : Config ;
31
+ public currentSessionId : string = defaultSessionId ;
20
32
21
33
constructor ( server : Server , config : Config ) {
22
34
this . server = server ;
@@ -28,26 +40,171 @@ export class Context {
28
40
setupLogRotation ( ) ;
29
41
registerExitHandlers ( ) ;
30
42
scheduleLogRotation ( ) ;
43
+ }
44
+
45
+ /**
46
+ * Gets the Stagehand instance for the current session, creating one if needed
47
+ */
48
+ public async getStagehand ( sessionId : string = this . currentSessionId ) : Promise < Stagehand > {
49
+ let stagehand = this . stagehands . get ( sessionId ) ;
50
+
51
+ if ( ! stagehand ) {
52
+ // Create a new Stagehand instance for this session
53
+ stagehand = new Stagehand ( {
54
+ env : "BROWSERBASE" ,
55
+ logger : ( logLine ) => {
56
+ log ( `Stagehand[${ sessionId } ]: ${ logLine . message } ` , 'info' ) ;
57
+ } ,
58
+ } ) ;
59
+ this . stagehands . set ( sessionId , stagehand ) ;
60
+ }
61
+
62
+ await stagehand . init ( ) ;
63
+
64
+ return stagehand ;
65
+ }
66
+
67
+ /**
68
+ * Sets the Stagehand instance for a specific session
69
+ */
70
+ public setStagehand ( sessionId : string , stagehand : Stagehand ) : void {
71
+ this . stagehands . set ( sessionId , stagehand ) ;
72
+ }
73
+
74
+ /**
75
+ * Removes the Stagehand instance for a specific session
76
+ */
77
+ public async removeStagehand ( sessionId : string ) : Promise < void > {
78
+ const stagehand = this . stagehands . get ( sessionId ) ;
79
+ if ( stagehand ) {
80
+ try {
81
+ await stagehand . close ( ) ;
82
+ } catch ( error ) {
83
+ log ( `Error closing Stagehand for session ${ sessionId } : ${ error } ` , 'error' ) ;
84
+ }
85
+ this . stagehands . delete ( sessionId ) ;
86
+ }
87
+ }
31
88
32
- // Initialize Stagehand
33
- this . stagehand = new Stagehand ( {
34
- env : "BROWSERBASE" ,
35
- logger : ( logLine ) => {
36
- log ( `Stagehand: ${ logLine . message } ` , 'info' ) ;
37
- } ,
38
- } ) ;
89
+ public async getActivePage ( ) : Promise < BrowserSession [ "page" ] | null > {
90
+ // Try to get page from Stagehand first (if available for this session)
91
+ const stagehand = this . stagehands . get ( this . currentSessionId ) ;
92
+ if ( stagehand && stagehand . page && ! stagehand . page . isClosed ( ) ) {
93
+ return stagehand . page ;
94
+ }
95
+
96
+ // Fallback to session manager
97
+ const session = await getSession ( this . currentSessionId , this . config ) ;
98
+ if ( ! session || ! session . page || session . page . isClosed ( ) ) {
99
+ try {
100
+ const currentSession = await getSession (
101
+ this . currentSessionId ,
102
+ this . config
103
+ ) ;
104
+ if (
105
+ ! currentSession ||
106
+ ! currentSession . page ||
107
+ currentSession . page . isClosed ( )
108
+ ) {
109
+ return null ;
110
+ }
111
+ return currentSession . page ;
112
+ } catch ( refreshError ) {
113
+ return null ;
114
+ }
115
+ }
116
+ return session . page ;
117
+ }
118
+
119
+ // Will create a new default session if one doesn't exist
120
+ public async getActiveBrowser ( ) : Promise < BrowserSession [ "browser" ] | null > {
121
+ const session = await getSession ( this . currentSessionId , this . config ) ;
122
+ if ( ! session || ! session . browser || ! session . browser . isConnected ( ) ) {
123
+ try {
124
+ const currentSession = await getSession (
125
+ this . currentSessionId ,
126
+ this . config
127
+ ) ;
128
+ if (
129
+ ! currentSession ||
130
+ ! currentSession . browser ||
131
+ ! currentSession . browser . isConnected ( )
132
+ ) {
133
+ return null ;
134
+ }
135
+ return currentSession . browser ;
136
+ } catch ( refreshError ) {
137
+ return null ;
138
+ }
139
+ }
140
+ return session . browser ;
141
+ }
142
+
143
+ /**
144
+ * Get the active browser without triggering session creation.
145
+ * This is a read-only operation used when we need to check for an existing browser
146
+ * without side effects (e.g., during close operations).
147
+ * @returns The browser if it exists and is connected, null otherwise
148
+ */
149
+ public getActiveBrowserReadOnly ( ) : BrowserSession [ "browser" ] | null {
150
+ const session = getSessionReadOnly ( this . currentSessionId ) ;
151
+ if ( ! session || ! session . browser || ! session . browser . isConnected ( ) ) {
152
+ return null ;
153
+ }
154
+ return session . browser ;
155
+ }
156
+
157
+ /**
158
+ * Get the active page without triggering session creation.
159
+ * This is a read-only operation used when we need to check for an existing page
160
+ * without side effects.
161
+ * @returns The page if it exists and is not closed, null otherwise
162
+ */
163
+ public getActivePageReadOnly ( ) : BrowserSession [ "page" ] | null {
164
+ const session = getSessionReadOnly ( this . currentSessionId ) ;
165
+ if ( ! session || ! session . page || session . page . isClosed ( ) ) {
166
+ return null ;
167
+ }
168
+ return session . page ;
39
169
}
40
170
41
171
async run ( tool : any , args : any ) : Promise < CallToolResult > {
42
172
try {
43
- log ( `Executing tool: ${ tool . name } with args: ${ JSON . stringify ( args ) } ` , 'info' ) ;
44
- const result = await handleToolCall ( tool . name , args , this . stagehand ) ;
45
- log ( `Tool ${ tool . name } completed successfully` , 'info' ) ;
46
- return result ;
173
+ log ( `Executing tool: ${ tool . schema . name } with args: ${ JSON . stringify ( args ) } ` , 'info' ) ;
174
+
175
+ // Check if this tool has a handle method (new session tools)
176
+ // Only use handle method for session create and close tools
177
+ if ( "handle" in tool && typeof tool . handle === "function" &&
178
+ ( tool . schema . name === "browserbase_session_create" || tool . schema . name === "browserbase_session_close" ) ) {
179
+ const toolResult = await tool . handle ( this as any , args ) ;
180
+
181
+ if ( toolResult ?. action ) {
182
+ const actionResult = await toolResult . action ( ) ;
183
+ const content = actionResult ?. content || [ ] ;
184
+
185
+ return {
186
+ content : Array . isArray ( content ) ? content : [ { type : "text" , text : "Action completed successfully." } ] ,
187
+ isError : false ,
188
+ } ;
189
+ } else {
190
+ return {
191
+ content : [ { type : "text" , text : `${ tool . schema . name } completed successfully.` } ] ,
192
+ isError : false ,
193
+ } ;
194
+ }
195
+ } else {
196
+ const stagehand = await this . getStagehand ( ) ;
197
+ const result = await handleToolCall ( tool . schema . name , args , stagehand ) ;
198
+ log ( `Tool ${ tool . schema . name } completed successfully` , 'info' ) ;
199
+ return result ;
200
+ }
47
201
} catch ( error ) {
48
202
const errorMessage = error instanceof Error ? error . message : String ( error ) ;
49
- log ( `Tool ${ tool . name } failed: ${ errorMessage } ` , 'error' ) ;
50
- throw error ;
203
+ log ( `Tool ${ tool . schema ?. name || 'unknown' } failed: ${ errorMessage } ` , 'error' ) ;
204
+ return {
205
+ content : [ { type : "text" , text : `Error: ${ errorMessage } ` } ] ,
206
+ isError : true ,
207
+ } ;
51
208
}
52
209
}
53
210
@@ -61,10 +218,19 @@ export class Context {
61
218
62
219
async close ( ) {
63
220
try {
64
- await this . stagehand . close ( ) ;
65
- log ( 'Stagehand context closed successfully' , 'info' ) ;
221
+ // Close all Stagehand instances
222
+ for ( const [ sessionId , stagehand ] of this . stagehands . entries ( ) ) {
223
+ try {
224
+ await stagehand . close ( ) ;
225
+ log ( `Closed Stagehand for session ${ sessionId } ` , 'info' ) ;
226
+ } catch ( error ) {
227
+ log ( `Error closing Stagehand for session ${ sessionId } : ${ error } ` , 'error' ) ;
228
+ }
229
+ }
230
+ this . stagehands . clear ( ) ;
231
+ log ( 'All Stagehand contexts closed successfully' , 'info' ) ;
66
232
} catch ( error ) {
67
- log ( `Error closing Stagehand context : ${ error } ` , 'error' ) ;
233
+ log ( `Error closing Stagehand contexts : ${ error } ` , 'error' ) ;
68
234
}
69
235
}
70
236
}
0 commit comments