nicer typescript types
This commit is contained in:
@@ -23,9 +23,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.0.315",
|
||||
"typescript": "^5.8.2",
|
||||
"concurrently": "^8.2.2",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^5.4.21",
|
||||
"vite-plugin-static-copy": "^3.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwriter": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Protocol } from 'playwriter/src/cdp-types';
|
||||
import type { ExtensionCommandMessage, ExtensionResponseMessage } from 'playwriter/src/extension/protocol';
|
||||
|
||||
export function debugLog(...args: unknown[]): void {
|
||||
const enabled = true;
|
||||
if (enabled) {
|
||||
@@ -22,20 +25,6 @@ export function debugLog(...args: unknown[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
type ProtocolCommand = {
|
||||
id: number;
|
||||
method: string;
|
||||
params?: any;
|
||||
};
|
||||
|
||||
type ProtocolResponse = {
|
||||
id?: number;
|
||||
method?: string;
|
||||
params?: any;
|
||||
result?: any;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export class RelayConnection {
|
||||
private _debuggee: chrome.debugger.Debuggee;
|
||||
private _ws: WebSocket;
|
||||
@@ -110,7 +99,7 @@ export class RelayConnection {
|
||||
}
|
||||
|
||||
private async _onMessageAsync(event: MessageEvent): Promise<void> {
|
||||
let message: ProtocolCommand;
|
||||
let message: ExtensionCommandMessage;
|
||||
try {
|
||||
message = JSON.parse(event.data);
|
||||
} catch (error: any) {
|
||||
@@ -121,7 +110,7 @@ export class RelayConnection {
|
||||
|
||||
debugLog('Received message:', message);
|
||||
|
||||
const response: ProtocolResponse = {
|
||||
const response: ExtensionResponseMessage = {
|
||||
id: message.id,
|
||||
};
|
||||
try {
|
||||
@@ -134,12 +123,12 @@ export class RelayConnection {
|
||||
this._sendMessage(response);
|
||||
}
|
||||
|
||||
private async _handleCommand(message: ProtocolCommand): Promise<any> {
|
||||
private async _handleCommand(message: ExtensionCommandMessage): Promise<any> {
|
||||
if (message.method === 'attachToTab') {
|
||||
await this._tabPromise;
|
||||
debugLog('Attaching debugger to tab:', this._debuggee);
|
||||
await chrome.debugger.attach(this._debuggee, '1.3');
|
||||
const result: any = await chrome.debugger.sendCommand(this._debuggee, 'Target.getTargetInfo');
|
||||
const result = await chrome.debugger.sendCommand(this._debuggee, 'Target.getTargetInfo') as Protocol.Target.GetTargetInfoResponse;
|
||||
return {
|
||||
targetInfo: result?.targetInfo,
|
||||
};
|
||||
@@ -147,13 +136,13 @@ export class RelayConnection {
|
||||
if (!this._debuggee.tabId)
|
||||
throw new Error('No tab is connected. Please go to the Playwright MCP extension and select the tab you want to connect to.');
|
||||
if (message.method === 'forwardCDPCommand') {
|
||||
const { sessionId, method, params } = message.params;
|
||||
const forwardParams = message.params as { sessionId?: string; method: string; params?: any };
|
||||
const { sessionId, method, params } = forwardParams;
|
||||
debugLog('CDP command:', method, params);
|
||||
const debuggerSession: chrome.debugger.DebuggerSession = {
|
||||
...this._debuggee,
|
||||
sessionId,
|
||||
};
|
||||
// Forward CDP command to chrome.debugger
|
||||
return await chrome.debugger.sendCommand(
|
||||
debuggerSession,
|
||||
method,
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
now see where attachToTab is sent and received. see that we basically attach to the tab as soon as the playwright instance connects. I want to change this: instead attach to the tab when we click on the extension icon, meaning inside onClicked. when this happens we should also send also a cdp message from the extension
|
||||
|
||||
currently the extension and server code assume there is only one tab attached to debugger only. i want instead to be able to manage multiple tabs. we should update all classes fields that manage tab state to a array {tabId, dsessionId, targetId}[], then we should send relevant commands to the right chrome.debugger debugee tab, finding the right tab for a specific CDP command (based on sessionId or targetId)
|
||||
|
||||
---
|
||||
|
||||
now see where attachToTab is sent and received. see that we basically attach to the tab as soon as the playwright instance connects. I want to change this: instead attach to the tab when we click on the extension icon, meaning inside onClicked. when this happens we should also send also a cdp message from the extension Target.attachedToTarget after we are connected. so playwright knows of this new page
|
||||
|
||||
now see where we already are sending Target.attachedToTarget. this is now sent setAutoAttach, meaning as soon as playwright connects. let's change this. we should not do anything during setAutoAttach, leave it empty for now.
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.21.1",
|
||||
"devtools-protocol": "^0.0.1543509",
|
||||
"playwright-core": "^1.56.1",
|
||||
"user-agents": "^1.1.669",
|
||||
"ws": "^8.18.3",
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import type { Protocol } from 'devtools-protocol';
|
||||
import type { ProtocolMapping } from 'devtools-protocol/types/protocol-mapping.js';
|
||||
|
||||
export type CDPCommand<T extends keyof ProtocolMapping.Commands = keyof ProtocolMapping.Commands> = {
|
||||
id: number;
|
||||
sessionId?: string;
|
||||
method: T;
|
||||
params?: ProtocolMapping.Commands[T]['paramsType'][0];
|
||||
};
|
||||
|
||||
export type CDPResponse<T extends keyof ProtocolMapping.Commands = keyof ProtocolMapping.Commands> = {
|
||||
id: number;
|
||||
sessionId?: string;
|
||||
result?: ProtocolMapping.Commands[T]['returnType'];
|
||||
error?: { code?: number; message: string };
|
||||
};
|
||||
|
||||
export type CDPEvent<T extends keyof ProtocolMapping.Events = keyof ProtocolMapping.Events> = {
|
||||
method: T;
|
||||
sessionId?: string;
|
||||
params?: ProtocolMapping.Events[T][0];
|
||||
};
|
||||
|
||||
export type CDPMessage = CDPCommand | CDPResponse | CDPEvent;
|
||||
|
||||
export { Protocol, ProtocolMapping };
|
||||
|
||||
// types tests. to see if types are right with some simple examples
|
||||
if (false as any) {
|
||||
const browserVersionCommand = {
|
||||
id: 1,
|
||||
method: 'Browser.getVersion',
|
||||
} satisfies CDPCommand;
|
||||
|
||||
const browserVersionResponse = {
|
||||
id: 1,
|
||||
result: {
|
||||
protocolVersion: '1.3',
|
||||
product: 'Chrome',
|
||||
revision: '123',
|
||||
userAgent: 'Mozilla/5.0',
|
||||
jsVersion: 'V8',
|
||||
}
|
||||
} satisfies CDPResponse;
|
||||
|
||||
const targetAttachCommand = {
|
||||
id: 2,
|
||||
method: 'Target.setAutoAttach',
|
||||
params: {
|
||||
autoAttach: true,
|
||||
waitForDebuggerOnStart: false,
|
||||
}
|
||||
} satisfies CDPCommand;
|
||||
|
||||
const targetAttachResponse = {
|
||||
id: 2,
|
||||
result: undefined,
|
||||
} satisfies CDPResponse;
|
||||
|
||||
const attachedToTargetEvent = {
|
||||
method: 'Target.attachedToTarget',
|
||||
params: {
|
||||
sessionId: 'session-1',
|
||||
targetInfo: {
|
||||
targetId: 'target-1',
|
||||
type: 'page',
|
||||
title: 'Example',
|
||||
url: 'https://example.com',
|
||||
attached: true,
|
||||
canAccessOpener: false,
|
||||
},
|
||||
waitingForDebugger: false,
|
||||
}
|
||||
} satisfies CDPEvent;
|
||||
|
||||
const consoleMessageEvent = {
|
||||
method: 'Runtime.consoleAPICalled',
|
||||
params: {
|
||||
type: 'log',
|
||||
args: [],
|
||||
executionContextId: 1,
|
||||
timestamp: 123456789,
|
||||
}
|
||||
} satisfies CDPEvent;
|
||||
|
||||
const pageNavigateCommand = {
|
||||
id: 3,
|
||||
method: 'Page.navigate',
|
||||
params: {
|
||||
url: 'https://example.com',
|
||||
}
|
||||
} satisfies CDPCommand;
|
||||
|
||||
const pageNavigateResponse = {
|
||||
id: 3,
|
||||
result: {
|
||||
frameId: 'frame-1',
|
||||
}
|
||||
} satisfies CDPResponse;
|
||||
|
||||
const networkRequestEvent = {
|
||||
method: 'Network.requestWillBeSent',
|
||||
sessionId: 'session-1',
|
||||
params: {
|
||||
requestId: 'req-1',
|
||||
loaderId: 'loader-1',
|
||||
documentURL: 'https://example.com',
|
||||
request: {
|
||||
url: 'https://example.com/api',
|
||||
method: 'GET',
|
||||
headers: {},
|
||||
initialPriority: 'High',
|
||||
referrerPolicy: 'no-referrer',
|
||||
},
|
||||
timestamp: 123456789,
|
||||
wallTime: 123456789,
|
||||
initiator: {
|
||||
type: 'other',
|
||||
},
|
||||
redirectHasExtraInfo: false,
|
||||
type: 'XHR',
|
||||
}
|
||||
} satisfies CDPEvent;
|
||||
}
|
||||
@@ -34,27 +34,12 @@ import { ManualPromise } from 'playwright-core/lib/utils';
|
||||
|
||||
import type { WebSocket, WebSocketServer } from 'playwright-core/lib/utilsBundle';
|
||||
import type websocket from 'ws';
|
||||
import type { ExtensionCommand, ExtensionEvents } from './protocol.js';
|
||||
import type { ExtensionCommandMessage, ExtensionEventMessage, ExtensionMessage } from './protocol.js';
|
||||
import type { CDPCommand, CDPResponse, CDPEvent, Protocol } from '../cdp-types.js';
|
||||
|
||||
|
||||
const debugLogger = debug('pw:mcp:relay');
|
||||
|
||||
type CDPCommand = {
|
||||
id: number;
|
||||
sessionId?: string;
|
||||
method: string;
|
||||
params?: any;
|
||||
};
|
||||
|
||||
type CDPResponse = {
|
||||
id?: number;
|
||||
sessionId?: string;
|
||||
method?: string;
|
||||
params?: any;
|
||||
result?: any;
|
||||
error?: { code?: number; message: string };
|
||||
};
|
||||
|
||||
export class CDPRelayServer {
|
||||
private _wsHost: string;
|
||||
|
||||
@@ -180,16 +165,14 @@ export class CDPRelayServer {
|
||||
this._extensionConnectionPromise.resolve();
|
||||
}
|
||||
|
||||
private _handleExtensionMessage<M extends keyof ExtensionEvents>(method: M, params: ExtensionEvents[M]['params']) {
|
||||
switch (method) {
|
||||
case 'forwardCDPEvent':
|
||||
const sessionId = params.sessionId || this._connectedTabInfo?.sessionId;
|
||||
this._sendToPlaywright({
|
||||
sessionId,
|
||||
method: params.method,
|
||||
params: params.params
|
||||
});
|
||||
break;
|
||||
private _handleExtensionMessage(message: ExtensionEventMessage) {
|
||||
if (message.method === 'forwardCDPEvent') {
|
||||
const sessionId = message.params.sessionId || this._connectedTabInfo?.sessionId;
|
||||
this._sendToPlaywright({
|
||||
sessionId,
|
||||
method: message.params.method,
|
||||
params: message.params.params
|
||||
} as CDPEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,8 +198,10 @@ export class CDPRelayServer {
|
||||
return {
|
||||
protocolVersion: '1.3',
|
||||
product: 'Chrome/Extension-Bridge',
|
||||
revision: '1.0.0',
|
||||
userAgent: 'CDP-Bridge-Server/1.0.0',
|
||||
};
|
||||
jsVersion: 'V8',
|
||||
} satisfies Protocol.Browser.GetVersionResponse;
|
||||
}
|
||||
case 'Browser.setDownloadBehavior': {
|
||||
return { };
|
||||
@@ -228,7 +213,7 @@ export class CDPRelayServer {
|
||||
// Simulate auto-attach behavior with real target info
|
||||
if (!this._extensionConnection)
|
||||
throw new Error('Extension not connected. Please ensure the Chrome extension is installed and connected to the extension endpoint before connecting Playwright.');
|
||||
const { targetInfo } = await this._extensionConnection.send('attachToTab', { });
|
||||
const { targetInfo } = await this._extensionConnection.send({ method: 'attachToTab' });
|
||||
this._connectedTabInfo = {
|
||||
targetInfo,
|
||||
sessionId: `pw-tab-${this._nextSessionId++}`,
|
||||
@@ -244,7 +229,7 @@ export class CDPRelayServer {
|
||||
},
|
||||
waitingForDebugger: false
|
||||
}
|
||||
});
|
||||
} satisfies CDPEvent);
|
||||
return { };
|
||||
}
|
||||
case 'Target.getTargetInfo': {
|
||||
@@ -260,29 +245,27 @@ export class CDPRelayServer {
|
||||
// Top level sessionId is only passed between the relay and the client.
|
||||
if (this._connectedTabInfo?.sessionId === sessionId)
|
||||
sessionId = undefined;
|
||||
return await this._extensionConnection.send('forwardCDPCommand', { sessionId, method, params });
|
||||
return await this._extensionConnection.send({
|
||||
method: 'forwardCDPCommand',
|
||||
params: { sessionId, method, params }
|
||||
});
|
||||
}
|
||||
|
||||
private _sendToPlaywright(message: CDPResponse): void {
|
||||
debugLogger('→ Playwright:', `${message.method ?? `response(id=${message.id})`}`);
|
||||
private _sendToPlaywright(message: CDPResponse | CDPEvent): void {
|
||||
const logMessage = 'method' in message && message.method
|
||||
? message.method
|
||||
: `response(id=${'id' in message ? message.id : 'unknown'})`;
|
||||
debugLogger('→ Playwright:', logMessage);
|
||||
this._playwrightConnection?.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
type ExtensionResponse = {
|
||||
id?: number;
|
||||
method?: string;
|
||||
params?: any;
|
||||
result?: any;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
class ExtensionConnection {
|
||||
private readonly _ws: WebSocket;
|
||||
private readonly _callbacks = new Map<number, { resolve: (o: any) => void, reject: (e: Error) => void, error: Error }>();
|
||||
private _lastId = 0;
|
||||
|
||||
onmessage?: <M extends keyof ExtensionEvents>(method: M, params: ExtensionEvents[M]['params']) => void;
|
||||
onmessage?: (message: ExtensionEventMessage) => void;
|
||||
onclose?: (self: ExtensionConnection, reason: string) => void;
|
||||
|
||||
constructor(ws: WebSocket) {
|
||||
@@ -292,12 +275,12 @@ class ExtensionConnection {
|
||||
this._ws.on('error', this._onError.bind(this));
|
||||
}
|
||||
|
||||
async send<M extends keyof ExtensionCommand>(method: M, params: ExtensionCommand[M]['params']): Promise<any> {
|
||||
async send(command: Omit<ExtensionCommandMessage, 'id'>): Promise<any> {
|
||||
if (this._ws.readyState !== ws.OPEN)
|
||||
throw new Error(`Unexpected WebSocket state: ${this._ws.readyState}`);
|
||||
const id = ++this._lastId;
|
||||
this._ws.send(JSON.stringify({ id, method, params }));
|
||||
const error = new Error(`Protocol error: ${method}`);
|
||||
this._ws.send(JSON.stringify({ id, ...command }));
|
||||
const error = new Error(`Protocol error: ${command.method}`);
|
||||
return new Promise((resolve, reject) => {
|
||||
this._callbacks.set(id, { resolve, reject, error });
|
||||
});
|
||||
@@ -327,8 +310,8 @@ class ExtensionConnection {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleParsedMessage(object: ExtensionResponse) {
|
||||
if (object.id && this._callbacks.has(object.id)) {
|
||||
private _handleParsedMessage(object: ExtensionMessage) {
|
||||
if ('id' in object && this._callbacks.has(object.id)) {
|
||||
const callback = this._callbacks.get(object.id)!;
|
||||
this._callbacks.delete(object.id);
|
||||
if (object.error) {
|
||||
@@ -338,10 +321,10 @@ class ExtensionConnection {
|
||||
} else {
|
||||
callback.resolve(object.result);
|
||||
}
|
||||
} else if (object.id) {
|
||||
} else if ('id' in object) {
|
||||
debugLogger('← Extension: unexpected response', object);
|
||||
} else {
|
||||
this.onmessage?.(object.method! as keyof ExtensionEvents, object.params);
|
||||
this.onmessage?.(object as ExtensionEventMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import { BrowserContextFactory, ClientInfo } from './types.js';
|
||||
|
||||
const debugLogger = debug('pw:mcp:relay');
|
||||
|
||||
export async function createExtensionContext(abortSignal: AbortSignal, ): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> {
|
||||
export async function startRelayServer(abortSignal: AbortSignal, ){
|
||||
// Merge obtainBrowser into this function
|
||||
const httpServer = await startHttpServer({ port: 9988 });
|
||||
if (abortSignal.aborted) {
|
||||
@@ -41,16 +41,16 @@ export async function createExtensionContext(abortSignal: AbortSignal, ): Promis
|
||||
// TODO simply call fetch. it was creatign a full fledged browser just to fetch an url previously. CRAZY
|
||||
// await cdpRelayServer.ensureExtensionConnectionForMCPContext(clientInfo, abortSignal, toolName);
|
||||
// await waitForExtension()
|
||||
return {}
|
||||
const browser = await playwright.chromium.connectOverCDP(cdpRelayServer.cdpEndpoint());
|
||||
return { cdpRelayServer }
|
||||
// const browser = await playwright.chromium.connectOverCDP(cdpRelayServer.cdpEndpoint());
|
||||
|
||||
return {
|
||||
browserContext: browser.contexts()[0],
|
||||
close: async () => {
|
||||
debugLogger('close() called for browser context');
|
||||
await browser.close();
|
||||
}
|
||||
};
|
||||
// return {
|
||||
// browserContext: browser.contexts()[0],
|
||||
// close: async () => {
|
||||
// debugLogger('close() called for browser context');
|
||||
// await browser.close();
|
||||
// }
|
||||
// };
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -18,25 +18,35 @@
|
||||
// extension version should be compatible with the old MCP clients.
|
||||
export const VERSION = 1;
|
||||
|
||||
export type ExtensionCommand = {
|
||||
'attachToTab': {
|
||||
params: {};
|
||||
};
|
||||
'forwardCDPCommand': {
|
||||
params: {
|
||||
method: string,
|
||||
sessionId?: string
|
||||
params?: any,
|
||||
export type ExtensionCommandMessage =
|
||||
| {
|
||||
id: number;
|
||||
method: 'attachToTab';
|
||||
params?: {};
|
||||
}
|
||||
| {
|
||||
id: number;
|
||||
method: 'forwardCDPCommand';
|
||||
params: {
|
||||
method: string;
|
||||
sessionId?: string;
|
||||
params?: any;
|
||||
};
|
||||
};
|
||||
|
||||
export type ExtensionResponseMessage = {
|
||||
id: number;
|
||||
result?: any;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export type ExtensionEventMessage = {
|
||||
method: 'forwardCDPEvent';
|
||||
params: {
|
||||
method: string;
|
||||
sessionId?: string;
|
||||
params?: any;
|
||||
};
|
||||
};
|
||||
|
||||
export type ExtensionEvents = {
|
||||
'forwardCDPEvent': {
|
||||
params: {
|
||||
method: string,
|
||||
sessionId?: string
|
||||
params?: any,
|
||||
};
|
||||
};
|
||||
};
|
||||
export type ExtensionMessage = ExtensionResponseMessage | ExtensionEventMessage;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
||||
import { z } from 'zod'
|
||||
import { Page, Browser, BrowserContext, chromium } from 'patchright-core'
|
||||
import { Page, Browser, BrowserContext, chromium } from 'playwright-core'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import os from 'node:os'
|
||||
|
||||
Generated
+12
@@ -28,6 +28,10 @@ importers:
|
||||
version: 4.0.8(@types/node@24.10.1)(@vitest/ui@4.0.8)(tsx@4.20.6)
|
||||
|
||||
extension-playwright:
|
||||
dependencies:
|
||||
playwriter:
|
||||
specifier: workspace:*
|
||||
version: link:../playwriter
|
||||
devDependencies:
|
||||
'@types/chrome':
|
||||
specifier: ^0.0.315
|
||||
@@ -52,6 +56,9 @@ importers:
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: ^1.21.1
|
||||
version: 1.21.1
|
||||
devtools-protocol:
|
||||
specifier: ^0.0.1543509
|
||||
version: 0.0.1543509
|
||||
playwright-core:
|
||||
specifier: ^1.56.1
|
||||
version: 1.56.1
|
||||
@@ -803,6 +810,9 @@ packages:
|
||||
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
devtools-protocol@0.0.1543509:
|
||||
resolution: {integrity: sha512-JlHmIPtgDeqcL2uwPA7xryDNSb1ug9h11IexYQUG98vcyV6/L3taLRECgUk/B/7yQhXC/sgBzKj9kaB+bxmdWg==}
|
||||
|
||||
dir-glob@3.0.1:
|
||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2310,6 +2320,8 @@ snapshots:
|
||||
|
||||
detect-indent@6.1.0: {}
|
||||
|
||||
devtools-protocol@0.0.1543509: {}
|
||||
|
||||
dir-glob@3.0.1:
|
||||
dependencies:
|
||||
path-type: 4.0.0
|
||||
|
||||
Reference in New Issue
Block a user