Skip to content

Commit 5bbfa72

Browse files
committed
feat: add SessionManager and session lifecycle tools
SessionManager creates isolated BrowserContext-based sessions within a single Chrome instance. Each session has its own McpContext and Mutex, providing full cookie/storage/network isolation. - Add SessionManager with createSession, getSession, listSessions, closeSession, closeAllSessions - Add create_session, list_sessions, close_session tool definitions - Register session tools in the tools array
1 parent f2c2686 commit 5bbfa72

3 files changed

Lines changed: 241 additions & 0 deletions

File tree

src/SessionManager.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import type {
8+
BrowserContext,
9+
Browser,
10+
Debugger,
11+
Viewport,
12+
} from './third_party/index.js';
13+
import {McpContext} from './McpContext.js';
14+
import type {McpContextOptions} from './McpContext.js';
15+
import {Mutex} from './Mutex.js';
16+
17+
export interface SessionInfo {
18+
sessionId: string;
19+
label?: string;
20+
url?: string;
21+
createdAt: number;
22+
}
23+
24+
export interface Session {
25+
sessionId: string;
26+
label?: string;
27+
url?: string;
28+
createdAt: number;
29+
context: McpContext;
30+
mutex: Mutex;
31+
browserContext: BrowserContext;
32+
}
33+
34+
export interface CreateSessionOptions {
35+
label?: string;
36+
url?: string;
37+
viewport?: Viewport;
38+
}
39+
40+
export class SessionManager {
41+
#sessions = new Map<string, Session>();
42+
#getBrowser: () => Promise<Browser>;
43+
#logger: Debugger;
44+
#contextOptions: McpContextOptions;
45+
#nextSessionId = 1;
46+
47+
constructor(
48+
getBrowser: () => Promise<Browser>,
49+
logger: Debugger,
50+
contextOptions: McpContextOptions,
51+
) {
52+
this.#getBrowser = getBrowser;
53+
this.#logger = logger;
54+
this.#contextOptions = contextOptions;
55+
}
56+
57+
async createSession(
58+
options: CreateSessionOptions = {},
59+
): Promise<SessionInfo> {
60+
const browser = await this.#getBrowser();
61+
if (!browser.connected) {
62+
throw new Error('Browser is not connected');
63+
}
64+
65+
const sessionId = `session_${this.#nextSessionId++}`;
66+
const browserContext = await browser.createBrowserContext();
67+
68+
this.#logger(`SessionManager: created BrowserContext for ${sessionId}`);
69+
70+
const contextOptions: McpContextOptions = {
71+
...this.#contextOptions,
72+
browserContext,
73+
};
74+
75+
const context = await McpContext.from(
76+
browser,
77+
this.#logger,
78+
contextOptions,
79+
);
80+
81+
if (options.url) {
82+
const page = context.getSelectedPage();
83+
await page.goto(options.url, {waitUntil: 'load'});
84+
}
85+
86+
if (options.viewport) {
87+
const page = context.getSelectedPage();
88+
await page.setViewport(options.viewport);
89+
}
90+
91+
const session: Session = {
92+
sessionId,
93+
label: options.label,
94+
url: options.url,
95+
createdAt: Date.now(),
96+
context,
97+
mutex: new Mutex(),
98+
browserContext,
99+
};
100+
101+
this.#sessions.set(sessionId, session);
102+
103+
this.#logger(
104+
`SessionManager: session ${sessionId} ready (label: ${options.label ?? 'none'})`,
105+
);
106+
107+
return {
108+
sessionId,
109+
label: options.label,
110+
url: options.url,
111+
createdAt: session.createdAt,
112+
};
113+
}
114+
115+
getSession(sessionId: string): Session {
116+
const session = this.#sessions.get(sessionId);
117+
if (!session) {
118+
throw new Error(
119+
`Session "${sessionId}" not found. Use create_session to create one.`,
120+
);
121+
}
122+
if (session.browserContext.closed) {
123+
this.#sessions.delete(sessionId);
124+
throw new Error(
125+
`Session "${sessionId}" was closed externally. Create a new session.`,
126+
);
127+
}
128+
return session;
129+
}
130+
131+
listSessions(): SessionInfo[] {
132+
const result: SessionInfo[] = [];
133+
for (const [, session] of this.#sessions) {
134+
if (session.browserContext.closed) {
135+
this.#sessions.delete(session.sessionId);
136+
continue;
137+
}
138+
result.push({
139+
sessionId: session.sessionId,
140+
label: session.label,
141+
url: session.url,
142+
createdAt: session.createdAt,
143+
});
144+
}
145+
return result;
146+
}
147+
148+
async closeSession(sessionId: string): Promise<void> {
149+
const session = this.#sessions.get(sessionId);
150+
if (!session) {
151+
throw new Error(`Session "${sessionId}" not found.`);
152+
}
153+
this.#sessions.delete(sessionId);
154+
session.context.dispose();
155+
if (!session.browserContext.closed) {
156+
await session.browserContext.close();
157+
}
158+
this.#logger(`SessionManager: closed session ${sessionId}`);
159+
}
160+
161+
async closeAllSessions(): Promise<void> {
162+
for (const sessionId of [...this.#sessions.keys()]) {
163+
await this.closeSession(sessionId);
164+
}
165+
}
166+
}

