mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2025-07-25 07:52:27 +08:00
chore: save session log (#740)
This commit is contained in:
parent
6320b08173
commit
b1a0f775cf
@ -162,6 +162,8 @@ Playwright MCP server supports following arguments. They can be provided in the
|
|||||||
example ".com,chromium.org,.domain.com"
|
example ".com,chromium.org,.domain.com"
|
||||||
--proxy-server <proxy> specify proxy server, for example
|
--proxy-server <proxy> specify proxy server, for example
|
||||||
"http://myproxy:3128" or "socks5://myproxy:8080"
|
"http://myproxy:3128" or "socks5://myproxy:8080"
|
||||||
|
--save-session Whether to save the Playwright MCP session into
|
||||||
|
the output directory.
|
||||||
--save-trace Whether to save the Playwright Trace of the
|
--save-trace Whether to save the Playwright Trace of the
|
||||||
session into the output directory.
|
session into the output directory.
|
||||||
--storage-state <path> path to the storage state file for isolated
|
--storage-state <path> path to the storage state file for isolated
|
||||||
|
5
config.d.ts
vendored
5
config.d.ts
vendored
@ -85,6 +85,11 @@ export type Config = {
|
|||||||
*/
|
*/
|
||||||
capabilities?: ToolCapability[];
|
capabilities?: ToolCapability[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to save the Playwright session into the output directory.
|
||||||
|
*/
|
||||||
|
saveSession?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to save the Playwright trace of the session into the output directory.
|
* Whether to save the Playwright trace of the session into the output directory.
|
||||||
*/
|
*/
|
||||||
|
@ -42,6 +42,7 @@ export type CLIOptions = {
|
|||||||
port?: number;
|
port?: number;
|
||||||
proxyBypass?: string;
|
proxyBypass?: string;
|
||||||
proxyServer?: string;
|
proxyServer?: string;
|
||||||
|
saveSession?: boolean;
|
||||||
saveTrace?: boolean;
|
saveTrace?: boolean;
|
||||||
storageState?: string;
|
storageState?: string;
|
||||||
userAgent?: string;
|
userAgent?: string;
|
||||||
@ -189,6 +190,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
|
|||||||
allowedOrigins: cliOptions.allowedOrigins,
|
allowedOrigins: cliOptions.allowedOrigins,
|
||||||
blockedOrigins: cliOptions.blockedOrigins,
|
blockedOrigins: cliOptions.blockedOrigins,
|
||||||
},
|
},
|
||||||
|
saveSession: cliOptions.saveSession,
|
||||||
saveTrace: cliOptions.saveTrace,
|
saveTrace: cliOptions.saveTrace,
|
||||||
outputDir: cliOptions.outputDir,
|
outputDir: cliOptions.outputDir,
|
||||||
imageResponses: cliOptions.imageResponses,
|
imageResponses: cliOptions.imageResponses,
|
||||||
|
@ -22,12 +22,11 @@ import { Context } from './context.js';
|
|||||||
import { Response } from './response.js';
|
import { Response } from './response.js';
|
||||||
import { allTools } from './tools.js';
|
import { allTools } from './tools.js';
|
||||||
import { packageJSON } from './package.js';
|
import { packageJSON } from './package.js';
|
||||||
|
|
||||||
import { FullConfig } from './config.js';
|
import { FullConfig } from './config.js';
|
||||||
|
import { SessionLog } from './sessionLog.js';
|
||||||
import type { BrowserContextFactory } from './browserContextFactory.js';
|
import type { BrowserContextFactory } from './browserContextFactory.js';
|
||||||
|
|
||||||
export function createConnection(config: FullConfig, browserContextFactory: BrowserContextFactory): Connection {
|
export async function createConnection(config: FullConfig, browserContextFactory: BrowserContextFactory): Promise<Connection> {
|
||||||
const tools = allTools.filter(tool => tool.capability.startsWith('core') || config.capabilities?.includes(tool.capability));
|
const tools = allTools.filter(tool => tool.capability.startsWith('core') || config.capabilities?.includes(tool.capability));
|
||||||
const context = new Context(tools, config, browserContextFactory);
|
const context = new Context(tools, config, browserContextFactory);
|
||||||
const server = new McpServer({ name: 'Playwright', version: packageJSON.version }, {
|
const server = new McpServer({ name: 'Playwright', version: packageJSON.version }, {
|
||||||
@ -36,6 +35,8 @@ export function createConnection(config: FullConfig, browserContextFactory: Brow
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sessionLog = config.saveSession ? await SessionLog.create(config) : undefined;
|
||||||
|
|
||||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
return {
|
return {
|
||||||
tools: tools.map(tool => ({
|
tools: tools.map(tool => ({
|
||||||
@ -62,8 +63,10 @@ export function createConnection(config: FullConfig, browserContextFactory: Brow
|
|||||||
return errorResult(`Tool "${request.params.name}" not found`);
|
return errorResult(`Tool "${request.params.name}" not found`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = new Response(context);
|
const response = new Response(context, request.params.name, request.params.arguments || {});
|
||||||
await tool.handle(context, tool.schema.inputSchema.parse(request.params.arguments || {}), response);
|
await tool.handle(context, tool.schema.inputSchema.parse(request.params.arguments || {}), response);
|
||||||
|
if (sessionLog)
|
||||||
|
await sessionLog.log(response);
|
||||||
return await response.serialize();
|
return await response.serialize();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResult(String(error));
|
return errorResult(String(error));
|
||||||
|
@ -46,6 +46,7 @@ program
|
|||||||
.option('--port <port>', 'port to listen on for SSE transport.')
|
.option('--port <port>', 'port to listen on for SSE transport.')
|
||||||
.option('--proxy-bypass <bypass>', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"')
|
.option('--proxy-bypass <bypass>', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"')
|
||||||
.option('--proxy-server <proxy>', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"')
|
.option('--proxy-server <proxy>', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"')
|
||||||
|
.option('--save-session', 'Whether to save the Playwright MCP session into the output directory.')
|
||||||
.option('--save-trace', 'Whether to save the Playwright Trace of the session into the output directory.')
|
.option('--save-trace', 'Whether to save the Playwright Trace of the session into the output directory.')
|
||||||
.option('--storage-state <path>', 'path to the storage state file for isolated sessions.')
|
.option('--storage-state <path>', 'path to the storage state file for isolated sessions.')
|
||||||
.option('--user-agent <ua string>', 'specify user agent string')
|
.option('--user-agent <ua string>', 'specify user agent string')
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js';
|
import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js';
|
||||||
import type { Context } from './context.js';
|
import type { Context } from './context.js';
|
||||||
|
|
||||||
export class Response {
|
export class Response {
|
||||||
@ -24,23 +24,41 @@ export class Response {
|
|||||||
private _context: Context;
|
private _context: Context;
|
||||||
private _includeSnapshot = false;
|
private _includeSnapshot = false;
|
||||||
private _includeTabs = false;
|
private _includeTabs = false;
|
||||||
|
private _snapshot: string | undefined;
|
||||||
|
|
||||||
constructor(context: Context) {
|
readonly toolName: string;
|
||||||
|
readonly toolArgs: Record<string, any>;
|
||||||
|
|
||||||
|
constructor(context: Context, toolName: string, toolArgs: Record<string, any>) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
|
this.toolName = toolName;
|
||||||
|
this.toolArgs = toolArgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
addResult(result: string) {
|
addResult(result: string) {
|
||||||
this._result.push(result);
|
this._result.push(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result() {
|
||||||
|
return this._result.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
addCode(code: string) {
|
addCode(code: string) {
|
||||||
this._code.push(code);
|
this._code.push(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
code() {
|
||||||
|
return this._code.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
addImage(image: { contentType: string, data: Buffer }) {
|
addImage(image: { contentType: string, data: Buffer }) {
|
||||||
this._images.push(image);
|
this._images.push(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
images() {
|
||||||
|
return this._images;
|
||||||
|
}
|
||||||
|
|
||||||
setIncludeSnapshot() {
|
setIncludeSnapshot() {
|
||||||
this._includeSnapshot = true;
|
this._includeSnapshot = true;
|
||||||
}
|
}
|
||||||
@ -49,8 +67,14 @@ export class Response {
|
|||||||
this._includeTabs = true;
|
this._includeTabs = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
includeSnapshot() {
|
async snapshot(): Promise<string> {
|
||||||
return this._includeSnapshot;
|
if (this._snapshot !== undefined)
|
||||||
|
return this._snapshot;
|
||||||
|
if (this._includeSnapshot && this._context.currentTab())
|
||||||
|
this._snapshot = await this._context.currentTabOrDie().captureSnapshot();
|
||||||
|
else
|
||||||
|
this._snapshot = '';
|
||||||
|
return this._snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
async serialize(): Promise<{ content: (TextContent | ImageContent)[] }> {
|
async serialize(): Promise<{ content: (TextContent | ImageContent)[] }> {
|
||||||
@ -77,8 +101,9 @@ ${this._code.join('\n')}
|
|||||||
response.push(...(await this._context.listTabsMarkdown(this._includeTabs)));
|
response.push(...(await this._context.listTabsMarkdown(this._includeTabs)));
|
||||||
|
|
||||||
// Add snapshot if provided.
|
// Add snapshot if provided.
|
||||||
if (this._includeSnapshot && this._context.currentTab())
|
const snapshot = await this.snapshot();
|
||||||
response.push(await this._context.currentTabOrDie().captureSnapshot(), '');
|
if (snapshot)
|
||||||
|
response.push(snapshot, '');
|
||||||
|
|
||||||
// Main response part
|
// Main response part
|
||||||
const content: (TextContent | ImageContent)[] = [
|
const content: (TextContent | ImageContent)[] = [
|
||||||
|
@ -35,7 +35,7 @@ export class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createConnection(transport: Transport): Promise<Connection> {
|
async createConnection(transport: Transport): Promise<Connection> {
|
||||||
const connection = createConnection(this.config, this._contextFactory);
|
const connection = await createConnection(this.config, this._contextFactory);
|
||||||
this._connectionList.push(connection);
|
this._connectionList.push(connection);
|
||||||
await connection.server.connect(transport);
|
await connection.server.connect(transport);
|
||||||
return connection;
|
return connection;
|
||||||
|
92
src/sessionLog.ts
Normal file
92
src/sessionLog.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { outputFile } from './config.js';
|
||||||
|
import { Response } from './response.js';
|
||||||
|
import type { FullConfig } from './config.js';
|
||||||
|
|
||||||
|
let sessionOrdinal = 0;
|
||||||
|
|
||||||
|
export class SessionLog {
|
||||||
|
private _folder: string;
|
||||||
|
private _file: string;
|
||||||
|
private _ordinal = 0;
|
||||||
|
|
||||||
|
constructor(sessionFolder: string) {
|
||||||
|
this._folder = sessionFolder;
|
||||||
|
this._file = path.join(this._folder, 'session.md');
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(config: FullConfig): Promise<SessionLog> {
|
||||||
|
const sessionFolder = await outputFile(config, `session-${(++sessionOrdinal).toString().padStart(3, '0')}`);
|
||||||
|
await fs.promises.mkdir(sessionFolder, { recursive: true });
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(`Session: ${sessionFolder}`);
|
||||||
|
return new SessionLog(sessionFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
async log(response: Response) {
|
||||||
|
const prefix = `${(++this._ordinal).toString().padStart(3, '0')}`;
|
||||||
|
const lines: string[] = [
|
||||||
|
`### Tool: ${response.toolName}`,
|
||||||
|
``,
|
||||||
|
`- Args`,
|
||||||
|
'```json',
|
||||||
|
JSON.stringify(response.toolArgs, null, 2),
|
||||||
|
'```',
|
||||||
|
];
|
||||||
|
if (response.result()) {
|
||||||
|
lines.push(
|
||||||
|
`- Result`,
|
||||||
|
'```',
|
||||||
|
response.result(),
|
||||||
|
'```');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.code()) {
|
||||||
|
lines.push(
|
||||||
|
`- Code`,
|
||||||
|
'```js',
|
||||||
|
response.code(),
|
||||||
|
'```');
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshot = await response.snapshot();
|
||||||
|
if (snapshot) {
|
||||||
|
const fileName = `${prefix}.snapshot.yml`;
|
||||||
|
await fs.promises.writeFile(path.join(this._folder, fileName), snapshot);
|
||||||
|
lines.push(`- Snapshot: ${fileName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const image of response.images()) {
|
||||||
|
const fileName = `${prefix}.screenshot.${extension(image.contentType)}`;
|
||||||
|
await fs.promises.writeFile(path.join(this._folder, fileName), image.data);
|
||||||
|
lines.push(`- Screenshot: ${fileName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('', '');
|
||||||
|
await fs.promises.appendFile(this._file, lines.join('\n'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extension(contentType: string): 'jpg' | 'png' {
|
||||||
|
if (contentType === 'image/jpeg')
|
||||||
|
return 'jpg';
|
||||||
|
return 'png';
|
||||||
|
}
|
@ -51,8 +51,6 @@ const goBack = defineTabTool({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handle: async (tab, params, response) => {
|
handle: async (tab, params, response) => {
|
||||||
response.setIncludeSnapshot();
|
|
||||||
|
|
||||||
await tab.page.goBack();
|
await tab.page.goBack();
|
||||||
response.setIncludeSnapshot();
|
response.setIncludeSnapshot();
|
||||||
response.addCode(`// Navigate back`);
|
response.addCode(`// Navigate back`);
|
||||||
@ -70,8 +68,6 @@ const goForward = defineTabTool({
|
|||||||
type: 'readOnly',
|
type: 'readOnly',
|
||||||
},
|
},
|
||||||
handle: async (tab, params, response) => {
|
handle: async (tab, params, response) => {
|
||||||
response.setIncludeSnapshot();
|
|
||||||
|
|
||||||
await tab.page.goForward();
|
await tab.page.goForward();
|
||||||
response.setIncludeSnapshot();
|
response.setIncludeSnapshot();
|
||||||
response.addCode(`// Navigate forward`);
|
response.addCode(`// Navigate forward`);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user