@simplysm/mcp-playwright 13.0.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import {} from "@playwright/mcp";
5
+ import { SessionManager } from "./session-manager.js";
6
+ import { registerProxiedTools } from "./tool-proxy.js";
7
+ const config = {
8
+ browser: { isolated: true, launchOptions: { headless: true } },
9
+ outputDir: ".tmp/playwright"
10
+ };
11
+ const server = new Server(
12
+ { name: "mcp-playwright", version: "1.0.0" },
13
+ {
14
+ capabilities: { tools: {} },
15
+ instructions: "Multi-session Playwright MCP server. Each tool requires a 'sessionId' for browser isolation.\nOutput directory: .tmp/playwright"
16
+ }
17
+ );
18
+ const sessionManager = new SessionManager(config);
19
+ await registerProxiedTools(server, sessionManager);
20
+ const transport = new StdioServerTransport();
21
+ await server.connect(transport);
22
+ async function shutdown() {
23
+ await sessionManager.disposeAll();
24
+ process.exit(0);
25
+ }
26
+ process.on("SIGINT", () => void shutdown());
27
+ process.on("SIGTERM", () => void shutdown());
28
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "mappings": ";AAEA,SAAS,cAAc;AACvB,SAAS,4BAA4B;AACrC,eAAsC;AACtC,SAAS,sBAAsB;AAC/B,SAAS,4BAA4B;AAErC,MAAM,SAA8D;AAAA,EAClE,SAAS,EAAE,UAAU,MAAM,eAAe,EAAE,UAAU,KAAK,EAAE;AAAA,EAC7D,WAAW;AACb;AAEA,MAAM,SAAS,IAAI;AAAA,EACjB,EAAE,MAAM,kBAAkB,SAAS,QAAQ;AAAA,EAC3C;AAAA,IACE,cAAc,EAAE,OAAO,CAAC,EAAE;AAAA,IAC1B,cAAc;AAAA,EAChB;AACF;AAEA,MAAM,iBAAiB,IAAI,eAAe,MAAM;AAEhD,MAAM,qBAAqB,QAAQ,cAAc;AAEjD,MAAM,YAAY,IAAI,qBAAqB;AAC3C,MAAM,OAAO,QAAQ,SAAS;AAE9B,eAAe,WAA0B;AACvC,QAAM,eAAe,WAAW;AAChC,UAAQ,KAAK,CAAC;AAChB;AAEA,QAAQ,GAAG,UAAU,MAAM,KAAK,SAAS,CAAC;AAC1C,QAAQ,GAAG,WAAW,MAAM,KAAK,SAAS,CAAC;",
5
+ "names": []
6
+ }
@@ -0,0 +1,16 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ export declare class SessionManager {
3
+ private readonly config;
4
+ private readonly timeoutMs;
5
+ private readonly _sessions;
6
+ private readonly _pending;
7
+ private readonly _cleanupInterval;
8
+ constructor(config: Record<string, unknown>, timeoutMs?: number);
9
+ getOrCreate(sessionId: string): Promise<Client>;
10
+ destroy(sessionId: string): Promise<void>;
11
+ disposeAll(): Promise<void>;
12
+ list(): string[];
13
+ private _createSession;
14
+ private _cleanup;
15
+ }
16
+ //# sourceMappingURL=session-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAQnE,qBAAa,cAAc;IAMvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAN5B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA8B;IACxD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuC;IAChE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAiC;gBAG/C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,SAAgB;IAOtC,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmB/C,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQzC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAMjC,IAAI,IAAI,MAAM,EAAE;YAIF,cAAc;YASd,QAAQ;CAQvB"}
@@ -0,0 +1,68 @@
1
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { createConnection } from "@playwright/mcp";
4
+ class SessionManager {
5
+ constructor(config, timeoutMs = 5 * 60 * 1e3) {
6
+ this.config = config;
7
+ this.timeoutMs = timeoutMs;
8
+ this._cleanupInterval = setInterval(() => {
9
+ void this._cleanup();
10
+ }, 3e4);
11
+ }
12
+ _sessions = /* @__PURE__ */ new Map();
13
+ _pending = /* @__PURE__ */ new Map();
14
+ _cleanupInterval;
15
+ async getOrCreate(sessionId) {
16
+ let session = this._sessions.get(sessionId);
17
+ if (session == null) {
18
+ let pending = this._pending.get(sessionId);
19
+ if (pending == null) {
20
+ pending = this._createSession().then((s) => {
21
+ this._sessions.set(sessionId, s);
22
+ return s;
23
+ }).finally(() => {
24
+ this._pending.delete(sessionId);
25
+ });
26
+ this._pending.set(sessionId, pending);
27
+ }
28
+ session = await pending;
29
+ }
30
+ session.lastUsed = Date.now();
31
+ return session.client;
32
+ }
33
+ async destroy(sessionId) {
34
+ const session = this._sessions.get(sessionId);
35
+ if (session != null) {
36
+ this._sessions.delete(sessionId);
37
+ await session.client.close();
38
+ }
39
+ }
40
+ async disposeAll() {
41
+ clearInterval(this._cleanupInterval);
42
+ const ids = [...this._sessions.keys()];
43
+ await Promise.all(ids.map((id) => this.destroy(id)));
44
+ }
45
+ list() {
46
+ return [...this._sessions.keys()];
47
+ }
48
+ async _createSession() {
49
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
50
+ const innerServer = await createConnection(this.config);
51
+ await innerServer.connect(serverTransport);
52
+ const client = new Client({ name: "mcp-playwright-proxy", version: "1.0.0" });
53
+ await client.connect(clientTransport);
54
+ return { client, lastUsed: Date.now() };
55
+ }
56
+ async _cleanup() {
57
+ const now = Date.now();
58
+ for (const [id, session] of this._sessions) {
59
+ if (now - session.lastUsed > this.timeoutMs) {
60
+ await this.destroy(id);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ export {
66
+ SessionManager
67
+ };
68
+ //# sourceMappingURL=session-manager.js.map
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/session-manager.ts"],
4
+ "mappings": "AAAA,SAAS,yBAAyB;AAClC,SAAS,cAAc;AACvB,SAAS,wBAAwB;AAO1B,MAAM,eAAe;AAAA,EAK1B,YACmB,QACA,YAAY,IAAI,KAAK,KACtC;AAFiB;AACA;AAEjB,SAAK,mBAAmB,YAAY,MAAM;AACxC,WAAK,KAAK,SAAS;AAAA,IACrB,GAAG,GAAM;AAAA,EACX;AAAA,EAXiB,YAAY,oBAAI,IAAqB;AAAA,EACrC,WAAW,oBAAI,IAA8B;AAAA,EAC7C;AAAA,EAWjB,MAAM,YAAY,WAAoC;AACpD,QAAI,UAAU,KAAK,UAAU,IAAI,SAAS;AAC1C,QAAI,WAAW,MAAM;AACnB,UAAI,UAAU,KAAK,SAAS,IAAI,SAAS;AACzC,UAAI,WAAW,MAAM;AACnB,kBAAU,KAAK,eAAe,EAAE,KAAK,CAAC,MAAM;AAC1C,eAAK,UAAU,IAAI,WAAW,CAAC;AAC/B,iBAAO;AAAA,QACT,CAAC,EAAE,QAAQ,MAAM;AACf,eAAK,SAAS,OAAO,SAAS;AAAA,QAChC,CAAC;AACD,aAAK,SAAS,IAAI,WAAW,OAAO;AAAA,MACtC;AACA,gBAAU,MAAM;AAAA,IAClB;AACA,YAAQ,WAAW,KAAK,IAAI;AAC5B,WAAO,QAAQ;AAAA,EACjB;AAAA,EAEA,MAAM,QAAQ,WAAkC;AAC9C,UAAM,UAAU,KAAK,UAAU,IAAI,SAAS;AAC5C,QAAI,WAAW,MAAM;AACnB,WAAK,UAAU,OAAO,SAAS;AAC/B,YAAM,QAAQ,OAAO,MAAM;AAAA,IAC7B;AAAA,EACF;AAAA,EAEA,MAAM,aAA4B;AAChC,kBAAc,KAAK,gBAAgB;AACnC,UAAM,MAAM,CAAC,GAAG,KAAK,UAAU,KAAK,CAAC;AACrC,UAAM,QAAQ,IAAI,IAAI,IAAI,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC,CAAC;AAAA,EACrD;AAAA,EAEA,OAAiB;AACf,WAAO,CAAC,GAAG,KAAK,UAAU,KAAK,CAAC;AAAA,EAClC;AAAA,EAEA,MAAc,iBAAmC;AAC/C,UAAM,CAAC,iBAAiB,eAAe,IAAI,kBAAkB,iBAAiB;AAC9E,UAAM,cAAc,MAAM,iBAAiB,KAAK,MAAe;AAC/D,UAAM,YAAY,QAAQ,eAAe;AACzC,UAAM,SAAS,IAAI,OAAO,EAAE,MAAM,wBAAwB,SAAS,QAAQ,CAAC;AAC5E,UAAM,OAAO,QAAQ,eAAe;AACpC,WAAO,EAAE,QAAQ,UAAU,KAAK,IAAI,EAAE;AAAA,EACxC;AAAA,EAEA,MAAc,WAA0B;AACtC,UAAM,MAAM,KAAK,IAAI;AACrB,eAAW,CAAC,IAAI,OAAO,KAAK,KAAK,WAAW;AAC1C,UAAI,MAAM,QAAQ,WAAW,KAAK,WAAW;AAC3C,cAAM,KAAK,QAAQ,EAAE;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AACF;",
5
+ "names": []
6
+ }
@@ -0,0 +1,6 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { type Tool } from "@modelcontextprotocol/sdk/types.js";
3
+ import { type SessionManager } from "./session-manager.js";
4
+ export declare function injectSessionId(tool: Tool): Tool;
5
+ export declare function registerProxiedTools(server: Server, sessionManager: SessionManager): Promise<void>;
6
+ //# sourceMappingURL=tool-proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-proxy.d.ts","sourceRoot":"","sources":["../src/tool-proxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAGL,KAAK,IAAI,EACV,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAahD;AAED,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,cAAc,GAC7B,OAAO,CAAC,IAAI,CAAC,CAiEf"}
@@ -0,0 +1,84 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import {
3
+ ListToolsRequestSchema,
4
+ CallToolRequestSchema
5
+ } from "@modelcontextprotocol/sdk/types.js";
6
+ import {} from "./session-manager.js";
7
+ function injectSessionId(tool) {
8
+ const existing = tool.inputSchema.required;
9
+ return {
10
+ ...tool,
11
+ inputSchema: {
12
+ ...tool.inputSchema,
13
+ properties: {
14
+ sessionId: { type: "string", description: "Session ID for browser isolation" },
15
+ ...tool.inputSchema.properties ?? {}
16
+ },
17
+ required: ["sessionId", ...existing?.filter((r) => r !== "sessionId") ?? []]
18
+ }
19
+ };
20
+ }
21
+ async function registerProxiedTools(server, sessionManager) {
22
+ const bootstrapClient = await sessionManager.getOrCreate("__bootstrap__");
23
+ let playwrightTools;
24
+ try {
25
+ ({ tools: playwrightTools } = await bootstrapClient.listTools());
26
+ } finally {
27
+ await sessionManager.destroy("__bootstrap__");
28
+ }
29
+ const proxiedTools = playwrightTools.map(injectSessionId);
30
+ const allTools = [
31
+ ...proxiedTools,
32
+ {
33
+ name: "session_close",
34
+ description: "Close a browser session and release resources",
35
+ inputSchema: {
36
+ type: "object",
37
+ properties: {
38
+ sessionId: { type: "string", description: "Session ID to close" }
39
+ },
40
+ required: ["sessionId"]
41
+ }
42
+ },
43
+ {
44
+ name: "session_list",
45
+ description: "List all active browser session IDs",
46
+ inputSchema: {
47
+ type: "object",
48
+ properties: {}
49
+ }
50
+ }
51
+ ];
52
+ server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: allTools }));
53
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
54
+ const { name, arguments: args = {} } = request.params;
55
+ if (name === "session_close") {
56
+ const { sessionId: sessionId2 } = args;
57
+ await sessionManager.destroy(sessionId2);
58
+ return { content: [{ type: "text", text: `Session '${sessionId2}' closed.` }] };
59
+ }
60
+ if (name === "session_list") {
61
+ const sessions = sessionManager.list();
62
+ return {
63
+ content: [{ type: "text", text: JSON.stringify(sessions, null, 2) }]
64
+ };
65
+ }
66
+ const sessionId = args["sessionId"];
67
+ if (typeof sessionId !== "string" || sessionId === "") {
68
+ throw new Error("Missing required argument: sessionId");
69
+ }
70
+ const { sessionId: _, ...toolArgs } = args;
71
+ const client = await sessionManager.getOrCreate(sessionId);
72
+ try {
73
+ return await client.callTool({ name, arguments: toolArgs });
74
+ } catch (err) {
75
+ await sessionManager.destroy(sessionId);
76
+ throw err;
77
+ }
78
+ });
79
+ }
80
+ export {
81
+ injectSessionId,
82
+ registerProxiedTools
83
+ };
84
+ //# sourceMappingURL=tool-proxy.js.map
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/tool-proxy.ts"],
4
+ "mappings": "AAAA,SAAS,cAAc;AACvB;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP,eAAoC;AAE7B,SAAS,gBAAgB,MAAkB;AAChD,QAAM,WAAW,KAAK,YAAY;AAClC,SAAO;AAAA,IACL,GAAG;AAAA,IACH,aAAa;AAAA,MACX,GAAG,KAAK;AAAA,MACR,YAAY;AAAA,QACV,WAAW,EAAE,MAAM,UAAU,aAAa,mCAAmC;AAAA,QAC7E,GAAI,KAAK,YAAY,cAAc,CAAC;AAAA,MACtC;AAAA,MACA,UAAU,CAAC,aAAa,GAAI,UAAU,OAAO,CAAC,MAAM,MAAM,WAAW,KAAK,CAAC,CAAE;AAAA,IAC/E;AAAA,EACF;AACF;AAEA,eAAsB,qBACpB,QACA,gBACe;AAEf,QAAM,kBAAkB,MAAM,eAAe,YAAY,eAAe;AACxE,MAAI;AACJ,MAAI;AACF,KAAC,EAAE,OAAO,gBAAgB,IAAI,MAAM,gBAAgB,UAAU;AAAA,EAChE,UAAE;AACA,UAAM,eAAe,QAAQ,eAAe;AAAA,EAC9C;AAEA,QAAM,eAAe,gBAAgB,IAAI,eAAe;AACxD,QAAM,WAAmB;AAAA,IACvB,GAAG;AAAA,IACH;AAAA,MACE,MAAM;AAAA,MACN,aAAa;AAAA,MACb,aAAa;AAAA,QACX,MAAM;AAAA,QACN,YAAY;AAAA,UACV,WAAW,EAAE,MAAM,UAAU,aAAa,sBAAsB;AAAA,QAClE;AAAA,QACA,UAAU,CAAC,WAAW;AAAA,MACxB;AAAA,IACF;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,aAAa;AAAA,MACb,aAAa;AAAA,QACX,MAAM;AAAA,QACN,YAAY,CAAC;AAAA,MACf;AAAA,IACF;AAAA,EACF;AAEA,SAAO,kBAAkB,wBAAwB,OAAO,EAAE,OAAO,SAAS,EAAE;AAE5E,SAAO,kBAAkB,uBAAuB,OAAO,YAAY;AACjE,UAAM,EAAE,MAAM,WAAW,OAAO,CAAC,EAAE,IAAI,QAAQ;AAE/C,QAAI,SAAS,iBAAiB;AAC5B,YAAM,EAAE,WAAAA,WAAU,IAAI;AACtB,YAAM,eAAe,QAAQA,UAAS;AACtC,aAAO,EAAE,SAAS,CAAC,EAAE,MAAM,QAAiB,MAAM,YAAYA,UAAS,YAAY,CAAC,EAAE;AAAA,IACxF;AAEA,QAAI,SAAS,gBAAgB;AAC3B,YAAM,WAAW,eAAe,KAAK;AACrC,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAiB,MAAM,KAAK,UAAU,UAAU,MAAM,CAAC,EAAE,CAAC;AAAA,MAC9E;AAAA,IACF;AAEA,UAAM,YAAY,KAAK,WAAW;AAClC,QAAI,OAAO,cAAc,YAAY,cAAc,IAAI;AACrD,YAAM,IAAI,MAAM,sCAAsC;AAAA,IACxD;AACA,UAAM,EAAE,WAAW,GAAG,GAAG,SAAS,IAAI;AACtC,UAAM,SAAS,MAAM,eAAe,YAAY,SAAS;AACzD,QAAI;AACF,aAAO,MAAM,OAAO,SAAS,EAAE,MAAM,WAAW,SAAS,CAAC;AAAA,IAC5D,SAAS,KAAK;AACZ,YAAM,eAAe,QAAQ,SAAS;AACtC,YAAM;AAAA,IACR;AAAA,EACF,CAAC;AACH;",
5
+ "names": ["sessionId"]
6
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@simplysm/mcp-playwright",
3
+ "version": "13.0.71",
4
+ "description": "Simplysm MCP server — multi-session Playwright proxy",
5
+ "author": "simplysm",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/kslhunter/simplysm.git",
10
+ "directory": "packages/mcp-playwright"
11
+ },
12
+ "type": "module",
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
19
+ "sideEffects": false,
20
+ "dependencies": {
21
+ "@modelcontextprotocol/sdk": "^1.27.1",
22
+ "@playwright/mcp": "^0.0.68",
23
+ "playwright": "^1.58.2"
24
+ },
25
+ "bin": {
26
+ "sd-mcp-playwright": "./dist/index.js"
27
+ }
28
+ }
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { type createConnection } from "@playwright/mcp";
6
+ import { SessionManager } from "./session-manager.js";
7
+ import { registerProxiedTools } from "./tool-proxy.js";
8
+
9
+ const config: NonNullable<Parameters<typeof createConnection>[0]> = {
10
+ browser: { isolated: true, launchOptions: { headless: true } },
11
+ outputDir: ".tmp/playwright",
12
+ };
13
+
14
+ const server = new Server(
15
+ { name: "mcp-playwright", version: "1.0.0" },
16
+ {
17
+ capabilities: { tools: {} },
18
+ instructions: "Multi-session Playwright MCP server. Each tool requires a 'sessionId' for browser isolation.\nOutput directory: .tmp/playwright",
19
+ },
20
+ );
21
+
22
+ const sessionManager = new SessionManager(config);
23
+
24
+ await registerProxiedTools(server, sessionManager);
25
+
26
+ const transport = new StdioServerTransport();
27
+ await server.connect(transport);
28
+
29
+ async function shutdown(): Promise<void> {
30
+ await sessionManager.disposeAll();
31
+ process.exit(0);
32
+ }
33
+
34
+ process.on("SIGINT", () => void shutdown());
35
+ process.on("SIGTERM", () => void shutdown());
@@ -0,0 +1,78 @@
1
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { createConnection } from "@playwright/mcp";
4
+
5
+ interface Session {
6
+ client: Client;
7
+ lastUsed: number;
8
+ }
9
+
10
+ export class SessionManager {
11
+ private readonly _sessions = new Map<string, Session>();
12
+ private readonly _pending = new Map<string, Promise<Session>>();
13
+ private readonly _cleanupInterval: ReturnType<typeof setInterval>;
14
+
15
+ constructor(
16
+ private readonly config: Record<string, unknown>,
17
+ private readonly timeoutMs = 5 * 60 * 1000,
18
+ ) {
19
+ this._cleanupInterval = setInterval(() => {
20
+ void this._cleanup();
21
+ }, 30_000);
22
+ }
23
+
24
+ async getOrCreate(sessionId: string): Promise<Client> {
25
+ let session = this._sessions.get(sessionId);
26
+ if (session == null) {
27
+ let pending = this._pending.get(sessionId);
28
+ if (pending == null) {
29
+ pending = this._createSession().then((s) => {
30
+ this._sessions.set(sessionId, s);
31
+ return s;
32
+ }).finally(() => {
33
+ this._pending.delete(sessionId);
34
+ });
35
+ this._pending.set(sessionId, pending);
36
+ }
37
+ session = await pending;
38
+ }
39
+ session.lastUsed = Date.now();
40
+ return session.client;
41
+ }
42
+
43
+ async destroy(sessionId: string): Promise<void> {
44
+ const session = this._sessions.get(sessionId);
45
+ if (session != null) {
46
+ this._sessions.delete(sessionId);
47
+ await session.client.close();
48
+ }
49
+ }
50
+
51
+ async disposeAll(): Promise<void> {
52
+ clearInterval(this._cleanupInterval);
53
+ const ids = [...this._sessions.keys()];
54
+ await Promise.all(ids.map((id) => this.destroy(id)));
55
+ }
56
+
57
+ list(): string[] {
58
+ return [...this._sessions.keys()];
59
+ }
60
+
61
+ private async _createSession(): Promise<Session> {
62
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
63
+ const innerServer = await createConnection(this.config as never);
64
+ await innerServer.connect(serverTransport);
65
+ const client = new Client({ name: "mcp-playwright-proxy", version: "1.0.0" });
66
+ await client.connect(clientTransport);
67
+ return { client, lastUsed: Date.now() };
68
+ }
69
+
70
+ private async _cleanup(): Promise<void> {
71
+ const now = Date.now();
72
+ for (const [id, session] of this._sessions) {
73
+ if (now - session.lastUsed > this.timeoutMs) {
74
+ await this.destroy(id);
75
+ }
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,92 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import {
3
+ ListToolsRequestSchema,
4
+ CallToolRequestSchema,
5
+ type Tool,
6
+ } from "@modelcontextprotocol/sdk/types.js";
7
+ import { type SessionManager } from "./session-manager.js";
8
+
9
+ export function injectSessionId(tool: Tool): Tool {
10
+ const existing = tool.inputSchema.required as string[] | undefined;
11
+ return {
12
+ ...tool,
13
+ inputSchema: {
14
+ ...tool.inputSchema,
15
+ properties: {
16
+ sessionId: { type: "string", description: "Session ID for browser isolation" },
17
+ ...(tool.inputSchema.properties ?? {}),
18
+ },
19
+ required: ["sessionId", ...(existing?.filter((r) => r !== "sessionId") ?? [])],
20
+ },
21
+ };
22
+ }
23
+
24
+ export async function registerProxiedTools(
25
+ server: Server,
26
+ sessionManager: SessionManager,
27
+ ): Promise<void> {
28
+ // Bootstrap: get tool list from @playwright/mcp (no browser launched)
29
+ const bootstrapClient = await sessionManager.getOrCreate("__bootstrap__");
30
+ let playwrightTools: Tool[];
31
+ try {
32
+ ({ tools: playwrightTools } = await bootstrapClient.listTools());
33
+ } finally {
34
+ await sessionManager.destroy("__bootstrap__");
35
+ }
36
+
37
+ const proxiedTools = playwrightTools.map(injectSessionId);
38
+ const allTools: Tool[] = [
39
+ ...proxiedTools,
40
+ {
41
+ name: "session_close",
42
+ description: "Close a browser session and release resources",
43
+ inputSchema: {
44
+ type: "object",
45
+ properties: {
46
+ sessionId: { type: "string", description: "Session ID to close" },
47
+ },
48
+ required: ["sessionId"],
49
+ },
50
+ },
51
+ {
52
+ name: "session_list",
53
+ description: "List all active browser session IDs",
54
+ inputSchema: {
55
+ type: "object",
56
+ properties: {},
57
+ },
58
+ },
59
+ ];
60
+
61
+ server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: allTools }));
62
+
63
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
64
+ const { name, arguments: args = {} } = request.params;
65
+
66
+ if (name === "session_close") {
67
+ const { sessionId } = args as { sessionId: string };
68
+ await sessionManager.destroy(sessionId);
69
+ return { content: [{ type: "text" as const, text: `Session '${sessionId}' closed.` }] };
70
+ }
71
+
72
+ if (name === "session_list") {
73
+ const sessions = sessionManager.list();
74
+ return {
75
+ content: [{ type: "text" as const, text: JSON.stringify(sessions, null, 2) }],
76
+ };
77
+ }
78
+
79
+ const sessionId = args["sessionId"];
80
+ if (typeof sessionId !== "string" || sessionId === "") {
81
+ throw new Error("Missing required argument: sessionId");
82
+ }
83
+ const { sessionId: _, ...toolArgs } = args as { sessionId: string; [key: string]: unknown };
84
+ const client = await sessionManager.getOrCreate(sessionId);
85
+ try {
86
+ return await client.callTool({ name, arguments: toolArgs });
87
+ } catch (err) {
88
+ await sessionManager.destroy(sessionId);
89
+ throw err;
90
+ }
91
+ });
92
+ }