@simplysm/mcp-playwright 13.0.72 → 13.0.74
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.
- package/README.md +135 -0
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/session-manager.d.ts +4 -1
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +24 -8
- package/dist/session-manager.js.map +1 -1
- package/dist/tool-proxy.d.ts.map +1 -1
- package/dist/tool-proxy.js +27 -7
- package/dist/tool-proxy.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +8 -3
- package/src/session-manager.ts +31 -9
- package/src/tool-proxy.ts +27 -6
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @simplysm/mcp-playwright
|
|
2
|
+
|
|
3
|
+
Simplysm MCP server — multi-session Playwright proxy.
|
|
4
|
+
|
|
5
|
+
Wraps [`@playwright/mcp`](https://github.com/microsoft/playwright-mcp) and adds **session isolation**: every Playwright tool call requires a `sessionId` argument that routes the request to a dedicated browser instance. Multiple sessions can exist concurrently without interfering with each other.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add -g @simplysm/mcp-playwright
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or run directly with npx/pnpm dlx:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm dlx @simplysm/mcp-playwright
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## MCP Configuration
|
|
20
|
+
|
|
21
|
+
Add the server to your MCP client configuration (e.g., Claude Desktop's `claude_desktop_config.json`):
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"playwright": {
|
|
27
|
+
"command": "pnpm",
|
|
28
|
+
"args": ["dlx", "@simplysm/mcp-playwright"]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The server communicates over **stdio** using the Model Context Protocol.
|
|
35
|
+
|
|
36
|
+
## How It Works
|
|
37
|
+
|
|
38
|
+
- The server starts a single MCP process.
|
|
39
|
+
- Each call to a Playwright tool must include a `sessionId` string.
|
|
40
|
+
- On the first call with a given `sessionId`, the server launches a dedicated headless browser (isolated Playwright context).
|
|
41
|
+
- Subsequent calls with the same `sessionId` reuse that browser context.
|
|
42
|
+
- Sessions are automatically cleaned up after **5 minutes of inactivity**.
|
|
43
|
+
- Output files (screenshots, traces, etc.) are written to `.tmp/playwright/`.
|
|
44
|
+
|
|
45
|
+
## Session Management Tools
|
|
46
|
+
|
|
47
|
+
### `session_list`
|
|
48
|
+
|
|
49
|
+
Returns all currently active session IDs.
|
|
50
|
+
|
|
51
|
+
**Input:** _(none)_
|
|
52
|
+
|
|
53
|
+
**Output:** JSON array of session ID strings.
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
// Example response content
|
|
57
|
+
["session-a", "session-b"]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### `session_close`
|
|
61
|
+
|
|
62
|
+
Closes a specific browser session and releases its resources.
|
|
63
|
+
|
|
64
|
+
**Input:**
|
|
65
|
+
|
|
66
|
+
| Field | Type | Required | Description |
|
|
67
|
+
|-------|------|----------|-------------|
|
|
68
|
+
| `sessionId` | `string` | Yes | ID of the session to close |
|
|
69
|
+
|
|
70
|
+
**Output:** Confirmation text: `Session '<sessionId>' closed.`
|
|
71
|
+
|
|
72
|
+
## Playwright Proxy Tools
|
|
73
|
+
|
|
74
|
+
All tools from `@playwright/mcp` are forwarded by this server. Each forwarded tool has a `sessionId` field **prepended** to its original input schema.
|
|
75
|
+
|
|
76
|
+
**Every proxied tool requires:**
|
|
77
|
+
|
|
78
|
+
| Field | Type | Required | Description |
|
|
79
|
+
|-------|------|----------|-------------|
|
|
80
|
+
| `sessionId` | `string` | Yes | Session ID for browser isolation |
|
|
81
|
+
| _(original fields)_ | — | — | Same as the original `@playwright/mcp` tool |
|
|
82
|
+
|
|
83
|
+
If an error occurs during a proxied tool call, the session is automatically destroyed to avoid leaving a browser in a broken state.
|
|
84
|
+
|
|
85
|
+
## Server Internals
|
|
86
|
+
|
|
87
|
+
### `SessionManager`
|
|
88
|
+
|
|
89
|
+
Internal class that manages the lifecycle of per-session Playwright MCP clients.
|
|
90
|
+
|
|
91
|
+
**Constructor**
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
new SessionManager(config: Record<string, unknown>, timeoutMs?: number)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
| Parameter | Type | Default | Description |
|
|
98
|
+
|-----------|------|---------|-------------|
|
|
99
|
+
| `config` | `Record<string, unknown>` | — | Configuration passed to `@playwright/mcp`'s `createConnection` |
|
|
100
|
+
| `timeoutMs` | `number` | `300000` (5 min) | Inactivity timeout before a session is auto-cleaned |
|
|
101
|
+
|
|
102
|
+
**Methods**
|
|
103
|
+
|
|
104
|
+
| Method | Signature | Description |
|
|
105
|
+
|--------|-----------|-------------|
|
|
106
|
+
| `getOrCreate` | `(sessionId: string) => Promise<Client>` | Returns an existing MCP client for the session, or creates a new one. Updates the last-used timestamp on each call. |
|
|
107
|
+
| `destroy` | `(sessionId: string) => Promise<void>` | Closes and removes a single session. No-op if the session does not exist. |
|
|
108
|
+
| `disposeAll` | `() => Promise<void>` | Closes all sessions and stops the cleanup interval. Called on `SIGINT`/`SIGTERM`. |
|
|
109
|
+
| `list` | `() => string[]` | Returns an array of all currently active session IDs. |
|
|
110
|
+
|
|
111
|
+
### `injectSessionId`
|
|
112
|
+
|
|
113
|
+
Internal utility that adds the `sessionId` field to a Playwright tool's input schema before the tool is registered on the proxy server.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
function injectSessionId(tool: Tool): Tool
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- Prepends `sessionId` to `inputSchema.properties`.
|
|
120
|
+
- Ensures `sessionId` appears first in the `required` array.
|
|
121
|
+
- Deduplicates `sessionId` if it is already present in `required`.
|
|
122
|
+
- Returns a new tool object; the original is not mutated.
|
|
123
|
+
|
|
124
|
+
### `registerProxiedTools`
|
|
125
|
+
|
|
126
|
+
Internal function that wires up all Playwright tools and the two session management tools (`session_list`, `session_close`) to an MCP `Server` instance.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
async function registerProxiedTools(
|
|
130
|
+
server: Server,
|
|
131
|
+
sessionManager: SessionManager,
|
|
132
|
+
): Promise<void>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
At startup it creates a temporary bootstrap session to discover the tool list from `@playwright/mcp`, then destroys it before serving real requests.
|
package/dist/index.js
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import {} from "
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
5
|
import { SessionManager } from "./session-manager.js";
|
|
6
6
|
import { registerProxiedTools } from "./tool-proxy.js";
|
|
7
|
+
const _require = createRequire(import.meta.url);
|
|
8
|
+
const { version } = _require("../package.json");
|
|
7
9
|
const config = {
|
|
8
10
|
browser: { isolated: true, launchOptions: { headless: true } },
|
|
9
11
|
outputDir: ".tmp/playwright"
|
|
10
12
|
};
|
|
11
13
|
const server = new Server(
|
|
12
|
-
{ name: "mcp-playwright", version
|
|
14
|
+
{ name: "mcp-playwright", version },
|
|
13
15
|
{
|
|
14
16
|
capabilities: { tools: {} },
|
|
15
17
|
instructions: "Multi-session Playwright MCP server. Each tool requires a 'sessionId' for browser isolation.\nOutput directory: .tmp/playwright"
|
|
16
18
|
}
|
|
17
19
|
);
|
|
18
|
-
const sessionManager = new SessionManager(config);
|
|
20
|
+
const sessionManager = new SessionManager(config, void 0, version);
|
|
19
21
|
await registerProxiedTools(server, sessionManager);
|
|
20
22
|
const transport = new StdioServerTransport();
|
|
21
23
|
await server.connect(transport);
|
|
22
24
|
async function shutdown() {
|
|
23
25
|
await sessionManager.disposeAll();
|
|
26
|
+
await server.close();
|
|
24
27
|
process.exit(0);
|
|
25
28
|
}
|
|
26
29
|
process.on("SIGINT", () => void shutdown());
|
package/dist/index.js.map
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"mappings": ";AAEA,SAAS,cAAc;AACvB,SAAS,4BAA4B;
|
|
4
|
+
"mappings": ";AAEA,SAAS,cAAc;AACvB,SAAS,4BAA4B;AAErC,SAAS,qBAAqB;AAC9B,SAAS,sBAAsB;AAC/B,SAAS,4BAA4B;AAErC,MAAM,WAAW,cAAc,YAAY,GAAG;AAC9C,MAAM,EAAE,QAAQ,IAAI,SAAS,iBAAiB;AAE9C,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,QAAQ;AAAA,EAClC;AAAA,IACE,cAAc,EAAE,OAAO,CAAC,EAAE;AAAA,IAC1B,cAAc;AAAA,EAChB;AACF;AAEA,MAAM,iBAAiB,IAAI,eAAe,QAAQ,QAAW,OAAO;AAEpE,MAAM,qBAAqB,QAAQ,cAAc;AAEjD,MAAM,YAAY,IAAI,qBAAqB;AAC3C,MAAM,OAAO,QAAQ,SAAS;AAE9B,eAAe,WAA0B;AACvC,QAAM,eAAe,WAAW;AAChC,QAAM,OAAO,MAAM;AACnB,UAAQ,KAAK,CAAC;AAChB;AAEA,QAAQ,GAAG,UAAU,MAAM,KAAK,SAAS,CAAC;AAC1C,QAAQ,GAAG,WAAW,MAAM,KAAK,SAAS,CAAC;",
|
|
5
5
|
"names": []
|
|
6
6
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import type { createConnection as CreateConnectionFn } from "@playwright/mcp";
|
|
2
3
|
export declare class SessionManager {
|
|
3
4
|
private readonly config;
|
|
4
5
|
private readonly timeoutMs;
|
|
6
|
+
private readonly version;
|
|
5
7
|
private readonly _sessions;
|
|
6
8
|
private readonly _pending;
|
|
7
9
|
private readonly _cleanupInterval;
|
|
8
|
-
|
|
10
|
+
private _cleanupRunning;
|
|
11
|
+
constructor(config: NonNullable<Parameters<typeof CreateConnectionFn>[0]>, timeoutMs?: number, version?: string);
|
|
9
12
|
getOrCreate(sessionId: string): Promise<Client>;
|
|
10
13
|
destroy(sessionId: string): Promise<void>;
|
|
11
14
|
disposeAll(): Promise<void>;
|
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAGnE,OAAO,KAAK,EAAE,gBAAgB,IAAI,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAY9E,qBAAa,cAAc;IAOvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAR1B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA8B;IACxD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuC;IAChE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAiC;IAClE,OAAO,CAAC,eAAe,CAAS;gBAGb,MAAM,EAAE,WAAW,CAAC,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC,EAC7D,SAAS,SAAgB,EACzB,OAAO,SAAU;IAO9B,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmB/C,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASzC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAMjC,IAAI,IAAI,MAAM,EAAE;YAIF,cAAc;YAcd,QAAQ;CAevB"}
|
package/dist/session-manager.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
2
2
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
-
import {
|
|
3
|
+
import { Server as McpServer } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
const _require = createRequire(import.meta.url);
|
|
6
|
+
const { createConnection } = _require("@playwright/mcp");
|
|
4
7
|
class SessionManager {
|
|
5
|
-
constructor(config, timeoutMs = 5 * 60 * 1e3) {
|
|
8
|
+
constructor(config, timeoutMs = 5 * 60 * 1e3, version = "0.0.0") {
|
|
6
9
|
this.config = config;
|
|
7
10
|
this.timeoutMs = timeoutMs;
|
|
11
|
+
this.version = version;
|
|
8
12
|
this._cleanupInterval = setInterval(() => {
|
|
9
13
|
void this._cleanup();
|
|
10
14
|
}, 3e4);
|
|
@@ -12,6 +16,7 @@ class SessionManager {
|
|
|
12
16
|
_sessions = /* @__PURE__ */ new Map();
|
|
13
17
|
_pending = /* @__PURE__ */ new Map();
|
|
14
18
|
_cleanupInterval;
|
|
19
|
+
_cleanupRunning = false;
|
|
15
20
|
async getOrCreate(sessionId) {
|
|
16
21
|
let session = this._sessions.get(sessionId);
|
|
17
22
|
if (session == null) {
|
|
@@ -35,6 +40,7 @@ class SessionManager {
|
|
|
35
40
|
if (session != null) {
|
|
36
41
|
this._sessions.delete(sessionId);
|
|
37
42
|
await session.client.close();
|
|
43
|
+
await session.innerServer.close();
|
|
38
44
|
}
|
|
39
45
|
}
|
|
40
46
|
async disposeAll() {
|
|
@@ -49,16 +55,26 @@ class SessionManager {
|
|
|
49
55
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
50
56
|
const innerServer = await createConnection(this.config);
|
|
51
57
|
await innerServer.connect(serverTransport);
|
|
52
|
-
const client = new Client({ name: "mcp-playwright-proxy", version:
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
const client = new Client({ name: "mcp-playwright-proxy", version: this.version });
|
|
59
|
+
try {
|
|
60
|
+
await client.connect(clientTransport);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
await innerServer.close();
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
return { client, innerServer, lastUsed: Date.now() };
|
|
55
66
|
}
|
|
56
67
|
async _cleanup() {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
if (this._cleanupRunning) return;
|
|
69
|
+
this._cleanupRunning = true;
|
|
70
|
+
try {
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
const expired = [...this._sessions.entries()].filter(([, s]) => now - s.lastUsed > this.timeoutMs).map(([id]) => id);
|
|
73
|
+
for (const id of expired) {
|
|
60
74
|
await this.destroy(id);
|
|
61
75
|
}
|
|
76
|
+
} finally {
|
|
77
|
+
this._cleanupRunning = false;
|
|
62
78
|
}
|
|
63
79
|
}
|
|
64
80
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/session-manager.ts"],
|
|
4
|
-
"mappings": "AAAA,SAAS,yBAAyB;AAClC,SAAS,cAAc;AACvB,SAAS,
|
|
4
|
+
"mappings": "AAAA,SAAS,yBAAyB;AAClC,SAAS,cAAc;AACvB,SAAS,UAAU,iBAAiB;AACpC,SAAS,qBAAqB;AAI9B,MAAM,WAAW,cAAc,YAAY,GAAG;AAC9C,MAAM,EAAE,iBAAiB,IAAI,SAAS,iBAAiB;AAQhD,MAAM,eAAe;AAAA,EAM1B,YACmB,QACA,YAAY,IAAI,KAAK,KACrB,UAAU,SAC3B;AAHiB;AACA;AACA;AAEjB,SAAK,mBAAmB,YAAY,MAAM;AACxC,WAAK,KAAK,SAAS;AAAA,IACrB,GAAG,GAAM;AAAA,EACX;AAAA,EAbiB,YAAY,oBAAI,IAAqB;AAAA,EACrC,WAAW,oBAAI,IAA8B;AAAA,EAC7C;AAAA,EACT,kBAAkB;AAAA,EAY1B,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;AAC3B,YAAM,QAAQ,YAAY,MAAM;AAAA,IAClC;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,MAAM;AACtD,UAAM,YAAY,QAAQ,eAAe;AACzC,UAAM,SAAS,IAAI,OAAO,EAAE,MAAM,wBAAwB,SAAS,KAAK,QAAQ,CAAC;AACjF,QAAI;AACF,YAAM,OAAO,QAAQ,eAAe;AAAA,IACtC,SAAS,KAAK;AACZ,YAAM,YAAY,MAAM;AACxB,YAAM;AAAA,IACR;AACA,WAAO,EAAE,QAAQ,aAAa,UAAU,KAAK,IAAI,EAAE;AAAA,EACrD;AAAA,EAEA,MAAc,WAA0B;AACtC,QAAI,KAAK,gBAAiB;AAC1B,SAAK,kBAAkB;AACvB,QAAI;AACF,YAAM,MAAM,KAAK,IAAI;AACrB,YAAM,UAAU,CAAC,GAAG,KAAK,UAAU,QAAQ,CAAC,EACzC,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,MAAM,EAAE,WAAW,KAAK,SAAS,EACnD,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE;AACnB,iBAAW,MAAM,SAAS;AACxB,cAAM,KAAK,QAAQ,EAAE;AAAA,MACvB;AAAA,IACF,UAAE;AACA,WAAK,kBAAkB;AAAA,IACzB;AAAA,EACF;AACF;",
|
|
5
5
|
"names": []
|
|
6
6
|
}
|
package/dist/tool-proxy.d.ts.map
CHANGED
|
@@ -1 +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;
|
|
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;AAI3D,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,CAoFf"}
|
package/dist/tool-proxy.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
CallToolRequestSchema
|
|
5
5
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
6
6
|
import {} from "./session-manager.js";
|
|
7
|
+
const BOOTSTRAP_SESSION_ID = "__bootstrap__";
|
|
7
8
|
function injectSessionId(tool) {
|
|
8
9
|
const existing = tool.inputSchema.required;
|
|
9
10
|
return {
|
|
@@ -11,20 +12,20 @@ function injectSessionId(tool) {
|
|
|
11
12
|
inputSchema: {
|
|
12
13
|
...tool.inputSchema,
|
|
13
14
|
properties: {
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
...tool.inputSchema.properties ?? {},
|
|
16
|
+
sessionId: { type: "string", description: "Session ID for browser isolation" }
|
|
16
17
|
},
|
|
17
18
|
required: ["sessionId", ...existing?.filter((r) => r !== "sessionId") ?? []]
|
|
18
19
|
}
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
22
|
async function registerProxiedTools(server, sessionManager) {
|
|
22
|
-
const bootstrapClient = await sessionManager.getOrCreate(
|
|
23
|
+
const bootstrapClient = await sessionManager.getOrCreate(BOOTSTRAP_SESSION_ID);
|
|
23
24
|
let playwrightTools;
|
|
24
25
|
try {
|
|
25
26
|
({ tools: playwrightTools } = await bootstrapClient.listTools());
|
|
26
27
|
} finally {
|
|
27
|
-
await sessionManager.destroy(
|
|
28
|
+
await sessionManager.destroy(BOOTSTRAP_SESSION_ID);
|
|
28
29
|
}
|
|
29
30
|
const proxiedTools = playwrightTools.map(injectSessionId);
|
|
30
31
|
const allTools = [
|
|
@@ -53,7 +54,10 @@ async function registerProxiedTools(server, sessionManager) {
|
|
|
53
54
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
54
55
|
const { name, arguments: args = {} } = request.params;
|
|
55
56
|
if (name === "session_close") {
|
|
56
|
-
const
|
|
57
|
+
const sessionId2 = args["sessionId"];
|
|
58
|
+
if (typeof sessionId2 !== "string" || sessionId2 === "") {
|
|
59
|
+
throw new Error("Missing required argument: sessionId");
|
|
60
|
+
}
|
|
57
61
|
await sessionManager.destroy(sessionId2);
|
|
58
62
|
return { content: [{ type: "text", text: `Session '${sessionId2}' closed.` }] };
|
|
59
63
|
}
|
|
@@ -72,8 +76,24 @@ async function registerProxiedTools(server, sessionManager) {
|
|
|
72
76
|
try {
|
|
73
77
|
return await client.callTool({ name, arguments: toolArgs });
|
|
74
78
|
} catch (err) {
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
80
|
+
try {
|
|
81
|
+
await client.listTools();
|
|
82
|
+
} catch {
|
|
83
|
+
await sessionManager.destroy(sessionId);
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: "text", text: `### Error
|
|
86
|
+
${message}
|
|
87
|
+
|
|
88
|
+
Session '${sessionId}' was destroyed due to a broken connection.` }],
|
|
89
|
+
isError: true
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
content: [{ type: "text", text: `### Error
|
|
94
|
+
${message}` }],
|
|
95
|
+
isError: true
|
|
96
|
+
};
|
|
77
97
|
}
|
|
78
98
|
});
|
|
79
99
|
}
|
package/dist/tool-proxy.js.map
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/tool-proxy.ts"],
|
|
4
|
-
"mappings": "AAAA,SAAS,cAAc;AACvB;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP,eAAoC;
|
|
4
|
+
"mappings": "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",
|
|
5
5
|
"names": ["sessionId"]
|
|
6
6
|
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -2,24 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
-
import {
|
|
5
|
+
import type { createConnection } from "@playwright/mcp";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
6
7
|
import { SessionManager } from "./session-manager.js";
|
|
7
8
|
import { registerProxiedTools } from "./tool-proxy.js";
|
|
8
9
|
|
|
10
|
+
const _require = createRequire(import.meta.url);
|
|
11
|
+
const { version } = _require("../package.json") as { version: string };
|
|
12
|
+
|
|
9
13
|
const config: NonNullable<Parameters<typeof createConnection>[0]> = {
|
|
10
14
|
browser: { isolated: true, launchOptions: { headless: true } },
|
|
11
15
|
outputDir: ".tmp/playwright",
|
|
12
16
|
};
|
|
13
17
|
|
|
14
18
|
const server = new Server(
|
|
15
|
-
{ name: "mcp-playwright", version
|
|
19
|
+
{ name: "mcp-playwright", version },
|
|
16
20
|
{
|
|
17
21
|
capabilities: { tools: {} },
|
|
18
22
|
instructions: "Multi-session Playwright MCP server. Each tool requires a 'sessionId' for browser isolation.\nOutput directory: .tmp/playwright",
|
|
19
23
|
},
|
|
20
24
|
);
|
|
21
25
|
|
|
22
|
-
const sessionManager = new SessionManager(config);
|
|
26
|
+
const sessionManager = new SessionManager(config, undefined, version);
|
|
23
27
|
|
|
24
28
|
await registerProxiedTools(server, sessionManager);
|
|
25
29
|
|
|
@@ -28,6 +32,7 @@ await server.connect(transport);
|
|
|
28
32
|
|
|
29
33
|
async function shutdown(): Promise<void> {
|
|
30
34
|
await sessionManager.disposeAll();
|
|
35
|
+
await server.close();
|
|
31
36
|
process.exit(0);
|
|
32
37
|
}
|
|
33
38
|
|
package/src/session-manager.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
2
2
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
-
import {
|
|
3
|
+
import { Server as McpServer } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import type { createConnection as CreateConnectionFn } from "@playwright/mcp";
|
|
6
|
+
|
|
7
|
+
// @playwright/mcp ships CJS only — use createRequire for ESM compatibility
|
|
8
|
+
const _require = createRequire(import.meta.url);
|
|
9
|
+
const { createConnection } = _require("@playwright/mcp") as { createConnection: typeof CreateConnectionFn };
|
|
4
10
|
|
|
5
11
|
interface Session {
|
|
6
12
|
client: Client;
|
|
13
|
+
innerServer: McpServer;
|
|
7
14
|
lastUsed: number;
|
|
8
15
|
}
|
|
9
16
|
|
|
@@ -11,10 +18,12 @@ export class SessionManager {
|
|
|
11
18
|
private readonly _sessions = new Map<string, Session>();
|
|
12
19
|
private readonly _pending = new Map<string, Promise<Session>>();
|
|
13
20
|
private readonly _cleanupInterval: ReturnType<typeof setInterval>;
|
|
21
|
+
private _cleanupRunning = false;
|
|
14
22
|
|
|
15
23
|
constructor(
|
|
16
|
-
private readonly config:
|
|
24
|
+
private readonly config: NonNullable<Parameters<typeof CreateConnectionFn>[0]>,
|
|
17
25
|
private readonly timeoutMs = 5 * 60 * 1000,
|
|
26
|
+
private readonly version = "0.0.0",
|
|
18
27
|
) {
|
|
19
28
|
this._cleanupInterval = setInterval(() => {
|
|
20
29
|
void this._cleanup();
|
|
@@ -45,6 +54,7 @@ export class SessionManager {
|
|
|
45
54
|
if (session != null) {
|
|
46
55
|
this._sessions.delete(sessionId);
|
|
47
56
|
await session.client.close();
|
|
57
|
+
await session.innerServer.close();
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
60
|
|
|
@@ -60,19 +70,31 @@ export class SessionManager {
|
|
|
60
70
|
|
|
61
71
|
private async _createSession(): Promise<Session> {
|
|
62
72
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
63
|
-
const innerServer = await createConnection(this.config
|
|
73
|
+
const innerServer = await createConnection(this.config);
|
|
64
74
|
await innerServer.connect(serverTransport);
|
|
65
|
-
const client = new Client({ name: "mcp-playwright-proxy", version:
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
const client = new Client({ name: "mcp-playwright-proxy", version: this.version });
|
|
76
|
+
try {
|
|
77
|
+
await client.connect(clientTransport);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
await innerServer.close();
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
return { client, innerServer, lastUsed: Date.now() };
|
|
68
83
|
}
|
|
69
84
|
|
|
70
85
|
private async _cleanup(): Promise<void> {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
86
|
+
if (this._cleanupRunning) return;
|
|
87
|
+
this._cleanupRunning = true;
|
|
88
|
+
try {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const expired = [...this._sessions.entries()]
|
|
91
|
+
.filter(([, s]) => now - s.lastUsed > this.timeoutMs)
|
|
92
|
+
.map(([id]) => id);
|
|
93
|
+
for (const id of expired) {
|
|
74
94
|
await this.destroy(id);
|
|
75
95
|
}
|
|
96
|
+
} finally {
|
|
97
|
+
this._cleanupRunning = false;
|
|
76
98
|
}
|
|
77
99
|
}
|
|
78
100
|
}
|
package/src/tool-proxy.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
7
|
import { type SessionManager } from "./session-manager.js";
|
|
8
8
|
|
|
9
|
+
const BOOTSTRAP_SESSION_ID = "__bootstrap__";
|
|
10
|
+
|
|
9
11
|
export function injectSessionId(tool: Tool): Tool {
|
|
10
12
|
const existing = tool.inputSchema.required as string[] | undefined;
|
|
11
13
|
return {
|
|
@@ -13,8 +15,8 @@ export function injectSessionId(tool: Tool): Tool {
|
|
|
13
15
|
inputSchema: {
|
|
14
16
|
...tool.inputSchema,
|
|
15
17
|
properties: {
|
|
16
|
-
sessionId: { type: "string", description: "Session ID for browser isolation" },
|
|
17
18
|
...(tool.inputSchema.properties ?? {}),
|
|
19
|
+
sessionId: { type: "string", description: "Session ID for browser isolation" },
|
|
18
20
|
},
|
|
19
21
|
required: ["sessionId", ...(existing?.filter((r) => r !== "sessionId") ?? [])],
|
|
20
22
|
},
|
|
@@ -26,12 +28,12 @@ export async function registerProxiedTools(
|
|
|
26
28
|
sessionManager: SessionManager,
|
|
27
29
|
): Promise<void> {
|
|
28
30
|
// Bootstrap: get tool list from @playwright/mcp (no browser launched)
|
|
29
|
-
const bootstrapClient = await sessionManager.getOrCreate(
|
|
31
|
+
const bootstrapClient = await sessionManager.getOrCreate(BOOTSTRAP_SESSION_ID);
|
|
30
32
|
let playwrightTools: Tool[];
|
|
31
33
|
try {
|
|
32
34
|
({ tools: playwrightTools } = await bootstrapClient.listTools());
|
|
33
35
|
} finally {
|
|
34
|
-
await sessionManager.destroy(
|
|
36
|
+
await sessionManager.destroy(BOOTSTRAP_SESSION_ID);
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
const proxiedTools = playwrightTools.map(injectSessionId);
|
|
@@ -64,7 +66,10 @@ export async function registerProxiedTools(
|
|
|
64
66
|
const { name, arguments: args = {} } = request.params;
|
|
65
67
|
|
|
66
68
|
if (name === "session_close") {
|
|
67
|
-
const
|
|
69
|
+
const sessionId = args["sessionId"];
|
|
70
|
+
if (typeof sessionId !== "string" || sessionId === "") {
|
|
71
|
+
throw new Error("Missing required argument: sessionId");
|
|
72
|
+
}
|
|
68
73
|
await sessionManager.destroy(sessionId);
|
|
69
74
|
return { content: [{ type: "text" as const, text: `Session '${sessionId}' closed.` }] };
|
|
70
75
|
}
|
|
@@ -85,8 +90,24 @@ export async function registerProxiedTools(
|
|
|
85
90
|
try {
|
|
86
91
|
return await client.callTool({ name, arguments: toolArgs });
|
|
87
92
|
} catch (err) {
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
94
|
+
|
|
95
|
+
// Check if the underlying connection is still alive
|
|
96
|
+
try {
|
|
97
|
+
await client.listTools();
|
|
98
|
+
} catch {
|
|
99
|
+
// Connection is broken — destroy the session
|
|
100
|
+
await sessionManager.destroy(sessionId);
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: "text" as const, text: `### Error\n${message}\n\nSession '${sessionId}' was destroyed due to a broken connection.` }],
|
|
103
|
+
isError: true,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
content: [{ type: "text" as const, text: `### Error\n${message}` }],
|
|
109
|
+
isError: true,
|
|
110
|
+
};
|
|
90
111
|
}
|
|
91
112
|
});
|
|
92
113
|
}
|