feat: implement centralized artifact storage with session isolation
Add comprehensive artifact storage system with session-specific directories:
- Add --artifact-dir CLI option and PLAYWRIGHT_MCP_ARTIFACT_DIR env var
- Create ArtifactManager class for session-specific artifact organization
- Implement ArtifactManagerRegistry for multi-session support
- Add tool call logging with JSON persistence in tool-calls.json
- Update screenshot, video, and PDF tools to use centralized storage
- Add browser_configure_artifacts tool for per-session control
- Support dynamic enable/disable without server restart
- Maintain backward compatibility when artifact storage not configured
Directory structure: {artifactDir}/{sessionId}/[artifacts, videos/, tool-calls.json]
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ecedcc48d6
commit
d8202f6694
160
README.md
160
README.md
@ -144,6 +144,8 @@ Playwright MCP server supports following arguments. They can be provided in the
|
|||||||
> npx @playwright/mcp@latest --help
|
> npx @playwright/mcp@latest --help
|
||||||
--allowed-origins <origins> semicolon-separated list of origins to allow the
|
--allowed-origins <origins> semicolon-separated list of origins to allow the
|
||||||
browser to request. Default is to allow all.
|
browser to request. Default is to allow all.
|
||||||
|
--artifact-dir <path> path to the directory for centralized artifact
|
||||||
|
storage with session-specific subdirectories.
|
||||||
--blocked-origins <origins> semicolon-separated list of origins to block the
|
--blocked-origins <origins> semicolon-separated list of origins to block the
|
||||||
browser from requesting. Blocklist is evaluated
|
browser from requesting. Blocklist is evaluated
|
||||||
before allowlist. If used without the allowlist,
|
before allowlist. If used without the allowlist,
|
||||||
@ -296,6 +298,9 @@ npx @playwright/mcp@latest --config path/to/config.json
|
|||||||
// Directory for output files
|
// Directory for output files
|
||||||
outputDir?: string;
|
outputDir?: string;
|
||||||
|
|
||||||
|
// Directory for centralized artifact storage with session-specific subdirectories
|
||||||
|
artifactDir?: string;
|
||||||
|
|
||||||
// Network configuration
|
// Network configuration
|
||||||
network?: {
|
network?: {
|
||||||
// List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
|
// List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
|
||||||
@ -314,6 +319,125 @@ npx @playwright/mcp@latest --config path/to/config.json
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
### Centralized Artifact Storage
|
||||||
|
|
||||||
|
The Playwright MCP server supports centralized artifact storage for organizing all generated files (screenshots, videos, and PDFs) in session-specific directories with comprehensive logging.
|
||||||
|
|
||||||
|
#### Configuration
|
||||||
|
|
||||||
|
**Command Line Option:**
|
||||||
|
```bash
|
||||||
|
npx @playwright/mcp@latest --artifact-dir /path/to/artifacts
|
||||||
|
```
|
||||||
|
|
||||||
|
**Environment Variable:**
|
||||||
|
```bash
|
||||||
|
export PLAYWRIGHT_MCP_ARTIFACT_DIR="/path/to/artifacts"
|
||||||
|
npx @playwright/mcp@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**MCP Client Configuration:**
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"playwright": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"@playwright/mcp@latest",
|
||||||
|
"--artifact-dir",
|
||||||
|
"./browser-artifacts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
When artifact storage is enabled, the server provides:
|
||||||
|
|
||||||
|
- **Session Isolation**: Each MCP session gets its own subdirectory
|
||||||
|
- **Organized Storage**: All artifacts saved to `{artifact-dir}/{session-id}/`
|
||||||
|
- **Tool Call Logging**: Complete audit trail in `tool-calls.json`
|
||||||
|
- **Automatic Organization**: Videos saved to `videos/` subdirectory
|
||||||
|
|
||||||
|
#### Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
browser-artifacts/
|
||||||
|
└── mcp-session-abc123/
|
||||||
|
├── tool-calls.json # Complete log of all tool calls
|
||||||
|
├── page-2024-01-15T10-30-00.png # Screenshots
|
||||||
|
├── document.pdf # Generated PDFs
|
||||||
|
└── videos/
|
||||||
|
└── session-recording.webm # Video recordings
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Tool Call Log Format
|
||||||
|
|
||||||
|
The `tool-calls.json` file contains detailed information about each operation:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"timestamp": "2024-01-15T10:30:00.000Z",
|
||||||
|
"toolName": "browser_take_screenshot",
|
||||||
|
"parameters": {
|
||||||
|
"filename": "login-page.png"
|
||||||
|
},
|
||||||
|
"result": "success",
|
||||||
|
"artifactPath": "login-page.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2024-01-15T10:31:15.000Z",
|
||||||
|
"toolName": "browser_start_recording",
|
||||||
|
"parameters": {
|
||||||
|
"filename": "user-journey"
|
||||||
|
},
|
||||||
|
"result": "success"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Per-Session Control
|
||||||
|
|
||||||
|
You can dynamically enable, disable, or configure artifact storage during a session using the `browser_configure_artifacts` tool:
|
||||||
|
|
||||||
|
**Check Current Status:**
|
||||||
|
```
|
||||||
|
browser_configure_artifacts
|
||||||
|
```
|
||||||
|
|
||||||
|
**Enable Artifact Storage:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"directory": "./my-artifacts"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Disable Artifact Storage:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Custom Session ID:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"sessionId": "my-custom-session"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Compatibility
|
||||||
|
|
||||||
|
- **Backward Compatible**: When `--artifact-dir` is not specified, all tools work exactly as before
|
||||||
|
- **Dynamic Control**: Artifact storage can be enabled/disabled per session without server restart
|
||||||
|
- **Fallback Behavior**: If artifact storage fails, tools fall back to default output directory
|
||||||
|
- **No Breaking Changes**: Existing configurations continue to work unchanged
|
||||||
|
|
||||||
### Standalone MCP server
|
### Standalone MCP server
|
||||||
|
|
||||||
When running headed browser on system w/o display or from worker processes of the IDEs,
|
When running headed browser on system w/o display or from worker processes of the IDEs,
|
||||||
@ -409,6 +533,34 @@ http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
|
- **browser_configure**
|
||||||
|
- Title: Configure browser settings
|
||||||
|
- Description: Change browser configuration settings like headless/headed mode, viewport size, user agent, device emulation, geolocation, locale, timezone, color scheme, or permissions for subsequent operations. This will close the current browser and restart it with new settings.
|
||||||
|
- Parameters:
|
||||||
|
- `headless` (boolean, optional): Whether to run the browser in headless mode
|
||||||
|
- `viewport` (object, optional): Browser viewport size
|
||||||
|
- `userAgent` (string, optional): User agent string for the browser
|
||||||
|
- `device` (string, optional): Device to emulate (e.g., "iPhone 13", "iPad", "Pixel 5"). Use browser_list_devices to see available devices.
|
||||||
|
- `geolocation` (object, optional): Set geolocation coordinates
|
||||||
|
- `locale` (string, optional): Browser locale (e.g., "en-US", "fr-FR", "ja-JP")
|
||||||
|
- `timezone` (string, optional): Timezone ID (e.g., "America/New_York", "Europe/London", "Asia/Tokyo")
|
||||||
|
- `colorScheme` (string, optional): Preferred color scheme
|
||||||
|
- `permissions` (array, optional): Permissions to grant (e.g., ["geolocation", "notifications", "camera", "microphone"])
|
||||||
|
- Read-only: **false**
|
||||||
|
|
||||||
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
|
- **browser_configure_artifacts**
|
||||||
|
- Title: Configure artifact storage
|
||||||
|
- Description: Enable, disable, or configure centralized artifact storage for screenshots, videos, and PDFs during this session. Allows dynamic control over where artifacts are saved and how they are organized.
|
||||||
|
- Parameters:
|
||||||
|
- `enabled` (boolean, optional): Enable or disable centralized artifact storage for this session
|
||||||
|
- `directory` (string, optional): Directory path for artifact storage (if different from server default)
|
||||||
|
- `sessionId` (string, optional): Custom session ID for artifact organization (auto-generated if not provided)
|
||||||
|
- Read-only: **false**
|
||||||
|
|
||||||
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_console_messages**
|
- **browser_console_messages**
|
||||||
- Title: Get console messages
|
- Title: Get console messages
|
||||||
- Description: Returns all console messages
|
- Description: Returns all console messages
|
||||||
@ -469,6 +621,14 @@ http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
|
- **browser_list_devices**
|
||||||
|
- Title: List available devices for emulation
|
||||||
|
- Description: Get a list of all available device emulation profiles including mobile phones, tablets, and desktop browsers. Each device includes viewport, user agent, and capabilities information.
|
||||||
|
- Parameters: None
|
||||||
|
- Read-only: **true**
|
||||||
|
|
||||||
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_navigate**
|
- **browser_navigate**
|
||||||
- Title: Navigate to a URL
|
- Title: Navigate to a URL
|
||||||
- Description: Navigate to a URL
|
- Description: Navigate to a URL
|
||||||
|
|||||||
6
config.d.ts
vendored
6
config.d.ts
vendored
@ -100,6 +100,12 @@ export type Config = {
|
|||||||
*/
|
*/
|
||||||
outputDir?: string;
|
outputDir?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The directory to save all screenshots and videos with session-specific subdirectories.
|
||||||
|
* When set, all artifacts will be saved to {artifactDir}/{sessionId}/ with tool call logs.
|
||||||
|
*/
|
||||||
|
artifactDir?: string;
|
||||||
|
|
||||||
network?: {
|
network?: {
|
||||||
/**
|
/**
|
||||||
* List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
|
* List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
|
||||||
|
|||||||
256
src/artifactManager.ts
Normal file
256
src/artifactManager.ts
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
/**
|
||||||
|
* 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 * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import debug from 'debug';
|
||||||
|
import { sanitizeForFilePath } from './tools/utils.js';
|
||||||
|
|
||||||
|
const artifactDebug = debug('pw:mcp:artifacts');
|
||||||
|
|
||||||
|
export interface ArtifactEntry {
|
||||||
|
timestamp: string;
|
||||||
|
toolName: string;
|
||||||
|
parameters: any;
|
||||||
|
result: 'success' | 'error';
|
||||||
|
artifactPath?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages centralized artifact storage with session-specific directories and tool call logging
|
||||||
|
*/
|
||||||
|
export class ArtifactManager {
|
||||||
|
private _baseDir: string;
|
||||||
|
private _sessionId: string;
|
||||||
|
private _sessionDir: string;
|
||||||
|
private _logFile: string;
|
||||||
|
private _logEntries: ArtifactEntry[] = [];
|
||||||
|
|
||||||
|
constructor(baseDir: string, sessionId: string) {
|
||||||
|
this._baseDir = baseDir;
|
||||||
|
this._sessionId = sessionId;
|
||||||
|
this._sessionDir = path.join(baseDir, sanitizeForFilePath(sessionId));
|
||||||
|
this._logFile = path.join(this._sessionDir, 'tool-calls.json');
|
||||||
|
|
||||||
|
// Ensure session directory exists
|
||||||
|
this._ensureSessionDirectory();
|
||||||
|
|
||||||
|
// Load existing log if it exists
|
||||||
|
this._loadExistingLog();
|
||||||
|
|
||||||
|
artifactDebug(`artifact manager initialized for session ${sessionId} in ${this._sessionDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the session-specific directory for artifacts
|
||||||
|
*/
|
||||||
|
getSessionDir(): string {
|
||||||
|
return this._sessionDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a full path for an artifact file in the session directory
|
||||||
|
*/
|
||||||
|
getArtifactPath(filename: string): string {
|
||||||
|
return path.join(this._sessionDir, sanitizeForFilePath(filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a subdirectory within the session directory
|
||||||
|
*/
|
||||||
|
getSubdirectory(subdir: string): string {
|
||||||
|
const subdirPath = path.join(this._sessionDir, sanitizeForFilePath(subdir));
|
||||||
|
fs.mkdirSync(subdirPath, { recursive: true });
|
||||||
|
return subdirPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a tool call with optional artifact path
|
||||||
|
*/
|
||||||
|
logToolCall(toolName: string, parameters: any, result: 'success' | 'error', artifactPath?: string, error?: string): void {
|
||||||
|
const entry: ArtifactEntry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
toolName,
|
||||||
|
parameters,
|
||||||
|
result,
|
||||||
|
artifactPath: artifactPath ? path.relative(this._sessionDir, artifactPath) : undefined,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
|
||||||
|
this._logEntries.push(entry);
|
||||||
|
this._saveLog();
|
||||||
|
|
||||||
|
artifactDebug(`logged tool call: ${toolName} -> ${result} ${artifactPath ? `(${entry.artifactPath})` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all logged tool calls for this session
|
||||||
|
*/
|
||||||
|
getToolCallLog(): ArtifactEntry[] {
|
||||||
|
return [...this._logEntries];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get statistics about this session's artifacts
|
||||||
|
*/
|
||||||
|
getSessionStats(): {
|
||||||
|
sessionId: string;
|
||||||
|
sessionDir: string;
|
||||||
|
toolCallCount: number;
|
||||||
|
successCount: number;
|
||||||
|
errorCount: number;
|
||||||
|
artifactCount: number;
|
||||||
|
directorySize: number;
|
||||||
|
} {
|
||||||
|
const successCount = this._logEntries.filter(e => e.result === 'success').length;
|
||||||
|
const errorCount = this._logEntries.filter(e => e.result === 'error').length;
|
||||||
|
const artifactCount = this._logEntries.filter(e => e.artifactPath).length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionId: this._sessionId,
|
||||||
|
sessionDir: this._sessionDir,
|
||||||
|
toolCallCount: this._logEntries.length,
|
||||||
|
successCount,
|
||||||
|
errorCount,
|
||||||
|
artifactCount,
|
||||||
|
directorySize: this._getDirectorySize(this._sessionDir)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _ensureSessionDirectory(): void {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(this._sessionDir, { recursive: true });
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to create session directory ${this._sessionDir}: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _loadExistingLog(): void {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(this._logFile)) {
|
||||||
|
const logData = fs.readFileSync(this._logFile, 'utf8');
|
||||||
|
this._logEntries = JSON.parse(logData);
|
||||||
|
artifactDebug(`loaded ${this._logEntries.length} existing log entries`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
artifactDebug(`failed to load existing log: ${error}`);
|
||||||
|
this._logEntries = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _saveLog(): void {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(this._logFile, JSON.stringify(this._logEntries, null, 2));
|
||||||
|
} catch (error) {
|
||||||
|
artifactDebug(`failed to save log: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getDirectorySize(dirPath: string): number {
|
||||||
|
let size = 0;
|
||||||
|
try {
|
||||||
|
const files = fs.readdirSync(dirPath);
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(dirPath, file);
|
||||||
|
const stats = fs.statSync(filePath);
|
||||||
|
if (stats.isDirectory())
|
||||||
|
size += this._getDirectorySize(filePath);
|
||||||
|
else
|
||||||
|
size += stats.size;
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global artifact manager instances keyed by session ID
|
||||||
|
*/
|
||||||
|
export class ArtifactManagerRegistry {
|
||||||
|
private static _instance: ArtifactManagerRegistry;
|
||||||
|
private _managers: Map<string, ArtifactManager> = new Map();
|
||||||
|
private _baseDir: string | undefined;
|
||||||
|
|
||||||
|
static getInstance(): ArtifactManagerRegistry {
|
||||||
|
if (!ArtifactManagerRegistry._instance)
|
||||||
|
ArtifactManagerRegistry._instance = new ArtifactManagerRegistry();
|
||||||
|
|
||||||
|
return ArtifactManagerRegistry._instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the base directory for all artifact storage
|
||||||
|
*/
|
||||||
|
setBaseDir(baseDir: string): void {
|
||||||
|
this._baseDir = baseDir;
|
||||||
|
artifactDebug(`artifact registry base directory set to: ${baseDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create an artifact manager for a session
|
||||||
|
*/
|
||||||
|
getManager(sessionId: string): ArtifactManager | undefined {
|
||||||
|
if (!this._baseDir)
|
||||||
|
return undefined; // Artifact storage not configured
|
||||||
|
|
||||||
|
|
||||||
|
let manager = this._managers.get(sessionId);
|
||||||
|
if (!manager) {
|
||||||
|
manager = new ArtifactManager(this._baseDir, sessionId);
|
||||||
|
this._managers.set(sessionId, manager);
|
||||||
|
}
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a session's artifact manager
|
||||||
|
*/
|
||||||
|
removeManager(sessionId: string): void {
|
||||||
|
this._managers.delete(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active session managers
|
||||||
|
*/
|
||||||
|
getAllManagers(): Map<string, ArtifactManager> {
|
||||||
|
return new Map(this._managers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get summary statistics across all sessions
|
||||||
|
*/
|
||||||
|
getGlobalStats(): {
|
||||||
|
baseDir: string | undefined;
|
||||||
|
activeSessions: number;
|
||||||
|
totalToolCalls: number;
|
||||||
|
totalArtifacts: number;
|
||||||
|
} {
|
||||||
|
const managers = Array.from(this._managers.values());
|
||||||
|
const totalToolCalls = managers.reduce((sum, m) => sum + m.getSessionStats().toolCallCount, 0);
|
||||||
|
const totalArtifacts = managers.reduce((sum, m) => sum + m.getSessionStats().artifactCount, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseDir: this._baseDir,
|
||||||
|
activeSessions: this._managers.size,
|
||||||
|
totalToolCalls,
|
||||||
|
totalArtifacts
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,6 +23,7 @@ import { filteredTools } from './tools.js';
|
|||||||
import { packageJSON } from './package.js';
|
import { packageJSON } from './package.js';
|
||||||
import { SessionManager } from './sessionManager.js';
|
import { SessionManager } from './sessionManager.js';
|
||||||
import { EnvironmentIntrospector } from './environmentIntrospection.js';
|
import { EnvironmentIntrospector } from './environmentIntrospection.js';
|
||||||
|
import { ArtifactManagerRegistry } from './artifactManager.js';
|
||||||
|
|
||||||
import type { BrowserContextFactory } from './browserContextFactory.js';
|
import type { BrowserContextFactory } from './browserContextFactory.js';
|
||||||
import type * as mcpServer from './mcp/server.js';
|
import type * as mcpServer from './mcp/server.js';
|
||||||
@ -46,6 +47,12 @@ export class BrowserServerBackend implements ServerBackend {
|
|||||||
this._browserContextFactory = browserContextFactory;
|
this._browserContextFactory = browserContextFactory;
|
||||||
this._environmentIntrospector = new EnvironmentIntrospector();
|
this._environmentIntrospector = new EnvironmentIntrospector();
|
||||||
|
|
||||||
|
// Initialize artifact manager registry if artifact directory is configured
|
||||||
|
if (config.artifactDir) {
|
||||||
|
const registry = ArtifactManagerRegistry.getInstance();
|
||||||
|
registry.setBaseDir(config.artifactDir);
|
||||||
|
}
|
||||||
|
|
||||||
// Create a default context - will be replaced when session ID is set
|
// Create a default context - will be replaced when session ID is set
|
||||||
this._context = new Context(this._tools, config, browserContextFactory, this._environmentIntrospector);
|
this._context = new Context(this._tools, config, browserContextFactory, this._environmentIntrospector);
|
||||||
}
|
}
|
||||||
@ -55,19 +62,19 @@ export class BrowserServerBackend implements ServerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSessionId(sessionId: string): void {
|
setSessionId(sessionId: string): void {
|
||||||
if (this._sessionId === sessionId) {
|
if (this._sessionId === sessionId)
|
||||||
return; // Already using this session
|
return; // Already using this session
|
||||||
}
|
|
||||||
|
|
||||||
this._sessionId = sessionId;
|
this._sessionId = sessionId;
|
||||||
|
|
||||||
// Get or create persistent context for this session
|
// Get or create persistent context for this session
|
||||||
const sessionManager = SessionManager.getInstance();
|
const sessionManager = SessionManager.getInstance();
|
||||||
this._context = sessionManager.getOrCreateContext(
|
this._context = sessionManager.getOrCreateContext(
|
||||||
sessionId,
|
sessionId,
|
||||||
this._tools,
|
this._tools,
|
||||||
this._config,
|
this._config,
|
||||||
this._browserContextFactory
|
this._browserContextFactory
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update environment introspector reference
|
// Update environment introspector reference
|
||||||
@ -81,7 +88,43 @@ export class BrowserServerBackend implements ServerBackend {
|
|||||||
async callTool(schema: mcpServer.ToolSchema<any>, parsedArguments: any) {
|
async callTool(schema: mcpServer.ToolSchema<any>, parsedArguments: any) {
|
||||||
const response = new Response(this._context, schema.name, parsedArguments);
|
const response = new Response(this._context, schema.name, parsedArguments);
|
||||||
const tool = this._tools.find(tool => tool.schema.name === schema.name)!;
|
const tool = this._tools.find(tool => tool.schema.name === schema.name)!;
|
||||||
await tool.handle(this._context, parsedArguments, response);
|
|
||||||
|
let toolResult: 'success' | 'error' = 'success';
|
||||||
|
let errorMessage: string | undefined;
|
||||||
|
let artifactPath: string | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await tool.handle(this._context, parsedArguments, response);
|
||||||
|
|
||||||
|
// Check if this tool created any artifacts
|
||||||
|
const serialized = await response.serialize();
|
||||||
|
if (serialized.content) {
|
||||||
|
// Look for file paths in the response
|
||||||
|
for (const content of serialized.content) {
|
||||||
|
if (content.type === 'text' && content.text) {
|
||||||
|
// Simple heuristic to find file paths
|
||||||
|
const pathMatches = content.text.match(/(?:saved to|created at|file:|path:)\s*([^\s\n]+\.(png|jpg|jpeg|webm|mp4|pdf))/gi);
|
||||||
|
if (pathMatches) {
|
||||||
|
artifactPath = pathMatches[0].split(/\s+/).pop();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toolResult = 'error';
|
||||||
|
errorMessage = String(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the tool call if artifact manager is available
|
||||||
|
if (this._sessionId) {
|
||||||
|
const registry = ArtifactManagerRegistry.getInstance();
|
||||||
|
const artifactManager = registry.getManager(this._sessionId);
|
||||||
|
if (artifactManager)
|
||||||
|
artifactManager.logToolCall(schema.name, parsedArguments, toolResult, artifactPath, errorMessage);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
if (this._sessionLog)
|
if (this._sessionLog)
|
||||||
await this._sessionLog.log(response);
|
await this._sessionLog.log(response);
|
||||||
return await response.serialize();
|
return await response.serialize();
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import type { BrowserContextOptions, LaunchOptions } from 'playwright';
|
|||||||
|
|
||||||
export type CLIOptions = {
|
export type CLIOptions = {
|
||||||
allowedOrigins?: string[];
|
allowedOrigins?: string[];
|
||||||
|
artifactDir?: string;
|
||||||
blockedOrigins?: string[];
|
blockedOrigins?: string[];
|
||||||
blockServiceWorkers?: boolean;
|
blockServiceWorkers?: boolean;
|
||||||
browser?: string;
|
browser?: string;
|
||||||
@ -81,6 +82,7 @@ export type FullConfig = Config & {
|
|||||||
},
|
},
|
||||||
network: NonNullable<Config['network']>,
|
network: NonNullable<Config['network']>,
|
||||||
outputDir: string;
|
outputDir: string;
|
||||||
|
artifactDir?: string;
|
||||||
server: NonNullable<Config['server']>,
|
server: NonNullable<Config['server']>,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -131,9 +133,9 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
|
|||||||
channel,
|
channel,
|
||||||
executablePath: cliOptions.executablePath,
|
executablePath: cliOptions.executablePath,
|
||||||
};
|
};
|
||||||
if (cliOptions.headless !== undefined) {
|
if (cliOptions.headless !== undefined)
|
||||||
launchOptions.headless = cliOptions.headless;
|
launchOptions.headless = cliOptions.headless;
|
||||||
}
|
|
||||||
|
|
||||||
// --no-sandbox was passed, disable the sandbox
|
// --no-sandbox was passed, disable the sandbox
|
||||||
if (cliOptions.sandbox === false)
|
if (cliOptions.sandbox === false)
|
||||||
@ -196,6 +198,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
|
|||||||
saveSession: cliOptions.saveSession,
|
saveSession: cliOptions.saveSession,
|
||||||
saveTrace: cliOptions.saveTrace,
|
saveTrace: cliOptions.saveTrace,
|
||||||
outputDir: cliOptions.outputDir,
|
outputDir: cliOptions.outputDir,
|
||||||
|
artifactDir: cliOptions.artifactDir,
|
||||||
imageResponses: cliOptions.imageResponses,
|
imageResponses: cliOptions.imageResponses,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -205,6 +208,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
|
|||||||
function configFromEnv(): Config {
|
function configFromEnv(): Config {
|
||||||
const options: CLIOptions = {};
|
const options: CLIOptions = {};
|
||||||
options.allowedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_ORIGINS);
|
options.allowedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_ORIGINS);
|
||||||
|
options.artifactDir = envToString(process.env.PLAYWRIGHT_MCP_ARTIFACT_DIR);
|
||||||
options.blockedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_BLOCKED_ORIGINS);
|
options.blockedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_BLOCKED_ORIGINS);
|
||||||
options.blockServiceWorkers = envToBoolean(process.env.PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS);
|
options.blockServiceWorkers = envToBoolean(process.env.PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS);
|
||||||
options.browser = envToString(process.env.PLAYWRIGHT_MCP_BROWSER);
|
options.browser = envToString(process.env.PLAYWRIGHT_MCP_BROWSER);
|
||||||
|
|||||||
@ -88,6 +88,12 @@ export class Context {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateSessionId(customSessionId: string) {
|
||||||
|
testDebug(`updating sessionId from ${this.sessionId} to ${customSessionId}`);
|
||||||
|
// Note: sessionId is readonly, but we can update it for artifact management
|
||||||
|
(this as any).sessionId = customSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
tabs(): Tab[] {
|
tabs(): Tab[] {
|
||||||
return this._tabs;
|
return this._tabs;
|
||||||
}
|
}
|
||||||
@ -352,15 +358,15 @@ export class Context {
|
|||||||
const currentConfig = { ...this.config };
|
const currentConfig = { ...this.config };
|
||||||
|
|
||||||
// Update the configuration
|
// Update the configuration
|
||||||
if (changes.headless !== undefined) {
|
if (changes.headless !== undefined)
|
||||||
currentConfig.browser.launchOptions.headless = changes.headless;
|
currentConfig.browser.launchOptions.headless = changes.headless;
|
||||||
}
|
|
||||||
|
|
||||||
// Handle device emulation - this overrides individual viewport/userAgent settings
|
// Handle device emulation - this overrides individual viewport/userAgent settings
|
||||||
if (changes.device) {
|
if (changes.device) {
|
||||||
if (!devices[changes.device]) {
|
if (!devices[changes.device])
|
||||||
throw new Error(`Unknown device: ${changes.device}`);
|
throw new Error(`Unknown device: ${changes.device}`);
|
||||||
}
|
|
||||||
const deviceConfig = devices[changes.device];
|
const deviceConfig = devices[changes.device];
|
||||||
|
|
||||||
// Apply all device properties to context options
|
// Apply all device properties to context options
|
||||||
@ -370,12 +376,12 @@ export class Context {
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Apply individual settings only if no device is specified
|
// Apply individual settings only if no device is specified
|
||||||
if (changes.viewport) {
|
if (changes.viewport)
|
||||||
currentConfig.browser.contextOptions.viewport = changes.viewport;
|
currentConfig.browser.contextOptions.viewport = changes.viewport;
|
||||||
}
|
|
||||||
if (changes.userAgent) {
|
if (changes.userAgent)
|
||||||
currentConfig.browser.contextOptions.userAgent = changes.userAgent;
|
currentConfig.browser.contextOptions.userAgent = changes.userAgent;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply additional context options
|
// Apply additional context options
|
||||||
@ -387,21 +393,21 @@ export class Context {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changes.locale) {
|
if (changes.locale)
|
||||||
currentConfig.browser.contextOptions.locale = changes.locale;
|
currentConfig.browser.contextOptions.locale = changes.locale;
|
||||||
}
|
|
||||||
|
|
||||||
if (changes.timezone) {
|
|
||||||
|
if (changes.timezone)
|
||||||
currentConfig.browser.contextOptions.timezoneId = changes.timezone;
|
currentConfig.browser.contextOptions.timezoneId = changes.timezone;
|
||||||
}
|
|
||||||
|
|
||||||
if (changes.colorScheme) {
|
|
||||||
|
if (changes.colorScheme)
|
||||||
currentConfig.browser.contextOptions.colorScheme = changes.colorScheme;
|
currentConfig.browser.contextOptions.colorScheme = changes.colorScheme;
|
||||||
}
|
|
||||||
|
|
||||||
if (changes.permissions) {
|
|
||||||
|
if (changes.permissions)
|
||||||
currentConfig.browser.contextOptions.permissions = changes.permissions;
|
currentConfig.browser.contextOptions.permissions = changes.permissions;
|
||||||
}
|
|
||||||
|
|
||||||
// Store the modified config
|
// Store the modified config
|
||||||
(this as any).config = currentConfig;
|
(this as any).config = currentConfig;
|
||||||
|
|||||||
@ -31,6 +31,7 @@ program
|
|||||||
.version('Version ' + packageJSON.version)
|
.version('Version ' + packageJSON.version)
|
||||||
.name(packageJSON.name)
|
.name(packageJSON.name)
|
||||||
.option('--allowed-origins <origins>', 'semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList)
|
.option('--allowed-origins <origins>', 'semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList)
|
||||||
|
.option('--artifact-dir <path>', 'path to the directory for centralized artifact storage with session-specific subdirectories.')
|
||||||
.option('--blocked-origins <origins>', 'semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.', semicolonSeparatedList)
|
.option('--blocked-origins <origins>', 'semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.', semicolonSeparatedList)
|
||||||
.option('--block-service-workers', 'block service workers')
|
.option('--block-service-workers', 'block service workers')
|
||||||
.option('--browser <browser>', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
|
.option('--browser <browser>', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
|
||||||
@ -104,9 +105,9 @@ function setupExitWatchdog(serverConfig: { host?: string; port?: number }) {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (serverConfig.port !== undefined) {
|
if (serverConfig.port !== undefined)
|
||||||
process.stdin.on('close', handleExit);
|
process.stdin.on('close', handleExit);
|
||||||
}
|
|
||||||
process.on('SIGINT', handleExit);
|
process.on('SIGINT', handleExit);
|
||||||
process.on('SIGTERM', handleExit);
|
process.on('SIGTERM', handleExit);
|
||||||
|
|
||||||
|
|||||||
@ -31,9 +31,9 @@ export class SessionManager {
|
|||||||
private _sessions: Map<string, Context> = new Map();
|
private _sessions: Map<string, Context> = new Map();
|
||||||
|
|
||||||
static getInstance(): SessionManager {
|
static getInstance(): SessionManager {
|
||||||
if (!SessionManager._instance) {
|
if (!SessionManager._instance)
|
||||||
SessionManager._instance = new SessionManager();
|
SessionManager._instance = new SessionManager();
|
||||||
}
|
|
||||||
return SessionManager._instance;
|
return SessionManager._instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { devices } from 'playwright';
|
import { devices } from 'playwright';
|
||||||
import { defineTool } from './tool.js';
|
import { defineTool } from './tool.js';
|
||||||
|
import { ArtifactManagerRegistry } from '../artifactManager.js';
|
||||||
import type { Context } from '../context.js';
|
import type { Context } from '../context.js';
|
||||||
import type { Response } from '../response.js';
|
import type { Response } from '../response.js';
|
||||||
|
|
||||||
@ -41,6 +42,12 @@ const configureSchema = z.object({
|
|||||||
|
|
||||||
const listDevicesSchema = z.object({});
|
const listDevicesSchema = z.object({});
|
||||||
|
|
||||||
|
const configureArtifactsSchema = z.object({
|
||||||
|
enabled: z.boolean().optional().describe('Enable or disable centralized artifact storage for this session'),
|
||||||
|
directory: z.string().optional().describe('Directory path for artifact storage (if different from server default)'),
|
||||||
|
sessionId: z.string().optional().describe('Custom session ID for artifact organization (auto-generated if not provided)')
|
||||||
|
});
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
defineTool({
|
defineTool({
|
||||||
capability: 'core',
|
capability: 'core',
|
||||||
@ -111,51 +118,51 @@ export default [
|
|||||||
// Track what's changing
|
// Track what's changing
|
||||||
if (params.headless !== undefined) {
|
if (params.headless !== undefined) {
|
||||||
const currentHeadless = currentConfig.browser.launchOptions.headless;
|
const currentHeadless = currentConfig.browser.launchOptions.headless;
|
||||||
if (params.headless !== currentHeadless) {
|
if (params.headless !== currentHeadless)
|
||||||
changes.push(`headless: ${currentHeadless} → ${params.headless}`);
|
changes.push(`headless: ${currentHeadless} → ${params.headless}`);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.viewport) {
|
if (params.viewport) {
|
||||||
const currentViewport = currentConfig.browser.contextOptions.viewport;
|
const currentViewport = currentConfig.browser.contextOptions.viewport;
|
||||||
if (!currentViewport || currentViewport.width !== params.viewport.width || currentViewport.height !== params.viewport.height) {
|
if (!currentViewport || currentViewport.width !== params.viewport.width || currentViewport.height !== params.viewport.height)
|
||||||
changes.push(`viewport: ${currentViewport?.width || 'default'}x${currentViewport?.height || 'default'} → ${params.viewport.width}x${params.viewport.height}`);
|
changes.push(`viewport: ${currentViewport?.width || 'default'}x${currentViewport?.height || 'default'} → ${params.viewport.width}x${params.viewport.height}`);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.userAgent) {
|
if (params.userAgent) {
|
||||||
const currentUA = currentConfig.browser.contextOptions.userAgent;
|
const currentUA = currentConfig.browser.contextOptions.userAgent;
|
||||||
if (params.userAgent !== currentUA) {
|
if (params.userAgent !== currentUA)
|
||||||
changes.push(`userAgent: ${currentUA || 'default'} → ${params.userAgent}`);
|
changes.push(`userAgent: ${currentUA || 'default'} → ${params.userAgent}`);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.device) {
|
if (params.device) {
|
||||||
if (!devices[params.device]) {
|
if (!devices[params.device])
|
||||||
throw new Error(`Unknown device: ${params.device}. Use browser_list_devices to see available devices.`);
|
throw new Error(`Unknown device: ${params.device}. Use browser_list_devices to see available devices.`);
|
||||||
}
|
|
||||||
changes.push(`device: emulating ${params.device}`);
|
changes.push(`device: emulating ${params.device}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.geolocation) {
|
if (params.geolocation)
|
||||||
changes.push(`geolocation: ${params.geolocation.latitude}, ${params.geolocation.longitude} (±${params.geolocation.accuracy || 100}m)`);
|
changes.push(`geolocation: ${params.geolocation.latitude}, ${params.geolocation.longitude} (±${params.geolocation.accuracy || 100}m)`);
|
||||||
}
|
|
||||||
|
|
||||||
if (params.locale) {
|
|
||||||
|
if (params.locale)
|
||||||
changes.push(`locale: ${params.locale}`);
|
changes.push(`locale: ${params.locale}`);
|
||||||
}
|
|
||||||
|
|
||||||
if (params.timezone) {
|
|
||||||
|
if (params.timezone)
|
||||||
changes.push(`timezone: ${params.timezone}`);
|
changes.push(`timezone: ${params.timezone}`);
|
||||||
}
|
|
||||||
|
|
||||||
if (params.colorScheme) {
|
|
||||||
|
if (params.colorScheme)
|
||||||
changes.push(`colorScheme: ${params.colorScheme}`);
|
changes.push(`colorScheme: ${params.colorScheme}`);
|
||||||
}
|
|
||||||
|
|
||||||
if (params.permissions && params.permissions.length > 0) {
|
|
||||||
|
if (params.permissions && params.permissions.length > 0)
|
||||||
changes.push(`permissions: ${params.permissions.join(', ')}`);
|
changes.push(`permissions: ${params.permissions.join(', ')}`);
|
||||||
}
|
|
||||||
|
|
||||||
if (changes.length === 0) {
|
if (changes.length === 0) {
|
||||||
response.addResult('No configuration changes detected. Current settings remain the same.');
|
response.addResult('No configuration changes detected. Current settings remain the same.');
|
||||||
@ -182,4 +189,119 @@ export default [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
defineTool({
|
||||||
|
capability: 'core',
|
||||||
|
schema: {
|
||||||
|
name: 'browser_configure_artifacts',
|
||||||
|
title: 'Configure artifact storage',
|
||||||
|
description: 'Enable, disable, or configure centralized artifact storage for screenshots, videos, and PDFs during this session. Allows dynamic control over where artifacts are saved and how they are organized.',
|
||||||
|
inputSchema: configureArtifactsSchema,
|
||||||
|
type: 'destructive',
|
||||||
|
},
|
||||||
|
handle: async (context: Context, params: z.output<typeof configureArtifactsSchema>, response: Response) => {
|
||||||
|
try {
|
||||||
|
const registry = ArtifactManagerRegistry.getInstance();
|
||||||
|
const currentSessionId = context.sessionId;
|
||||||
|
const changes: string[] = [];
|
||||||
|
|
||||||
|
// Check current artifact storage status
|
||||||
|
const hasArtifactManager = currentSessionId && registry.getManager(currentSessionId);
|
||||||
|
const currentBaseDir = registry.getGlobalStats().baseDir;
|
||||||
|
|
||||||
|
if (params.enabled === false) {
|
||||||
|
// Disable artifact storage for this session
|
||||||
|
if (hasArtifactManager) {
|
||||||
|
if (currentSessionId)
|
||||||
|
registry.removeManager(currentSessionId);
|
||||||
|
// Clear the session ID from context when disabling
|
||||||
|
context.updateSessionId('');
|
||||||
|
changes.push('Disabled centralized artifact storage');
|
||||||
|
changes.push('Artifacts will now be saved to the default output directory');
|
||||||
|
} else {
|
||||||
|
response.addResult('Centralized artifact storage is already disabled for this session.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (params.enabled === true || params.directory) {
|
||||||
|
// Enable or reconfigure artifact storage
|
||||||
|
const baseDir = params.directory || currentBaseDir;
|
||||||
|
|
||||||
|
if (!baseDir)
|
||||||
|
throw new Error('No artifact directory specified. Use the "directory" parameter or start the server with --artifact-dir.');
|
||||||
|
|
||||||
|
|
||||||
|
// Set or update the base directory if provided
|
||||||
|
if (params.directory && params.directory !== currentBaseDir) {
|
||||||
|
registry.setBaseDir(params.directory);
|
||||||
|
changes.push(`Updated artifact base directory: ${params.directory}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle session ID
|
||||||
|
let sessionId = currentSessionId;
|
||||||
|
if (params.sessionId && params.sessionId !== currentSessionId) {
|
||||||
|
// Update session ID in context if provided and different
|
||||||
|
context.updateSessionId(params.sessionId);
|
||||||
|
sessionId = params.sessionId;
|
||||||
|
changes.push(`Updated session ID: ${sessionId}`);
|
||||||
|
} else if (!sessionId) {
|
||||||
|
// Generate a new session ID if none exists
|
||||||
|
sessionId = `mcp-session-${Date.now()}`;
|
||||||
|
context.updateSessionId(sessionId);
|
||||||
|
changes.push(`Generated session ID: ${sessionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create artifact manager for the session
|
||||||
|
const artifactManager = registry.getManager(sessionId);
|
||||||
|
|
||||||
|
if (artifactManager) {
|
||||||
|
changes.push(`Enabled centralized artifact storage`);
|
||||||
|
changes.push(`Session directory: ${artifactManager.getSessionDir()}`);
|
||||||
|
|
||||||
|
// Show current session stats
|
||||||
|
const stats = artifactManager.getSessionStats();
|
||||||
|
if (stats.toolCallCount > 0)
|
||||||
|
changes.push(`Current session stats: ${stats.toolCallCount} tool calls, ${stats.artifactCount} artifacts`);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error(`Failed to initialize artifact manager for session: ${sessionId}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Show current status - re-check after potential changes
|
||||||
|
const currentManager = currentSessionId ? registry.getManager(currentSessionId) : undefined;
|
||||||
|
|
||||||
|
if (currentManager && currentSessionId) {
|
||||||
|
const stats = currentManager.getSessionStats();
|
||||||
|
response.addResult(
|
||||||
|
`✅ Centralized artifact storage is ENABLED\n\n` +
|
||||||
|
`Session ID: ${currentSessionId}\n` +
|
||||||
|
`Base directory: ${currentBaseDir}\n` +
|
||||||
|
`Session directory: ${currentManager.getSessionDir()}\n` +
|
||||||
|
`Tool calls logged: ${stats.toolCallCount}\n` +
|
||||||
|
`Artifacts saved: ${stats.artifactCount}\n` +
|
||||||
|
`Directory size: ${(stats.directorySize / 1024).toFixed(1)} KB\n\n` +
|
||||||
|
`Use browser_configure_artifacts with:\n` +
|
||||||
|
`• enabled: false - to disable artifact storage\n` +
|
||||||
|
`• directory: "/new/path" - to change base directory\n` +
|
||||||
|
`• sessionId: "custom-id" - to change session ID`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
response.addResult(
|
||||||
|
`❌ Centralized artifact storage is DISABLED\n\n` +
|
||||||
|
`Artifacts are saved to the default output directory: ${context.config.outputDir}\n\n` +
|
||||||
|
`Use browser_configure_artifacts with:\n` +
|
||||||
|
`• enabled: true - to enable artifact storage\n` +
|
||||||
|
`• directory: "/path/to/artifacts" - to specify artifact directory`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes.length > 0)
|
||||||
|
response.addResult(`Artifact storage configuration updated:\n${changes.map(c => `• ${c}`).join('\n')}`);
|
||||||
|
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to configure artifact storage: ${error}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
@ -19,6 +19,7 @@ import { defineTabTool } from './tool.js';
|
|||||||
|
|
||||||
import * as javascript from '../javascript.js';
|
import * as javascript from '../javascript.js';
|
||||||
import { outputFile } from '../config.js';
|
import { outputFile } from '../config.js';
|
||||||
|
import { ArtifactManagerRegistry } from '../artifactManager.js';
|
||||||
|
|
||||||
const pdfSchema = z.object({
|
const pdfSchema = z.object({
|
||||||
filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'),
|
filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'),
|
||||||
@ -36,7 +37,18 @@ const pdf = defineTabTool({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handle: async (tab, params, response) => {
|
handle: async (tab, params, response) => {
|
||||||
const fileName = await outputFile(tab.context.config, params.filename ?? `page-${new Date().toISOString()}.pdf`);
|
// Use centralized artifact storage if configured
|
||||||
|
let fileName: string;
|
||||||
|
const registry = ArtifactManagerRegistry.getInstance();
|
||||||
|
const artifactManager = tab.context.sessionId ? registry.getManager(tab.context.sessionId) : undefined;
|
||||||
|
|
||||||
|
if (artifactManager) {
|
||||||
|
const defaultName = params.filename ?? `page-${new Date().toISOString()}.pdf`;
|
||||||
|
fileName = artifactManager.getArtifactPath(defaultName);
|
||||||
|
} else {
|
||||||
|
fileName = await outputFile(tab.context.config, params.filename ?? `page-${new Date().toISOString()}.pdf`);
|
||||||
|
}
|
||||||
|
|
||||||
response.addCode(`// Save page as ${fileName}`);
|
response.addCode(`// Save page as ${fileName}`);
|
||||||
response.addCode(`await page.pdf(${javascript.formatObject({ path: fileName })});`);
|
response.addCode(`await page.pdf(${javascript.formatObject({ path: fileName })});`);
|
||||||
response.addResult(`Saved page as ${fileName}`);
|
response.addResult(`Saved page as ${fileName}`);
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import { defineTabTool } from './tool.js';
|
|||||||
import * as javascript from '../javascript.js';
|
import * as javascript from '../javascript.js';
|
||||||
import { outputFile } from '../config.js';
|
import { outputFile } from '../config.js';
|
||||||
import { generateLocator } from './utils.js';
|
import { generateLocator } from './utils.js';
|
||||||
|
import { ArtifactManagerRegistry } from '../artifactManager.js';
|
||||||
|
|
||||||
import type * as playwright from 'playwright';
|
import type * as playwright from 'playwright';
|
||||||
|
|
||||||
@ -53,7 +54,19 @@ const screenshot = defineTabTool({
|
|||||||
|
|
||||||
handle: async (tab, params, response) => {
|
handle: async (tab, params, response) => {
|
||||||
const fileType = params.raw ? 'png' : 'jpeg';
|
const fileType = params.raw ? 'png' : 'jpeg';
|
||||||
const fileName = await outputFile(tab.context.config, params.filename ?? `page-${new Date().toISOString()}.${fileType}`);
|
|
||||||
|
// Use centralized artifact storage if configured
|
||||||
|
let fileName: string;
|
||||||
|
const registry = ArtifactManagerRegistry.getInstance();
|
||||||
|
const artifactManager = tab.context.sessionId ? registry.getManager(tab.context.sessionId) : undefined;
|
||||||
|
|
||||||
|
if (artifactManager) {
|
||||||
|
const defaultName = params.filename ?? `page-${new Date().toISOString()}.${fileType}`;
|
||||||
|
fileName = artifactManager.getArtifactPath(defaultName);
|
||||||
|
} else {
|
||||||
|
fileName = await outputFile(tab.context.config, params.filename ?? `page-${new Date().toISOString()}.${fileType}`);
|
||||||
|
}
|
||||||
|
|
||||||
const options: playwright.PageScreenshotOptions = {
|
const options: playwright.PageScreenshotOptions = {
|
||||||
type: fileType,
|
type: fileType,
|
||||||
quality: fileType === 'png' ? undefined : 50,
|
quality: fileType === 'png' ? undefined : 50,
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { defineTool } from './tool.js';
|
import { defineTool } from './tool.js';
|
||||||
|
import { ArtifactManagerRegistry } from '../artifactManager.js';
|
||||||
|
|
||||||
const startRecording = defineTool({
|
const startRecording = defineTool({
|
||||||
capability: 'core',
|
capability: 'core',
|
||||||
@ -38,7 +39,17 @@ const startRecording = defineTool({
|
|||||||
handle: async (context, params, response) => {
|
handle: async (context, params, response) => {
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
const baseFilename = params.filename || `session-${timestamp}`;
|
const baseFilename = params.filename || `session-${timestamp}`;
|
||||||
const videoDir = path.join(context.config.outputDir, 'videos');
|
|
||||||
|
// Use centralized artifact storage if configured
|
||||||
|
let videoDir: string;
|
||||||
|
const registry = ArtifactManagerRegistry.getInstance();
|
||||||
|
const artifactManager = context.sessionId ? registry.getManager(context.sessionId) : undefined;
|
||||||
|
|
||||||
|
if (artifactManager)
|
||||||
|
videoDir = artifactManager.getSubdirectory('videos');
|
||||||
|
else
|
||||||
|
videoDir = path.join(context.config.outputDir, 'videos');
|
||||||
|
|
||||||
|
|
||||||
// Update context options to enable video recording
|
// Update context options to enable video recording
|
||||||
const recordVideoOptions: any = {
|
const recordVideoOptions: any = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user