Skip to main content
Glama

Playwright MCP

sessionLog.ts5.26 kB
/** * 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 { Response } from './response.js'; import { logUnhandledError } from './log.js'; import { outputFile } from './config.js'; import type { FullConfig } from './config.js'; import type * as actions from './actions.js'; import type { Tab, TabSnapshot } from './tab.js'; type LogEntry = { timestamp: number; toolCall?: { toolName: string; toolArgs: Record<string, any>; result: string; isError?: boolean; }; userAction?: actions.Action; code: string; tabSnapshot?: TabSnapshot; }; export class SessionLog { private _folder: string; private _file: string; private _ordinal = 0; private _pendingEntries: LogEntry[] = []; private _sessionFileQueue = Promise.resolve(); private _flushEntriesTimeout: NodeJS.Timeout | undefined; constructor(sessionFolder: string) { this._folder = sessionFolder; this._file = path.join(this._folder, 'session.md'); } static async create(config: FullConfig, rootPath: string | undefined): Promise<SessionLog> { const sessionFolder = await outputFile(config, rootPath, `session-${Date.now()}`); await fs.promises.mkdir(sessionFolder, { recursive: true }); // eslint-disable-next-line no-console console.error(`Session: ${sessionFolder}`); return new SessionLog(sessionFolder); } logResponse(response: Response) { const entry: LogEntry = { timestamp: performance.now(), toolCall: { toolName: response.toolName, toolArgs: response.toolArgs, result: response.result(), isError: response.isError(), }, code: response.code(), tabSnapshot: response.tabSnapshot(), }; this._appendEntry(entry); } logUserAction(action: actions.Action, tab: Tab, code: string, isUpdate: boolean) { code = code.trim(); if (isUpdate) { const lastEntry = this._pendingEntries[this._pendingEntries.length - 1]; if (lastEntry.userAction?.name === action.name) { lastEntry.userAction = action; lastEntry.code = code; return; } } if (action.name === 'navigate') { // Already logged at this location. const lastEntry = this._pendingEntries[this._pendingEntries.length - 1]; if (lastEntry?.tabSnapshot?.url === action.url) return; } const entry: LogEntry = { timestamp: performance.now(), userAction: action, code, tabSnapshot: { url: tab.page.url(), title: '', ariaSnapshot: action.ariaSnapshot || '', modalStates: [], consoleMessages: [], downloads: [], }, }; this._appendEntry(entry); } private _appendEntry(entry: LogEntry) { this._pendingEntries.push(entry); if (this._flushEntriesTimeout) clearTimeout(this._flushEntriesTimeout); this._flushEntriesTimeout = setTimeout(() => this._flushEntries(), 1000); } private async _flushEntries() { clearTimeout(this._flushEntriesTimeout); const entries = this._pendingEntries; this._pendingEntries = []; const lines: string[] = ['']; for (const entry of entries) { const ordinal = (++this._ordinal).toString().padStart(3, '0'); if (entry.toolCall) { lines.push( `### Tool call: ${entry.toolCall.toolName}`, `- Args`, '```json', JSON.stringify(entry.toolCall.toolArgs, null, 2), '```', ); if (entry.toolCall.result) { lines.push( entry.toolCall.isError ? `- Error` : `- Result`, '```', entry.toolCall.result, '```', ); } } if (entry.userAction) { const actionData = { ...entry.userAction } as any; delete actionData.ariaSnapshot; delete actionData.selector; delete actionData.signals; lines.push( `### User action: ${entry.userAction.name}`, `- Args`, '```json', JSON.stringify(actionData, null, 2), '```', ); } if (entry.code) { lines.push( `- Code`, '```js', entry.code, '```'); } if (entry.tabSnapshot) { const fileName = `${ordinal}.snapshot.yml`; fs.promises.writeFile(path.join(this._folder, fileName), entry.tabSnapshot.ariaSnapshot).catch(logUnhandledError); lines.push(`- Snapshot: ${fileName}`); } lines.push('', ''); } this._sessionFileQueue = this._sessionFileQueue.then(() => fs.promises.appendFile(this._file, lines.join('\n'))); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/maywzh/playwright-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server