@playwright/mcp 0.0.34 → 0.0.35
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 +29 -38
- package/lib/browserContextFactory.js +10 -12
- package/lib/browserServerBackend.js +3 -6
- package/lib/context.js +5 -5
- package/lib/extension/cdpRelay.js +12 -6
- package/lib/extension/extensionContextFactory.js +5 -7
- package/lib/index.js +2 -1
- package/lib/loopTools/context.js +3 -2
- package/lib/loopTools/main.js +8 -5
- package/lib/mcp/{transport.js → http.js} +51 -37
- package/lib/mcp/mdb.js +198 -0
- package/lib/mcp/proxyBackend.js +5 -7
- package/lib/mcp/server.js +39 -12
- package/lib/mcp/tool.js +3 -0
- package/lib/program.js +44 -23
- package/lib/tab.js +1 -1
- package/lib/tools/form.js +57 -0
- package/lib/tools/navigate.js +0 -16
- package/lib/tools/tabs.js +31 -59
- package/lib/tools.js +2 -0
- package/lib/vscode/host.js +128 -0
- package/lib/vscode/main.js +62 -0
- package/package.json +1 -1
- package/lib/utils/httpServer.js +0 -39
- /package/lib/{utils → mcp}/manualPromise.js +0 -0
package/README.md
CHANGED
|
@@ -56,6 +56,21 @@ Follow the MCP install [guide](https://modelcontextprotocol.io/quickstart/user),
|
|
|
56
56
|
|
|
57
57
|
</details>
|
|
58
58
|
|
|
59
|
+
<details>
|
|
60
|
+
<summary>Codex</summary>
|
|
61
|
+
|
|
62
|
+
Create or edit the configuration file `~/.codex/config.toml` and add:
|
|
63
|
+
|
|
64
|
+
```toml
|
|
65
|
+
[mcp_servers.playwright]
|
|
66
|
+
command = "npx"
|
|
67
|
+
args = ["@playwright/mcp@latest"]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For more information, see the [Codex MCP documentation](https://github.com/openai/codex/blob/main/codex-rs/config.md#mcp_servers).
|
|
71
|
+
|
|
72
|
+
</details>
|
|
73
|
+
|
|
59
74
|
<details>
|
|
60
75
|
<summary>Cursor</summary>
|
|
61
76
|
|
|
@@ -479,6 +494,15 @@ http.createServer(async (req, res) => {
|
|
|
479
494
|
|
|
480
495
|
<!-- NOTE: This has been generated via update-readme.js -->
|
|
481
496
|
|
|
497
|
+
- **browser_fill_form**
|
|
498
|
+
- Title: Fill form
|
|
499
|
+
- Description: Fill multiple form fields
|
|
500
|
+
- Parameters:
|
|
501
|
+
- `fields` (array): Fields to fill in
|
|
502
|
+
- Read-only: **false**
|
|
503
|
+
|
|
504
|
+
<!-- NOTE: This has been generated via update-readme.js -->
|
|
505
|
+
|
|
482
506
|
- **browser_handle_dialog**
|
|
483
507
|
- Title: Handle a dialog
|
|
484
508
|
- Description: Handle a dialog
|
|
@@ -516,14 +540,6 @@ http.createServer(async (req, res) => {
|
|
|
516
540
|
|
|
517
541
|
<!-- NOTE: This has been generated via update-readme.js -->
|
|
518
542
|
|
|
519
|
-
- **browser_navigate_forward**
|
|
520
|
-
- Title: Go forward
|
|
521
|
-
- Description: Go forward to the next page
|
|
522
|
-
- Parameters: None
|
|
523
|
-
- Read-only: **true**
|
|
524
|
-
|
|
525
|
-
<!-- NOTE: This has been generated via update-readme.js -->
|
|
526
|
-
|
|
527
543
|
- **browser_network_requests**
|
|
528
544
|
- Title: List network requests
|
|
529
545
|
- Description: Returns all network requests since loading the page
|
|
@@ -612,39 +628,14 @@ http.createServer(async (req, res) => {
|
|
|
612
628
|
|
|
613
629
|
<!-- NOTE: This has been generated via update-readme.js -->
|
|
614
630
|
|
|
615
|
-
- **
|
|
616
|
-
- Title:
|
|
617
|
-
- Description:
|
|
631
|
+
- **browser_tabs**
|
|
632
|
+
- Title: Manage tabs
|
|
633
|
+
- Description: List, create, close, or select a browser tab.
|
|
618
634
|
- Parameters:
|
|
619
|
-
- `
|
|
635
|
+
- `action` (string): Operation to perform
|
|
636
|
+
- `index` (number, optional): Tab index, used for close/select. If omitted for close, current tab is closed.
|
|
620
637
|
- Read-only: **false**
|
|
621
638
|
|
|
622
|
-
<!-- NOTE: This has been generated via update-readme.js -->
|
|
623
|
-
|
|
624
|
-
- **browser_tab_list**
|
|
625
|
-
- Title: List tabs
|
|
626
|
-
- Description: List browser tabs
|
|
627
|
-
- Parameters: None
|
|
628
|
-
- Read-only: **true**
|
|
629
|
-
|
|
630
|
-
<!-- NOTE: This has been generated via update-readme.js -->
|
|
631
|
-
|
|
632
|
-
- **browser_tab_new**
|
|
633
|
-
- Title: Open a new tab
|
|
634
|
-
- Description: Open a new tab
|
|
635
|
-
- Parameters:
|
|
636
|
-
- `url` (string, optional): The URL to navigate to in the new tab. If not provided, the new tab will be blank.
|
|
637
|
-
- Read-only: **true**
|
|
638
|
-
|
|
639
|
-
<!-- NOTE: This has been generated via update-readme.js -->
|
|
640
|
-
|
|
641
|
-
- **browser_tab_select**
|
|
642
|
-
- Title: Select a tab
|
|
643
|
-
- Description: Select a tab by index
|
|
644
|
-
- Parameters:
|
|
645
|
-
- `index` (number): The index of the tab to select
|
|
646
|
-
- Read-only: **true**
|
|
647
|
-
|
|
648
639
|
</details>
|
|
649
640
|
|
|
650
641
|
<details>
|
|
@@ -34,19 +34,17 @@ export function contextFactory(config) {
|
|
|
34
34
|
return new PersistentContextFactory(config);
|
|
35
35
|
}
|
|
36
36
|
class BaseContextFactory {
|
|
37
|
-
name;
|
|
38
|
-
description;
|
|
39
37
|
config;
|
|
38
|
+
_logName;
|
|
40
39
|
_browserPromise;
|
|
41
|
-
constructor(name,
|
|
42
|
-
this.
|
|
43
|
-
this.description = description;
|
|
40
|
+
constructor(name, config) {
|
|
41
|
+
this._logName = name;
|
|
44
42
|
this.config = config;
|
|
45
43
|
}
|
|
46
44
|
async _obtainBrowser(clientInfo) {
|
|
47
45
|
if (this._browserPromise)
|
|
48
46
|
return this._browserPromise;
|
|
49
|
-
testDebug(`obtain browser (${this.
|
|
47
|
+
testDebug(`obtain browser (${this._logName})`);
|
|
50
48
|
this._browserPromise = this._doObtainBrowser(clientInfo);
|
|
51
49
|
void this._browserPromise.then(browser => {
|
|
52
50
|
browser.on('disconnected', () => {
|
|
@@ -61,7 +59,7 @@ class BaseContextFactory {
|
|
|
61
59
|
throw new Error('Not implemented');
|
|
62
60
|
}
|
|
63
61
|
async createContext(clientInfo) {
|
|
64
|
-
testDebug(`create browser context (${this.
|
|
62
|
+
testDebug(`create browser context (${this._logName})`);
|
|
65
63
|
const browser = await this._obtainBrowser(clientInfo);
|
|
66
64
|
const browserContext = await this._doCreateContext(browser);
|
|
67
65
|
return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) };
|
|
@@ -70,19 +68,19 @@ class BaseContextFactory {
|
|
|
70
68
|
throw new Error('Not implemented');
|
|
71
69
|
}
|
|
72
70
|
async _closeBrowserContext(browserContext, browser) {
|
|
73
|
-
testDebug(`close browser context (${this.
|
|
71
|
+
testDebug(`close browser context (${this._logName})`);
|
|
74
72
|
if (browser.contexts().length === 1)
|
|
75
73
|
this._browserPromise = undefined;
|
|
76
74
|
await browserContext.close().catch(logUnhandledError);
|
|
77
75
|
if (browser.contexts().length === 0) {
|
|
78
|
-
testDebug(`close browser (${this.
|
|
76
|
+
testDebug(`close browser (${this._logName})`);
|
|
79
77
|
await browser.close().catch(logUnhandledError);
|
|
80
78
|
}
|
|
81
79
|
}
|
|
82
80
|
}
|
|
83
81
|
class IsolatedContextFactory extends BaseContextFactory {
|
|
84
82
|
constructor(config) {
|
|
85
|
-
super('isolated',
|
|
83
|
+
super('isolated', config);
|
|
86
84
|
}
|
|
87
85
|
async _doObtainBrowser(clientInfo) {
|
|
88
86
|
await injectCdpPort(this.config.browser);
|
|
@@ -104,7 +102,7 @@ class IsolatedContextFactory extends BaseContextFactory {
|
|
|
104
102
|
}
|
|
105
103
|
class CdpContextFactory extends BaseContextFactory {
|
|
106
104
|
constructor(config) {
|
|
107
|
-
super('cdp',
|
|
105
|
+
super('cdp', config);
|
|
108
106
|
}
|
|
109
107
|
async _doObtainBrowser() {
|
|
110
108
|
return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint);
|
|
@@ -115,7 +113,7 @@ class CdpContextFactory extends BaseContextFactory {
|
|
|
115
113
|
}
|
|
116
114
|
class RemoteContextFactory extends BaseContextFactory {
|
|
117
115
|
constructor(config) {
|
|
118
|
-
super('remote',
|
|
116
|
+
super('remote', config);
|
|
119
117
|
}
|
|
120
118
|
async _doObtainBrowser() {
|
|
121
119
|
const url = new URL(this.config.browser.remoteEndpoint);
|
|
@@ -19,11 +19,8 @@ import { logUnhandledError } from './utils/log.js';
|
|
|
19
19
|
import { Response } from './response.js';
|
|
20
20
|
import { SessionLog } from './sessionLog.js';
|
|
21
21
|
import { filteredTools } from './tools.js';
|
|
22
|
-
import { packageJSON } from './utils/package.js';
|
|
23
22
|
import { toMcpTool } from './mcp/tool.js';
|
|
24
23
|
export class BrowserServerBackend {
|
|
25
|
-
name = 'Playwright';
|
|
26
|
-
version = packageJSON.version;
|
|
27
24
|
_tools;
|
|
28
25
|
_context;
|
|
29
26
|
_sessionLog;
|
|
@@ -34,7 +31,7 @@ export class BrowserServerBackend {
|
|
|
34
31
|
this._browserContextFactory = factory;
|
|
35
32
|
this._tools = filteredTools(config);
|
|
36
33
|
}
|
|
37
|
-
async initialize(clientVersion, roots) {
|
|
34
|
+
async initialize(server, clientVersion, roots) {
|
|
38
35
|
let rootPath;
|
|
39
36
|
if (roots.length > 0) {
|
|
40
37
|
const firstRootUri = roots[0]?.uri;
|
|
@@ -60,7 +57,7 @@ export class BrowserServerBackend {
|
|
|
60
57
|
const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {});
|
|
61
58
|
const context = this._context;
|
|
62
59
|
const response = new Response(context, name, parsedArguments);
|
|
63
|
-
context.setRunningTool(
|
|
60
|
+
context.setRunningTool(name);
|
|
64
61
|
try {
|
|
65
62
|
await tool.handle(context, parsedArguments, response);
|
|
66
63
|
await response.finish();
|
|
@@ -70,7 +67,7 @@ export class BrowserServerBackend {
|
|
|
70
67
|
response.addError(String(error));
|
|
71
68
|
}
|
|
72
69
|
finally {
|
|
73
|
-
context.setRunningTool(
|
|
70
|
+
context.setRunningTool(undefined);
|
|
74
71
|
}
|
|
75
72
|
return response.serialize();
|
|
76
73
|
}
|
package/lib/context.js
CHANGED
|
@@ -30,7 +30,7 @@ export class Context {
|
|
|
30
30
|
_clientInfo;
|
|
31
31
|
static _allContexts = new Set();
|
|
32
32
|
_closeBrowserContextPromise;
|
|
33
|
-
|
|
33
|
+
_runningToolName;
|
|
34
34
|
_abortController = new AbortController();
|
|
35
35
|
constructor(options) {
|
|
36
36
|
this.tools = options.tools;
|
|
@@ -110,10 +110,10 @@ export class Context {
|
|
|
110
110
|
this._closeBrowserContextPromise = undefined;
|
|
111
111
|
}
|
|
112
112
|
isRunningTool() {
|
|
113
|
-
return this.
|
|
113
|
+
return this._runningToolName !== undefined;
|
|
114
114
|
}
|
|
115
|
-
setRunningTool(
|
|
116
|
-
this.
|
|
115
|
+
setRunningTool(name) {
|
|
116
|
+
this._runningToolName = name;
|
|
117
117
|
}
|
|
118
118
|
async _closeBrowserContextImpl() {
|
|
119
119
|
if (!this._browserContextPromise)
|
|
@@ -156,7 +156,7 @@ export class Context {
|
|
|
156
156
|
if (this._closeBrowserContextPromise)
|
|
157
157
|
throw new Error('Another browser context is being closed.');
|
|
158
158
|
// TODO: move to the browser context factory to make it based on isolation mode.
|
|
159
|
-
const result = await this._browserContextFactory.createContext(this._clientInfo, this._abortController.signal);
|
|
159
|
+
const result = await this._browserContextFactory.createContext(this._clientInfo, this._abortController.signal, this._runningToolName);
|
|
160
160
|
const { browserContext } = result;
|
|
161
161
|
await this._setupRequestInterception(browserContext);
|
|
162
162
|
if (this.sessionLog)
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
import { spawn } from 'child_process';
|
|
24
24
|
import debug from 'debug';
|
|
25
25
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
26
|
-
import { httpAddressToString } from '../
|
|
26
|
+
import { httpAddressToString } from '../mcp/http.js';
|
|
27
27
|
import { logUnhandledError } from '../utils/log.js';
|
|
28
|
-
import { ManualPromise } from '../
|
|
28
|
+
import { ManualPromise } from '../mcp/manualPromise.js';
|
|
29
29
|
// @ts-ignore
|
|
30
30
|
const { registry } = await import('playwright-core/lib/server/registry/index');
|
|
31
31
|
const debugLogger = debug('pw:mcp:relay');
|
|
@@ -58,11 +58,11 @@ export class CDPRelayServer {
|
|
|
58
58
|
extensionEndpoint() {
|
|
59
59
|
return `${this._wsHost}${this._extensionPath}`;
|
|
60
60
|
}
|
|
61
|
-
async ensureExtensionConnectionForMCPContext(clientInfo, abortSignal) {
|
|
61
|
+
async ensureExtensionConnectionForMCPContext(clientInfo, abortSignal, toolName) {
|
|
62
62
|
debugLogger('Ensuring extension connection for MCP context');
|
|
63
63
|
if (this._extensionConnection)
|
|
64
64
|
return;
|
|
65
|
-
this._connectBrowser(clientInfo);
|
|
65
|
+
this._connectBrowser(clientInfo, toolName);
|
|
66
66
|
debugLogger('Waiting for incoming extension connection');
|
|
67
67
|
await Promise.race([
|
|
68
68
|
this._extensionConnectionPromise,
|
|
@@ -73,12 +73,18 @@ export class CDPRelayServer {
|
|
|
73
73
|
]);
|
|
74
74
|
debugLogger('Extension connection established');
|
|
75
75
|
}
|
|
76
|
-
_connectBrowser(clientInfo) {
|
|
76
|
+
_connectBrowser(clientInfo, toolName) {
|
|
77
77
|
const mcpRelayEndpoint = `${this._wsHost}${this._extensionPath}`;
|
|
78
78
|
// Need to specify "key" in the manifest.json to make the id stable when loading from file.
|
|
79
79
|
const url = new URL('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html');
|
|
80
80
|
url.searchParams.set('mcpRelayUrl', mcpRelayEndpoint);
|
|
81
|
-
|
|
81
|
+
const client = {
|
|
82
|
+
name: clientInfo.name,
|
|
83
|
+
version: clientInfo.version,
|
|
84
|
+
};
|
|
85
|
+
url.searchParams.set('client', JSON.stringify(client));
|
|
86
|
+
if (toolName)
|
|
87
|
+
url.searchParams.set('newTab', String(toolName === 'browser_navigate'));
|
|
82
88
|
const href = url.toString();
|
|
83
89
|
const executableInfo = registry.findExecutable(this._browserChannel);
|
|
84
90
|
if (!executableInfo)
|
|
@@ -15,20 +15,18 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import debug from 'debug';
|
|
17
17
|
import * as playwright from 'playwright';
|
|
18
|
-
import { startHttpServer } from '../
|
|
18
|
+
import { startHttpServer } from '../mcp/http.js';
|
|
19
19
|
import { CDPRelayServer } from './cdpRelay.js';
|
|
20
20
|
const debugLogger = debug('pw:mcp:relay');
|
|
21
21
|
export class ExtensionContextFactory {
|
|
22
|
-
name = 'extension';
|
|
23
|
-
description = 'Connect to a browser using the Playwright MCP extension';
|
|
24
22
|
_browserChannel;
|
|
25
23
|
_userDataDir;
|
|
26
24
|
constructor(browserChannel, userDataDir) {
|
|
27
25
|
this._browserChannel = browserChannel;
|
|
28
26
|
this._userDataDir = userDataDir;
|
|
29
27
|
}
|
|
30
|
-
async createContext(clientInfo, abortSignal) {
|
|
31
|
-
const browser = await this._obtainBrowser(clientInfo, abortSignal);
|
|
28
|
+
async createContext(clientInfo, abortSignal, toolName) {
|
|
29
|
+
const browser = await this._obtainBrowser(clientInfo, abortSignal, toolName);
|
|
32
30
|
return {
|
|
33
31
|
browserContext: browser.contexts()[0],
|
|
34
32
|
close: async () => {
|
|
@@ -37,9 +35,9 @@ export class ExtensionContextFactory {
|
|
|
37
35
|
}
|
|
38
36
|
};
|
|
39
37
|
}
|
|
40
|
-
async _obtainBrowser(clientInfo, abortSignal) {
|
|
38
|
+
async _obtainBrowser(clientInfo, abortSignal, toolName) {
|
|
41
39
|
const relay = await this._startRelay(abortSignal);
|
|
42
|
-
await relay.ensureExtensionConnectionForMCPContext(clientInfo, abortSignal);
|
|
40
|
+
await relay.ensureExtensionConnectionForMCPContext(clientInfo, abortSignal, toolName);
|
|
43
41
|
return await playwright.chromium.connectOverCDP(relay.cdpEndpoint());
|
|
44
42
|
}
|
|
45
43
|
async _startRelay(abortSignal) {
|
package/lib/index.js
CHANGED
|
@@ -17,10 +17,11 @@ import { BrowserServerBackend } from './browserServerBackend.js';
|
|
|
17
17
|
import { resolveConfig } from './config.js';
|
|
18
18
|
import { contextFactory } from './browserContextFactory.js';
|
|
19
19
|
import * as mcpServer from './mcp/server.js';
|
|
20
|
+
import { packageJSON } from './utils/package.js';
|
|
20
21
|
export async function createConnection(userConfig = {}, contextGetter) {
|
|
21
22
|
const config = await resolveConfig(userConfig);
|
|
22
23
|
const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config);
|
|
23
|
-
return mcpServer.createServer(new BrowserServerBackend(config, factory), false);
|
|
24
|
+
return mcpServer.createServer('Playwright', packageJSON.version, new BrowserServerBackend(config, factory), false);
|
|
24
25
|
}
|
|
25
26
|
class SimpleBrowserContextFactory {
|
|
26
27
|
name = 'custom';
|
package/lib/loopTools/context.js
CHANGED
|
@@ -22,6 +22,7 @@ import { OpenAIDelegate } from '../loop/loopOpenAI.js';
|
|
|
22
22
|
import { ClaudeDelegate } from '../loop/loopClaude.js';
|
|
23
23
|
import { InProcessTransport } from '../mcp/inProcessTransport.js';
|
|
24
24
|
import * as mcpServer from '../mcp/server.js';
|
|
25
|
+
import { packageJSON } from '../utils/package.js';
|
|
25
26
|
export class Context {
|
|
26
27
|
config;
|
|
27
28
|
_client;
|
|
@@ -37,9 +38,9 @@ export class Context {
|
|
|
37
38
|
throw new Error('No LLM API key found. Please set OPENAI_API_KEY or ANTHROPIC_API_KEY environment variable.');
|
|
38
39
|
}
|
|
39
40
|
static async create(config) {
|
|
40
|
-
const client = new Client({ name: 'Playwright Proxy', version:
|
|
41
|
+
const client = new Client({ name: 'Playwright Proxy', version: packageJSON.version });
|
|
41
42
|
const browserContextFactory = contextFactory(config);
|
|
42
|
-
const server = mcpServer.createServer(new BrowserServerBackend(config, browserContextFactory), false);
|
|
43
|
+
const server = mcpServer.createServer('Playwright Subagent', packageJSON.version, new BrowserServerBackend(config, browserContextFactory), false);
|
|
43
44
|
await client.connect(new InProcessTransport(server));
|
|
44
45
|
await client.ping();
|
|
45
46
|
return new Context(config, client);
|
package/lib/loopTools/main.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import dotenv from 'dotenv';
|
|
17
|
-
import * as
|
|
17
|
+
import * as mcpServer from '../mcp/server.js';
|
|
18
18
|
import { packageJSON } from '../utils/package.js';
|
|
19
19
|
import { Context } from './context.js';
|
|
20
20
|
import { perform } from './perform.js';
|
|
@@ -22,12 +22,15 @@ import { snapshot } from './snapshot.js';
|
|
|
22
22
|
import { toMcpTool } from '../mcp/tool.js';
|
|
23
23
|
export async function runLoopTools(config) {
|
|
24
24
|
dotenv.config();
|
|
25
|
-
const serverBackendFactory =
|
|
26
|
-
|
|
25
|
+
const serverBackendFactory = {
|
|
26
|
+
name: 'Playwright',
|
|
27
|
+
nameInConfig: 'playwright-loop',
|
|
28
|
+
version: packageJSON.version,
|
|
29
|
+
create: () => new LoopToolsServerBackend(config)
|
|
30
|
+
};
|
|
31
|
+
await mcpServer.start(serverBackendFactory, config.server);
|
|
27
32
|
}
|
|
28
33
|
class LoopToolsServerBackend {
|
|
29
|
-
name = 'Playwright';
|
|
30
|
-
version = packageJSON.version;
|
|
31
34
|
_config;
|
|
32
35
|
_context;
|
|
33
36
|
_tools = [perform, snapshot];
|
|
@@ -13,26 +13,52 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
import assert from 'assert';
|
|
17
|
+
import http from 'http';
|
|
16
18
|
import crypto from 'crypto';
|
|
17
19
|
import debug from 'debug';
|
|
18
20
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
19
21
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
20
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
21
|
-
import { httpAddressToString, startHttpServer } from '../utils/httpServer.js';
|
|
22
22
|
import * as mcpServer from './server.js';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
const testDebug = debug('pw:mcp:test');
|
|
24
|
+
export async function startHttpServer(config, abortSignal) {
|
|
25
|
+
const { host, port } = config;
|
|
26
|
+
const httpServer = http.createServer();
|
|
27
|
+
decorateServer(httpServer);
|
|
28
|
+
await new Promise((resolve, reject) => {
|
|
29
|
+
httpServer.on('error', reject);
|
|
30
|
+
abortSignal?.addEventListener('abort', () => {
|
|
31
|
+
httpServer.close();
|
|
32
|
+
reject(new Error('Aborted'));
|
|
33
|
+
});
|
|
34
|
+
httpServer.listen(port, host, () => {
|
|
35
|
+
resolve();
|
|
36
|
+
httpServer.removeListener('error', reject);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
return httpServer;
|
|
31
40
|
}
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
export function httpAddressToString(address) {
|
|
42
|
+
assert(address, 'Could not bind server socket');
|
|
43
|
+
if (typeof address === 'string')
|
|
44
|
+
return address;
|
|
45
|
+
const resolvedPort = address.port;
|
|
46
|
+
let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
|
|
47
|
+
if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]')
|
|
48
|
+
resolvedHost = 'localhost';
|
|
49
|
+
return `http://${resolvedHost}:${resolvedPort}`;
|
|
50
|
+
}
|
|
51
|
+
export async function installHttpTransport(httpServer, serverBackendFactory) {
|
|
52
|
+
const sseSessions = new Map();
|
|
53
|
+
const streamableSessions = new Map();
|
|
54
|
+
httpServer.on('request', async (req, res) => {
|
|
55
|
+
const url = new URL(`http://localhost${req.url}`);
|
|
56
|
+
if (url.pathname.startsWith('/sse'))
|
|
57
|
+
await handleSSE(serverBackendFactory, req, res, url, sseSessions);
|
|
58
|
+
else
|
|
59
|
+
await handleStreamable(serverBackendFactory, req, res, streamableSessions);
|
|
60
|
+
});
|
|
34
61
|
}
|
|
35
|
-
const testDebug = debug('pw:mcp:test');
|
|
36
62
|
async function handleSSE(serverBackendFactory, req, res, url, sessions) {
|
|
37
63
|
if (req.method === 'POST') {
|
|
38
64
|
const sessionId = url.searchParams.get('sessionId');
|
|
@@ -93,29 +119,17 @@ async function handleStreamable(serverBackendFactory, req, res, sessions) {
|
|
|
93
119
|
res.statusCode = 400;
|
|
94
120
|
res.end('Invalid request');
|
|
95
121
|
}
|
|
96
|
-
function
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (url.pathname.startsWith('/sse'))
|
|
102
|
-
await handleSSE(serverBackendFactory, req, res, url, sseSessions);
|
|
103
|
-
else
|
|
104
|
-
await handleStreamable(serverBackendFactory, req, res, streamableSessions);
|
|
122
|
+
function decorateServer(server) {
|
|
123
|
+
const sockets = new Set();
|
|
124
|
+
server.on('connection', socket => {
|
|
125
|
+
sockets.add(socket);
|
|
126
|
+
socket.once('close', () => sockets.delete(socket));
|
|
105
127
|
});
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
'url': `${url}/mcp`
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}, undefined, 2),
|
|
117
|
-
'For legacy SSE transport support, you can use the /sse endpoint instead.',
|
|
118
|
-
].join('\n');
|
|
119
|
-
// eslint-disable-next-line no-console
|
|
120
|
-
console.error(message);
|
|
128
|
+
const close = server.close;
|
|
129
|
+
server.close = (callback) => {
|
|
130
|
+
for (const socket of sockets)
|
|
131
|
+
socket.destroy();
|
|
132
|
+
sockets.clear();
|
|
133
|
+
return close.call(server, callback);
|
|
134
|
+
};
|
|
121
135
|
}
|