chore: use mcp sdk via playwright (#973)

This commit is contained in:
Pavel Feldman
2025-09-03 07:58:36 -07:00
committed by GitHub
parent 87741662f4
commit 8d86ce4958
34 changed files with 305 additions and 117 deletions

View File

@@ -19,10 +19,9 @@
import path from 'path';
import url from 'url';
import dotenv from 'dotenv';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { program } from 'commander';
import * as mcpBundle from '../mcp/bundle.js';
import { OpenAIDelegate } from './loopOpenAI.js';
import { ClaudeDelegate } from './loopClaude.js';
import { runTask } from './loop.js';
@@ -34,7 +33,7 @@ dotenv.config();
const __filename = url.fileURLToPath(import.meta.url);
async function run(delegate: LLMDelegate) {
const transport = new StdioClientTransport({
const transport = new mcpBundle.StdioClientTransport({
command: 'node',
args: [
path.resolve(__filename, '../../../cli.js'),
@@ -45,7 +44,7 @@ async function run(delegate: LLMDelegate) {
env: process.env as Record<string, string>,
});
const client = new Client({ name: 'test', version: '1.0.0' });
const client = new mcpBundle.Client({ name: 'test', version: '1.0.0' });
await client.connect(transport);
await client.ping();

View File

@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { contextFactory } from '../browserContextFactory.js';
import { BrowserServerBackend } from '../browserServerBackend.js';
import { Context as BrowserContext } from '../context.js';
@@ -24,7 +23,9 @@ import { ClaudeDelegate } from '../loop/loopClaude.js';
import { InProcessTransport } from '../mcp/inProcessTransport.js';
import * as mcpServer from '../mcp/server.js';
import { packageJSON } from '../utils/package.js';
import * as mcpBundle from '../mcp/bundle.js';
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
import type { LLMDelegate } from '../loop/loop.js';
import type { FullConfig } from '../config.js';
@@ -45,7 +46,7 @@ export class Context {
}
static async create(config: FullConfig) {
const client = new Client({ name: 'Playwright Proxy', version: packageJSON.version });
const client = new mcpBundle.Client({ name: 'Playwright Proxy', version: packageJSON.version });
const browserContextFactory = contextFactory(config);
const server = mcpServer.createServer('Playwright Subagent', packageJSON.version, new BrowserServerBackend(config, browserContextFactory), false);
await client.connect(new InProcessTransport(server));

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { z } from 'zod';
import { z } from '../mcp/bundle.js';
import { defineTool } from './tool.js';
const performSchema = z.object({

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { z } from 'zod';
import { z } from '../mcp/bundle.js';
import { defineTool } from './tool.js';
export const snapshot = defineTool({

48
src/mcp/bundle.ts Normal file
View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @ts-ignore
import * as bundle from 'playwright/lib/mcpBundleImpl';
const zodToJsonSchema: typeof import('zod-to-json-schema').zodToJsonSchema = bundle.zodToJsonSchema;
const Client: typeof import('@modelcontextprotocol/sdk/client/index.js').Client = bundle.Client;
const Server: typeof import('@modelcontextprotocol/sdk/server/index.js').Server = bundle.Server;
const SSEServerTransport: typeof import('@modelcontextprotocol/sdk/server/sse.js').SSEServerTransport = bundle.SSEServerTransport;
const StdioClientTransport: typeof import('@modelcontextprotocol/sdk/client/stdio.js').StdioClientTransport = bundle.StdioClientTransport;
const StdioServerTransport: typeof import('@modelcontextprotocol/sdk/server/stdio.js').StdioServerTransport = bundle.StdioServerTransport;
const StreamableHTTPServerTransport: typeof import('@modelcontextprotocol/sdk/server/streamableHttp.js').StreamableHTTPServerTransport = bundle.StreamableHTTPServerTransport;
const StreamableHTTPClientTransport: typeof import('@modelcontextprotocol/sdk/client/streamableHttp.js').StreamableHTTPClientTransport = bundle.StreamableHTTPClientTransport;
const CallToolRequestSchema: typeof import('@modelcontextprotocol/sdk/types.js').CallToolRequestSchema = bundle.CallToolRequestSchema;
const ListRootsRequestSchema: typeof import('@modelcontextprotocol/sdk/types.js').ListRootsRequestSchema = bundle.ListRootsRequestSchema;
const ListToolsRequestSchema: typeof import('@modelcontextprotocol/sdk/types.js').ListToolsRequestSchema = bundle.ListToolsRequestSchema;
const PingRequestSchema: typeof import('@modelcontextprotocol/sdk/types.js').PingRequestSchema = bundle.PingRequestSchema;
const z: typeof import('zod') = bundle.z;
export {
zodToJsonSchema,
Client,
Server,
SSEServerTransport,
StdioClientTransport,
StdioServerTransport,
StreamableHTTPClientTransport,
StreamableHTTPServerTransport,
CallToolRequestSchema,
ListRootsRequestSchema,
ListToolsRequestSchema,
PingRequestSchema,
z,
};

View File

@@ -21,11 +21,12 @@ import crypto from 'crypto';
import debug from 'debug';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import * as mcpBundle from './bundle.js';
import * as mcpServer from './server.js';
import type { ServerBackendFactory } from './server.js';
import type { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import type { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
const testDebug = debug('pw:mcp:test');
@@ -86,7 +87,7 @@ async function handleSSE(serverBackendFactory: ServerBackendFactory, req: http.I
return await transport.handlePostMessage(req, res);
} else if (req.method === 'GET') {
const transport = new SSEServerTransport('/sse', res);
const transport = new mcpBundle.SSEServerTransport('/sse', res);
sessions.set(transport.sessionId, transport);
testDebug(`create SSE session: ${transport.sessionId}`);
await mcpServer.connect(serverBackendFactory, transport, false);
@@ -114,7 +115,7 @@ async function handleStreamable(serverBackendFactory: ServerBackendFactory, req:
}
if (req.method === 'POST') {
const transport = new StreamableHTTPServerTransport({
const transport = new mcpBundle.StreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
onsessioninitialized: async sessionId => {
testDebug(`create http session: ${transport.sessionId}`);

View File

@@ -15,23 +15,20 @@
*/
import debug from 'debug';
import { z } from 'zod';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { PingRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { defineToolSchema } from './tool.js';
import * as mcpBundle from './bundle.js';
import * as mcpServer from './server.js';
import * as mcpHttp from './http.js';
import { wrapInProcess } from './server.js';
import { ManualPromise } from './manualPromise.js';
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
const mdbDebug = debug('pw:mcp:mdb');
const errorsDebug = debug('pw:mcp:errors');
const z = mcpBundle.z;
export class MDBBackend implements mcpServer.ServerBackend {
private _stack: { client: Client, toolNames: string[], resultPromise: ManualPromise<mcpServer.CallToolResult> | undefined }[] = [];
@@ -107,15 +104,15 @@ export class MDBBackend implements mcpServer.ServerBackend {
private async _pushTools(params: { mcpUrl: string, introMessage?: string }): Promise<mcpServer.CallToolResult> {
mdbDebug('pushing tools to the stack', params.mcpUrl);
const transport = new StreamableHTTPClientTransport(new URL(params.mcpUrl));
const transport = new mcpBundle.StreamableHTTPClientTransport(new URL(params.mcpUrl));
await this._pushClient(transport, params.introMessage);
return { content: [{ type: 'text', text: 'Tools pushed' }] };
}
private async _pushClient(transport: Transport, introMessage?: string): Promise<mcpServer.CallToolResult> {
mdbDebug('pushing client to the stack');
const client = new Client({ name: 'Internal client', version: '0.0.0' });
client.setRequestHandler(PingRequestSchema, () => ({}));
const client = new mcpBundle.Client({ name: 'Internal client', version: '0.0.0' });
client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({}));
await client.connect(transport);
mdbDebug('connected to the new client');
const { tools } = await client.listTools();
@@ -162,7 +159,7 @@ export async function runMainBackend(backendFactory: mcpServer.ServerBackendFact
return url;
// Start stdio conditionally.
await mcpServer.connect(factory, new StdioServerTransport(), false);
await mcpServer.connect(factory, new mcpBundle.StdioServerTransport(), false);
}
export async function runOnPauseBackendLoop(mdbUrl: string, backend: ServerBackendOnPause, introMessage: string) {
@@ -179,9 +176,9 @@ export async function runOnPauseBackendLoop(mdbUrl: string, backend: ServerBacke
await mcpHttp.installHttpTransport(httpServer, factory);
const url = mcpHttp.httpAddressToString(httpServer.address());
const client = new Client({ name: 'Internal client', version: '0.0.0' });
client.setRequestHandler(PingRequestSchema, () => ({}));
const transport = new StreamableHTTPClientTransport(new URL(mdbUrl));
const client = new mcpBundle.Client({ name: 'Internal client', version: '0.0.0' });
client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({}));
const transport = new mcpBundle.StreamableHTTPClientTransport(new URL(mdbUrl));
await client.connect(transport);
const pushToolsResult = await client.callTool({

View File

@@ -15,15 +15,13 @@
*/
import debug from 'debug';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { ListRootsRequestSchema, PingRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import * as mcpBundle from './bundle.js';
import type { ServerBackend, ClientVersion, Root, Server } from './server.js';
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import type { Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
export type MCPProvider = {
name: string;
@@ -32,6 +30,7 @@ export type MCPProvider = {
};
const errorsDebug = debug('pw:mcp:errors');
const { z, zodToJsonSchema } = mcpBundle;
export class ProxyBackend implements ServerBackend {
private _mcpProviders: MCPProvider[];
@@ -112,14 +111,14 @@ export class ProxyBackend implements ServerBackend {
await this._currentClient?.close();
this._currentClient = undefined;
const client = new Client({ name: 'Playwright MCP Proxy', version: '0.0.0' });
const client = new mcpBundle.Client({ name: 'Playwright MCP Proxy', version: '0.0.0' });
client.registerCapabilities({
roots: {
listRoots: true,
},
});
client.setRequestHandler(ListRootsRequestSchema, () => ({ roots: this._roots }));
client.setRequestHandler(PingRequestSchema, () => ({}));
client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._roots }));
client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({}));
const transport = await factory.connect();
await client.connect(transport);

View File

@@ -16,9 +16,7 @@
import debug from 'debug';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import * as mcpBundle from './bundle.js';
import { httpAddressToString, installHttpTransport, startHttpServer } from './http.js';
import { InProcessTransport } from './inProcessTransport.js';
@@ -26,6 +24,7 @@ import type { Tool, CallToolResult, CallToolRequest, Root } from '@modelcontextp
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
export type { Server } from '@modelcontextprotocol/sdk/server/index.js';
export type { Tool, CallToolResult, CallToolRequest, Root } from '@modelcontextprotocol/sdk/types.js';
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
const serverDebug = debug('pw:mcp:server');
const errorsDebug = debug('pw:mcp:errors');
@@ -59,13 +58,13 @@ export async function wrapInProcess(backend: ServerBackend): Promise<Transport>
export function createServer(name: string, version: string, backend: ServerBackend, runHeartbeat: boolean): Server {
let initializedPromiseResolve = () => {};
const initializedPromise = new Promise<void>(resolve => initializedPromiseResolve = resolve);
const server = new Server({ name, version }, {
const server = new mcpBundle.Server({ name, version }, {
capabilities: {
tools: {},
}
});
server.setRequestHandler(ListToolsRequestSchema, async () => {
server.setRequestHandler(mcpBundle.ListToolsRequestSchema, async () => {
serverDebug('listTools');
await initializedPromise;
const tools = await backend.listTools();
@@ -73,7 +72,7 @@ export function createServer(name: string, version: string, backend: ServerBacke
});
let heartbeatRunning = false;
server.setRequestHandler(CallToolRequestSchema, async request => {
server.setRequestHandler(mcpBundle.CallToolRequestSchema, async request => {
serverDebug('callTool', request);
await initializedPromise;
@@ -135,7 +134,7 @@ function addServerListener(server: Server, event: 'close' | 'initialized', liste
export async function start(serverBackendFactory: ServerBackendFactory, options: { host?: string; port?: number }) {
if (options.port === undefined) {
await connect(serverBackendFactory, new StdioServerTransport(), false);
await connect(serverBackendFactory, new mcpBundle.StdioServerTransport(), false);
return;
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { zodToJsonSchema } from 'zod-to-json-schema';
import { zodToJsonSchema } from '../mcp/bundle.js';
import type { z } from 'zod';
import type * as mcpServer from './server.js';

View File

@@ -1,2 +1,3 @@
[*]
../utils/
../mcp/

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { z } from 'zod';
import { z } from '../mcp/bundle.js';
import { defineTabTool, defineTool } from './tool.js';
const close = defineTool({

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { z } from 'zod';
import { z } from '../mcp/bundle.js';
import { defineTabTool } from './tool.js';
const console = defineTabTool({

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { z } from 'zod';
import { z } from '../mcp/bundle.js';
import { defineTabTool } from './tool.js';
const handleDialog = defineTabTool({

View File

@@ -14,8 +14,7 @@
* limitations under the License.
*/
import { z } from 'zod';
import { z } from '../mcp/bundle.js';
import { defineTabTool } from './tool.js';
import * as javascript from '../utils/codegen.js';
import { generateLocator } from './utils.js';

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { z } from 'zod';
import { z } from '../mcp/bundle.js';
import { defineTabTool } from './tool.js';
const uploadFile = defineTabTool({

View File

@@ -14,8 +14,7 @@
* limitations under the License.
*/
import { z } from 'zod';
import { z } from '../mcp/bundle.js';
import { defineTabTool } from './tool.js';
import { generateLocator } from './utils.js';
import * as javascript from '../utils/codegen.js';

View File

@@ -16,10 +16,10 @@
import { fork } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
import { z } from 'zod';
import { defineTool } from './tool.js';
import url from 'url';
import { z } from '../mcp/bundle.js';
import { defineTool } from './tool.js';
const install = defineTool({
capability: 'core-install',
@@ -34,7 +34,7 @@ const install = defineTool({
handle: async (context, params, response) => {
const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.browserName ?? 'chrome';
const cliUrl = import.meta.resolve('playwright/package.json');
const cliPath = path.join(fileURLToPath(cliUrl), '..', 'cli.js');
const cliPath = path.join(url.fileURLToPath(cliUrl), '..', 'cli.js');
const child = fork(cliPath, ['install', channel], {
stdio: 'pipe',
});

View File

@@ -14,8 +14,7 @@
* limitations under the License.
*/
import { z } from 'zod';
import { z } from '../mcp/bundle.js';
import { defineTabTool } from './tool.js';
import { elementSchema } from './snapshot.js';
import { generateLocator } from './utils.js';

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { z } from 'zod';
import { z } from '../mcp/bundle.js';
import { defineTabTool } from './tool.js';
const elementSchema = z.object({

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { z } from 'zod';
import { z } from '../mcp/bundle.js';
import { defineTool, defineTabTool } from './tool.js';
const navigate = defineTool({

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { z } from 'zod';
import { z } from '../mcp/bundle.js';
import { defineTabTool } from './tool.js';
import type * as playwright from 'playwright';

View File

@@ -14,9 +14,8 @@
* limitations under the License.
*/
import { z } from 'zod';
import { z } from '../mcp/bundle.js';
import { defineTabTool } from './tool.js';
import * as javascript from '../utils/codegen.js';
const pdfSchema = z.object({

View File

@@ -14,8 +14,7 @@
* limitations under the License.
*/
import { z } from 'zod';
import { z } from '../mcp/bundle.js';
import { defineTabTool } from './tool.js';
import * as javascript from '../utils/codegen.js';
import { generateLocator } from './utils.js';

View File

@@ -14,8 +14,7 @@
* limitations under the License.
*/
import { z } from 'zod';
import { z } from '../mcp/bundle.js';
import { defineTabTool, defineTool } from './tool.js';
import * as javascript from '../utils/codegen.js';
import { generateLocator } from './utils.js';

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { z } from 'zod';
import { z } from '../mcp/bundle.js';
import { defineTool } from './tool.js';
const browserTabs = defineTool({

View File

@@ -14,8 +14,7 @@
* limitations under the License.
*/
import { z } from 'zod';
import { z } from '../mcp/bundle.js';
import { defineTabTool } from './tool.js';
import * as javascript from '../utils/codegen.js';
import { generateLocator } from './utils.js';

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { z } from 'zod';
import { z } from '../mcp/bundle.js';
import { defineTool } from './tool.js';
const wait = defineTool({

View File

@@ -14,8 +14,8 @@
* limitations under the License.
*/
import os from 'node:os';
import path from 'node:path';
import os from 'os';
import path from 'path';
export function cacheDir() {
let cacheDirectory: string;

View File

@@ -14,15 +14,10 @@
* limitations under the License.
*/
import { fileURLToPath } from 'url';
import url from 'url';
import path from 'path';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { ListRootsRequestSchema, PingRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import * as mcpBundle from '../mcp/bundle.js';
import * as mcpServer from '../mcp/server.js';
import { logUnhandledError } from '../utils/log.js';
import { packageJSON } from '../utils/package.js';
@@ -30,10 +25,15 @@ import { packageJSON } from '../utils/package.js';
import { FullConfig } from '../config.js';
import { BrowserServerBackend } from '../browserServerBackend.js';
import { contextFactory } from '../browserContextFactory.js';
import type { z as zod } from 'zod';
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import type { ClientVersion, ServerBackend } from '../mcp/server.js';
import type { Root, Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
import type { Browser, BrowserContext, BrowserServer } from 'playwright';
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
const { z, zodToJsonSchema } = mcpBundle;
const contextSwitchOptions = z.object({
connectionString: z.string().optional().describe('The connection string to use to connect to the browser'),
@@ -111,7 +111,7 @@ class VSCodeProxyBackend implements ServerBackend {
return url.toString();
}
private async _callContextSwitchTool(params: z.infer<typeof contextSwitchOptions>): Promise<CallToolResult> {
private async _callContextSwitchTool(params: zod.infer<typeof contextSwitchOptions>): Promise<CallToolResult> {
if (params.debugController) {
const url = await this._getDebugControllerURL();
const lines = [`### Result`];
@@ -133,11 +133,11 @@ class VSCodeProxyBackend implements ServerBackend {
}
await this._setCurrentClient(
new StdioClientTransport({
new mcpBundle.StdioClientTransport({
command: process.execPath,
cwd: process.cwd(),
args: [
path.join(fileURLToPath(import.meta.url), '..', 'main.js'),
path.join(url.fileURLToPath(import.meta.url), '..', 'main.js'),
JSON.stringify(this._config),
params.connectionString,
params.lib,
@@ -166,14 +166,14 @@ class VSCodeProxyBackend implements ServerBackend {
await this._currentClient?.close();
this._currentClient = undefined;
const client = new Client(this._clientVersion!);
const client = new mcpBundle.Client(this._clientVersion!);
client.registerCapabilities({
roots: {
listRoots: true,
},
});
client.setRequestHandler(ListRootsRequestSchema, () => ({ roots: this._roots }));
client.setRequestHandler(PingRequestSchema, () => ({}));
client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._roots }));
client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({}));
await client.connect(transport);
this._currentClient = client;

View File

@@ -14,10 +14,11 @@
* limitations under the License.
*/
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import * as mcpBundle from '../mcp/bundle.js';
import * as mcpServer from '../mcp/server.js';
import { BrowserServerBackend } from '../browserServerBackend.js';
import { BrowserContextFactory, ClientInfo } from '../browserContextFactory.js';
import type { FullConfig } from '../config.js';
import type { BrowserContext } from 'playwright-core';
@@ -63,7 +64,7 @@ async function main(config: FullConfig, connectionString: string, lib: string) {
create: () => new BrowserServerBackend(config, factory),
version: 'unused'
},
new StdioServerTransport(),
new mcpBundle.StdioServerTransport(),
false
);
}