mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2025-07-23 22:22:28 +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"
|
||||
--proxy-server <proxy> specify proxy server, for example
|
||||
"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
|
||||
session into the output directory.
|
||||
--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[];
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
@ -42,6 +42,7 @@ export type CLIOptions = {
|
||||
port?: number;
|
||||
proxyBypass?: string;
|
||||
proxyServer?: string;
|
||||
saveSession?: boolean;
|
||||
saveTrace?: boolean;
|
||||
storageState?: string;
|
||||
userAgent?: string;
|
||||
@ -189,6 +190,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
|
||||
allowedOrigins: cliOptions.allowedOrigins,
|
||||
blockedOrigins: cliOptions.blockedOrigins,
|
||||
},
|
||||
saveSession: cliOptions.saveSession,
|
||||
saveTrace: cliOptions.saveTrace,
|
||||
outputDir: cliOptions.outputDir,
|
||||
imageResponses: cliOptions.imageResponses,
|
||||
|
@ -22,12 +22,11 @@ import { Context } from './context.js';
|
||||
import { Response } from './response.js';
|
||||
import { allTools } from './tools.js';
|
||||
import { packageJSON } from './package.js';
|
||||
|
||||
import { FullConfig } from './config.js';
|
||||
|
||||
import { SessionLog } from './sessionLog.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 context = new Context(tools, config, browserContextFactory);
|
||||
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 () => {
|
||||
return {
|
||||
tools: tools.map(tool => ({
|
||||
@ -62,8 +63,10 @@ export function createConnection(config: FullConfig, browserContextFactory: Brow
|
||||
return errorResult(`Tool "${request.params.name}" not found`);
|
||||
|
||||
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);
|
||||
if (sessionLog)
|
||||
await sessionLog.log(response);
|
||||
return await response.serialize();
|
||||
} catch (error) {
|
||||
return errorResult(String(error));
|
||||
|
@ -46,6 +46,7 @@ program
|
||||
.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-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('--storage-state <path>', 'path to the storage state file for isolated sessions.')
|
||||
.option('--user-agent <ua string>', 'specify user agent string')
|
||||
|
@ -14,7 +14,7 @@
|
||||
* 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';
|
||||
|
||||
export class Response {
|
||||
@ -24,23 +24,41 @@ export class Response {
|
||||
private _context: Context;
|
||||
private _includeSnapshot = 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.toolName = toolName;
|
||||
this.toolArgs = toolArgs;
|
||||
}
|
||||
|
||||
addResult(result: string) {
|
||||
this._result.push(result);
|
||||
}
|
||||
|
||||
result() {
|
||||
return this._result.join('\n');
|
||||
}
|
||||
|
||||
addCode(code: string) {
|
||||
this._code.push(code);
|
||||
}
|
||||
|
||||
code() {
|
||||
return this._code.join('\n');
|
||||
}
|
||||
|
||||
addImage(image: { contentType: string, data: Buffer }) {
|
||||
this._images.push(image);
|
||||
}
|
||||
|
||||
images() {
|
||||
return this._images;
|
||||
}
|
||||
|
||||
setIncludeSnapshot() {
|
||||
this._includeSnapshot = true;
|
||||
}
|
||||
@ -49,8 +67,14 @@ export class Response {
|
||||
this._includeTabs = true;
|
||||
}
|
||||
|
||||
includeSnapshot() {
|
||||
return this._includeSnapshot;
|
||||
async snapshot(): Promise<string> {
|
||||
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)[] }> {
|
||||
@ -77,8 +101,9 @@ ${this._code.join('\n')}
|
||||
response.push(...(await this._context.listTabsMarkdown(this._includeTabs)));
|
||||
|
||||
// Add snapshot if provided.
|
||||
if (this._includeSnapshot && this._context.currentTab())
|
||||
response.push(await this._context.currentTabOrDie().captureSnapshot(), '');
|
||||
const snapshot = await this.snapshot();
|
||||
if (snapshot)
|
||||
response.push(snapshot, '');
|
||||
|
||||
// Main response part
|
||||
const content: (TextContent | ImageContent)[] = [
|
||||
|
@ -35,7 +35,7 @@ export class Server {
|
||||
}
|
||||
|
||||
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);
|
||||
await connection.server.connect(transport);
|
||||
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) => {
|
||||
response.setIncludeSnapshot();
|
||||
|
||||
await tab.page.goBack();
|
||||
response.setIncludeSnapshot();
|
||||
response.addCode(`// Navigate back`);
|
||||
@ -70,8 +68,6 @@ const goForward = defineTabTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
handle: async (tab, params, response) => {
|
||||
response.setIncludeSnapshot();
|
||||
|
||||
await tab.page.goForward();
|
||||
response.setIncludeSnapshot();
|
||||
response.addCode(`// Navigate forward`);
|
||||
|
Loading…
x
Reference in New Issue
Block a user