src/tools/session.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {zod} from '../third_party/index.js';
8+
9+
import {ToolCategory} from './categories.js';
10+
import {defineTool} from './ToolDefinition.js';
11+
12+
export const createSession = defineTool({
13+
name: 'create_session',
14+
description: `Create an isolated browser session using a new BrowserContext.
15+
Each session has its own cookies, localStorage, and network state — ideal for
16+
testing multi-user scenarios (e.g., chat between User A and User B).
17+
Returns a sessionId to use with other tools.`,
18+
annotations: {
19+
category: ToolCategory.SESSION,
20+
readOnlyHint: false,
21+
},
22+
schema: {
23+
label: zod
24+
.string()
25+
.optional()
26+
.describe('A human-readable label for the session (e.g., "User A").'),
27+
url: zod
28+
.string()
29+
.optional()
30+
.describe(
31+
'Navigate the initial page to this URL after creating the session.',
32+
),
33+
},
34+
handler: async (_request, response) => {
35+
// The actual session creation is handled by main.ts which intercepts
36+
// this tool and delegates to SessionManager. This handler is a placeholder
37+
// that will never be called directly.
38+
response.appendResponseLine('Session created.');
39+
},
40+
});
41+
42+
export const listSessions = defineTool({
43+
name: 'list_sessions',
44+
description: `List all active browser sessions. Each session is an isolated
45+
BrowserContext with its own cookies, storage, and network state.`,
46+
annotations: {
47+
category: ToolCategory.SESSION,
48+
readOnlyHint: true,
49+
},
50+
schema: {},
51+
handler: async (_request, response) => {
52+
// Handled by main.ts via SessionManager.
53+
response.appendResponseLine('No active sessions.');
54+
},
55+
});
56+
57+
export const closeSession = defineTool({
58+
name: 'close_session',
59+
description: `Close a browser session and its BrowserContext, freeing resources.`,
60+
annotations: {
61+
category: ToolCategory.SESSION,
62+
readOnlyHint: false,
63+
},
64+
schema: {
65+
sessionId: zod
66+
.string()
67+
.describe('The ID of the session to close (from create_session).'),
68+
},
69+
handler: async (_request, response) => {
70+
// Handled by main.ts via SessionManager.
71+
response.appendResponseLine('Session closed.');
72+
},
73+
});

src/tools/tools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import * as pagesTools from './pages.js';
1313
import * as performanceTools from './performance.js';
1414
import * as screenshotTools from './screenshot.js';
1515
import * as scriptTools from './script.js';
16+
import * as sessionTools from './session.js';
1617
import * as snapshotTools from './snapshot.js';
1718
import type {ToolDefinition} from './ToolDefinition.js';
1819

@@ -26,6 +27,7 @@ const tools = [
2627
...Object.values(performanceTools),
2728
...Object.values(screenshotTools),
2829
...Object.values(scriptTools),
30+
...Object.values(sessionTools),
2931
...Object.values(snapshotTools),
3032
] as ToolDefinition[];
3133

0 commit comments

Comments
 (0)