nicer typescript types
This commit is contained in:
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user