Skip to content

Commit a2d7712

Browse files
pavelfeldmanEdward-Upton
authored andcommitted
chore: do not wrap mcp in proxy by default, drive-by deps fix (microsoft#909)
1 parent 51aa6ef commit a2d7712

File tree

14 files changed

+158
-164
lines changed

14 files changed

+158
-164
lines changed

src/browserContextFactory.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,27 +42,23 @@ export function contextFactory(config: FullConfig): BrowserContextFactory {
4242
export type ClientInfo = { name?: string, version?: string, rootPath?: string };
4343

4444
export interface BrowserContextFactory {
45-
readonly name: string;
46-
readonly description: string;
4745
createContext(clientInfo: ClientInfo, abortSignal: AbortSignal): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }>;
4846
}
4947

5048
class BaseContextFactory implements BrowserContextFactory {
51-
readonly name: string;
52-
readonly description: string;
5349
readonly config: FullConfig;
50+
private _logName: string;
5451
protected _browserPromise: Promise<playwright.Browser> | undefined;
5552

56-
constructor(name: string, description: string, config: FullConfig) {
57-
this.name = name;
58-
this.description = description;
53+
constructor(name: string, config: FullConfig) {
54+
this._logName = name;
5955
this.config = config;
6056
}
6157

6258
protected async _obtainBrowser(clientInfo: ClientInfo): Promise<playwright.Browser> {
6359
if (this._browserPromise)
6460
return this._browserPromise;
65-
testDebug(`obtain browser (${this.name})`);
61+
testDebug(`obtain browser (${this._logName})`);
6662
this._browserPromise = this._doObtainBrowser(clientInfo);
6763
void this._browserPromise.then(browser => {
6864
browser.on('disconnected', () => {
@@ -79,7 +75,7 @@ class BaseContextFactory implements BrowserContextFactory {
7975
}
8076

8177
async createContext(clientInfo: ClientInfo): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> {
82-
testDebug(`create browser context (${this.name})`);
78+
testDebug(`create browser context (${this._logName})`);
8379
const browser = await this._obtainBrowser(clientInfo);
8480
const browserContext = await this._doCreateContext(browser);
8581
return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) };
@@ -90,20 +86,20 @@ class BaseContextFactory implements BrowserContextFactory {
9086
}
9187

9288
private async _closeBrowserContext(browserContext: playwright.BrowserContext, browser: playwright.Browser) {
93-
testDebug(`close browser context (${this.name})`);
89+
testDebug(`close browser context (${this._logName})`);
9490
if (browser.contexts().length === 1)
9591
this._browserPromise = undefined;
9692
await browserContext.close().catch(logUnhandledError);
9793
if (browser.contexts().length === 0) {
98-
testDebug(`close browser (${this.name})`);
94+
testDebug(`close browser (${this._logName})`);
9995
await browser.close().catch(logUnhandledError);
10096
}
10197
}
10298
}
10399

104100
class IsolatedContextFactory extends BaseContextFactory {
105101
constructor(config: FullConfig) {
106-
super('isolated', 'Create a new isolated browser context', config);
102+
super('isolated', config);
107103
}
108104

109105
protected override async _doObtainBrowser(clientInfo: ClientInfo): Promise<playwright.Browser> {
@@ -128,7 +124,7 @@ class IsolatedContextFactory extends BaseContextFactory {
128124

129125
class CdpContextFactory extends BaseContextFactory {
130126
constructor(config: FullConfig) {
131-
super('cdp', 'Connect to a browser over CDP', config);
127+
super('cdp', config);
132128
}
133129

134130
protected override async _doObtainBrowser(): Promise<playwright.Browser> {
@@ -142,7 +138,7 @@ class CdpContextFactory extends BaseContextFactory {
142138

143139
class RemoteContextFactory extends BaseContextFactory {
144140
constructor(config: FullConfig) {
145-
super('remote', 'Connect to a browser using a remote endpoint', config);
141+
super('remote', config);
146142
}
147143

148144
protected override async _doObtainBrowser(): Promise<playwright.Browser> {

src/browserServerBackend.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import { logUnhandledError } from './utils/log.js';
2121
import { Response } from './response.js';
2222
import { SessionLog } from './sessionLog.js';
2323
import { filteredTools } from './tools.js';
24-
import { packageJSON } from './utils/package.js';
2524
import { toMcpTool } from './mcp/tool.js';
2625

2726
import type { Tool } from './tools/tool.js';
@@ -30,9 +29,6 @@ import type * as mcpServer from './mcp/server.js';
3029
import type { ServerBackend } from './mcp/server.js';
3130

3231
export class BrowserServerBackend implements ServerBackend {
33-
name = 'Playwright';
34-
version = packageJSON.version;
35-
3632
private _tools: Tool[];
3733
private _context: Context | undefined;
3834
private _sessionLog: SessionLog | undefined;

src/extension/cdpRelay.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { spawn } from 'child_process';
2626
import http from 'http';
2727
import debug from 'debug';
2828
import { WebSocket, WebSocketServer } from 'ws';
29-
import { httpAddressToString } from '../utils/httpServer.js';
29+
import { httpAddressToString } from '../mcp/http.js';
3030
import { logUnhandledError } from '../utils/log.js';
3131
import { ManualPromise } from '../utils/manualPromise.js';
3232
import type websocket from 'ws';

src/extension/extensionContextFactory.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,14 @@
1616

1717
import debug from 'debug';
1818
import * as playwright from 'playwright';
19-
import { startHttpServer } from '../utils/httpServer.js';
19+
import { startHttpServer } from '../mcp/http.js';
2020
import { CDPRelayServer } from './cdpRelay.js';
2121

2222
import type { BrowserContextFactory, ClientInfo } from '../browserContextFactory.js';
2323

2424
const debugLogger = debug('pw:mcp:relay');
2525

2626
export class ExtensionContextFactory implements BrowserContextFactory {
27-
name = 'extension';
28-
description = 'Connect to a browser using the Playwright MCP extension';
29-
3027
private _browserChannel: string;
3128
private _userDataDir?: string;
3229

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { BrowserServerBackend } from './browserServerBackend.js';
1818
import { resolveConfig } from './config.js';
1919
import { contextFactory } from './browserContextFactory.js';
2020
import * as mcpServer from './mcp/server.js';
21+
import { packageJSON } from './utils/package.js';
2122

2223
import type { Config } from '../config.js';
2324
import type { BrowserContext } from 'playwright';
@@ -27,7 +28,7 @@ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
2728
export async function createConnection(userConfig: Config = {}, contextGetter?: () => Promise<BrowserContext>): Promise<Server> {
2829
const config = await resolveConfig(userConfig);
2930
const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config);
30-
return mcpServer.createServer(new BrowserServerBackend(config, factory), false);
31+
return mcpServer.createServer('Playwright', packageJSON.version, new BrowserServerBackend(config, factory), false);
3132
}
3233

3334
class SimpleBrowserContextFactory implements BrowserContextFactory {

src/loopTools/context.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { OpenAIDelegate } from '../loop/loopOpenAI.js';
2323
import { ClaudeDelegate } from '../loop/loopClaude.js';
2424
import { InProcessTransport } from '../mcp/inProcessTransport.js';
2525
import * as mcpServer from '../mcp/server.js';
26+
import { packageJSON } from '../utils/package.js';
2627

2728
import type { LLMDelegate } from '../loop/loop.js';
2829
import type { FullConfig } from '../config.js';
@@ -44,9 +45,9 @@ export class Context {
4445
}
4546

4647
static async create(config: FullConfig) {
47-
const client = new Client({ name: 'Playwright Proxy', version: '1.0.0' });
48+
const client = new Client({ name: 'Playwright Proxy', version: packageJSON.version });
4849
const browserContextFactory = contextFactory(config);
49-
const server = mcpServer.createServer(new BrowserServerBackend(config, browserContextFactory), false);
50+
const server = mcpServer.createServer('Playwright Subagent', packageJSON.version, new BrowserServerBackend(config, browserContextFactory), false);
5051
await client.connect(new InProcessTransport(server));
5152
await client.ping();
5253
return new Context(config, client);

src/loopTools/main.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import dotenv from 'dotenv';
1818

1919
import * as mcpServer from '../mcp/server.js';
20-
import * as mcpTransport from '../mcp/transport.js';
2120
import { packageJSON } from '../utils/package.js';
2221
import { Context } from './context.js';
2322
import { perform } from './perform.js';
@@ -30,13 +29,16 @@ import type { Tool } from './tool.js';
3029

3130
export async function runLoopTools(config: FullConfig) {
3231
dotenv.config();
33-
const serverBackendFactory = () => new LoopToolsServerBackend(config);
34-
await mcpTransport.start(serverBackendFactory, config.server);
32+
const serverBackendFactory = {
33+
name: 'Playwright',
34+
nameInConfig: 'playwright-loop',
35+
version: packageJSON.version,
36+
create: () => new LoopToolsServerBackend(config)
37+
};
38+
await mcpServer.start(serverBackendFactory, config.server);
3539
}
3640

3741
class LoopToolsServerBackend implements ServerBackend {
38-
readonly name = 'Playwright';
39-
readonly version = packageJSON.version;
4042
private _config: FullConfig;
4143
private _context: Context | undefined;
4244
private _tools: Tool<any>[] = [perform, snapshot];

src/mcp/DEPS.list

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
[*]
2-
../utils/

src/mcp/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
- Generic MCP utils, no dependencies on Playwright here.
1+
- Generic MCP utils, no dependencies on anything.

src/mcp/transport.ts renamed to src/mcp/http.ts

Lines changed: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,60 @@
1414
* limitations under the License.
1515
*/
1616

17+
import assert from 'assert';
18+
import net from 'net';
1719
import http from 'http';
1820
import crypto from 'crypto';
21+
1922
import debug from 'debug';
2023

2124
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
2225
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
23-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
24-
import { httpAddressToString, startHttpServer } from '../utils/httpServer.js';
2526
import * as mcpServer from './server.js';
2627

2728
import type { ServerBackendFactory } from './server.js';
2829

29-
export async function start(serverBackendFactory: ServerBackendFactory, options: { host?: string; port?: number }) {
30-
if (options.port !== undefined) {
31-
const httpServer = await startHttpServer(options);
32-
startHttpTransport(httpServer, serverBackendFactory);
33-
} else {
34-
await startStdioTransport(serverBackendFactory);
35-
}
30+
const testDebug = debug('pw:mcp:test');
31+
32+
export async function startHttpServer(config: { host?: string, port?: number }, abortSignal?: AbortSignal): Promise<http.Server> {
33+
const { host, port } = config;
34+
const httpServer = http.createServer();
35+
await new Promise<void>((resolve, reject) => {
36+
httpServer.on('error', reject);
37+
abortSignal?.addEventListener('abort', () => {
38+
httpServer.close();
39+
reject(new Error('Aborted'));
40+
});
41+
httpServer.listen(port, host, () => {
42+
resolve();
43+
httpServer.removeListener('error', reject);
44+
});
45+
});
46+
return httpServer;
3647
}
3748

38-
async function startStdioTransport(serverBackendFactory: ServerBackendFactory) {
39-
await mcpServer.connect(serverBackendFactory, new StdioServerTransport(), false);
49+
export function httpAddressToString(address: string | net.AddressInfo | null): string {
50+
assert(address, 'Could not bind server socket');
51+
if (typeof address === 'string')
52+
return address;
53+
const resolvedPort = address.port;
54+
let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
55+
if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]')
56+
resolvedHost = 'localhost';
57+
return `http://${resolvedHost}:${resolvedPort}`;
4058
}
4159

42-
const testDebug = debug('pw:mcp:test');
60+
export async function installHttpTransport(httpServer: http.Server, serverBackendFactory: ServerBackendFactory) {
61+
const sseSessions = new Map();
62+
const streamableSessions = new Map();
63+
httpServer.on('request', async (req, res) => {
64+
const url = new URL(`http://localhost${req.url}`);
65+
if (url.pathname.startsWith('/sse'))
66+
await handleSSE(serverBackendFactory, req, res, url, sseSessions);
67+
else
68+
await handleStreamable(serverBackendFactory, req, res, streamableSessions);
69+
});
70+
}
4371

4472
async function handleSSE(serverBackendFactory: ServerBackendFactory, req: http.IncomingMessage, res: http.ServerResponse, url: URL, sessions: Map<string, SSEServerTransport>) {
4573
if (req.method === 'POST') {
@@ -108,30 +136,3 @@ async function handleStreamable(serverBackendFactory: ServerBackendFactory, req:
108136
res.statusCode = 400;
109137
res.end('Invalid request');
110138
}
111-
112-
function startHttpTransport(httpServer: http.Server, serverBackendFactory: ServerBackendFactory) {
113-
const sseSessions = new Map();
114-
const streamableSessions = new Map();
115-
httpServer.on('request', async (req, res) => {
116-
const url = new URL(`http://localhost${req.url}`);
117-
if (url.pathname.startsWith('/sse'))
118-
await handleSSE(serverBackendFactory, req, res, url, sseSessions);
119-
else
120-
await handleStreamable(serverBackendFactory, req, res, streamableSessions);
121-
});
122-
const url = httpAddressToString(httpServer.address());
123-
const message = [
124-
`Listening on ${url}`,
125-
'Put this in your client config:',
126-
JSON.stringify({
127-
'mcpServers': {
128-
'playwright': {
129-
'url': `${url}/mcp`
130-
}
131-
}
132-
}, undefined, 2),
133-
'For legacy SSE transport support, you can use the /sse endpoint instead.',
134-
].join('\n');
135-
// eslint-disable-next-line no-console
136-
console.error(message);
137-
}

0 commit comments

Comments
 (0)