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:
Ryan Malloy 2025-08-15 06:42:16 -06:00
parent ecedcc48d6
commit d8202f6694
12 changed files with 727 additions and 93 deletions

160
README.md
View File

@ -144,6 +144,8 @@ Playwright MCP server supports following arguments. They can be provided in the
> npx @playwright/mcp@latest --help
--allowed-origins <origins> semicolon-separated list of origins to allow the
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
browser from requesting. Blocklist is evaluated
before allowlist. If used without the allowlist,
@ -296,6 +298,9 @@ npx @playwright/mcp@latest --config path/to/config.json
// Directory for output files
outputDir?: string;
// Directory for centralized artifact storage with session-specific subdirectories
artifactDir?: string;
// Network configuration
network?: {
// 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>
### 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
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 -->
- **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**
- Title: Get 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 -->
- **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**
- Title: Navigate to a URL
- Description: Navigate to a URL

6
config.d.ts vendored
View File

@ -100,6 +100,12 @@ export type Config = {
*/
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?: {
/**
* 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
View 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
};
}
}

View File

@ -23,6 +23,7 @@ import { filteredTools } from './tools.js';
import { packageJSON } from './package.js';
import { SessionManager } from './sessionManager.js';
import { EnvironmentIntrospector } from './environmentIntrospection.js';
import { ArtifactManagerRegistry } from './artifactManager.js';
import type { BrowserContextFactory } from './browserContextFactory.js';
import type * as mcpServer from './mcp/server.js';
@ -46,6 +47,12 @@ export class BrowserServerBackend implements ServerBackend {
this._browserContextFactory = browserContextFactory;
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
this._context = new Context(this._tools, config, browserContextFactory, this._environmentIntrospector);
}
@ -55,19 +62,19 @@ export class BrowserServerBackend implements ServerBackend {
}
setSessionId(sessionId: string): void {
if (this._sessionId === sessionId) {
if (this._sessionId === sessionId)
return; // Already using this session
}
this._sessionId = sessionId;
// Get or create persistent context for this session
const sessionManager = SessionManager.getInstance();
this._context = sessionManager.getOrCreateContext(
sessionId,
this._tools,
this._config,
this._browserContextFactory
sessionId,
this._tools,
this._config,
this._browserContextFactory
);
// Update environment introspector reference
@ -81,7 +88,43 @@ export class BrowserServerBackend implements ServerBackend {
async callTool(schema: mcpServer.ToolSchema<any>, parsedArguments: any) {
const response = new Response(this._context, schema.name, parsedArguments);
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)
await this._sessionLog.log(response);
return await response.serialize();

View File

@ -24,6 +24,7 @@ import type { BrowserContextOptions, LaunchOptions } from 'playwright';
export type CLIOptions = {
allowedOrigins?: string[];
artifactDir?: string;
blockedOrigins?: string[];
blockServiceWorkers?: boolean;
browser?: string;
@ -81,6 +82,7 @@ export type FullConfig = Config & {
},
network: NonNullable<Config['network']>,
outputDir: string;
artifactDir?: string;
server: NonNullable<Config['server']>,
};
@ -131,9 +133,9 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
channel,
executablePath: cliOptions.executablePath,
};
if (cliOptions.headless !== undefined) {
if (cliOptions.headless !== undefined)
launchOptions.headless = cliOptions.headless;
}
// --no-sandbox was passed, disable the sandbox
if (cliOptions.sandbox === false)
@ -196,6 +198,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
saveSession: cliOptions.saveSession,
saveTrace: cliOptions.saveTrace,
outputDir: cliOptions.outputDir,
artifactDir: cliOptions.artifactDir,
imageResponses: cliOptions.imageResponses,
};
@ -205,6 +208,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
function configFromEnv(): Config {
const options: CLIOptions = {};
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.blockServiceWorkers = envToBoolean(process.env.PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS);
options.browser = envToString(process.env.PLAYWRIGHT_MCP_BROWSER);

View File

@ -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[] {
return this._tabs;
}
@ -352,15 +358,15 @@ export class Context {
const currentConfig = { ...this.config };
// Update the configuration
if (changes.headless !== undefined) {
if (changes.headless !== undefined)
currentConfig.browser.launchOptions.headless = changes.headless;
}
// Handle device emulation - this overrides individual viewport/userAgent settings
if (changes.device) {
if (!devices[changes.device]) {
if (!devices[changes.device])
throw new Error(`Unknown device: ${changes.device}`);
}
const deviceConfig = devices[changes.device];
// Apply all device properties to context options
@ -370,12 +376,12 @@ export class Context {
};
} else {
// Apply individual settings only if no device is specified
if (changes.viewport) {
if (changes.viewport)
currentConfig.browser.contextOptions.viewport = changes.viewport;
}
if (changes.userAgent) {
if (changes.userAgent)
currentConfig.browser.contextOptions.userAgent = changes.userAgent;
}
}
// Apply additional context options
@ -387,21 +393,21 @@ export class Context {
};
}
if (changes.locale) {
if (changes.locale)
currentConfig.browser.contextOptions.locale = changes.locale;
}
if (changes.timezone) {
if (changes.timezone)
currentConfig.browser.contextOptions.timezoneId = changes.timezone;
}
if (changes.colorScheme) {
if (changes.colorScheme)
currentConfig.browser.contextOptions.colorScheme = changes.colorScheme;
}
if (changes.permissions) {
if (changes.permissions)
currentConfig.browser.contextOptions.permissions = changes.permissions;
}
// Store the modified config
(this as any).config = currentConfig;

View File

@ -31,6 +31,7 @@ program
.version('Version ' + packageJSON.version)
.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('--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('--block-service-workers', 'block service workers')
.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);
};
if (serverConfig.port !== undefined) {
if (serverConfig.port !== undefined)
process.stdin.on('close', handleExit);
}
process.on('SIGINT', handleExit);
process.on('SIGTERM', handleExit);

View File

@ -31,9 +31,9 @@ export class SessionManager {
private _sessions: Map<string, Context> = new Map();
static getInstance(): SessionManager {
if (!SessionManager._instance) {
if (!SessionManager._instance)
SessionManager._instance = new SessionManager();
}
return SessionManager._instance;
}

View File

@ -17,6 +17,7 @@
import { z } from 'zod';
import { devices } from 'playwright';
import { defineTool } from './tool.js';
import { ArtifactManagerRegistry } from '../artifactManager.js';
import type { Context } from '../context.js';
import type { Response } from '../response.js';
@ -41,6 +42,12 @@ const configureSchema = 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 [
defineTool({
capability: 'core',
@ -111,51 +118,51 @@ export default [
// Track what's changing
if (params.headless !== undefined) {
const currentHeadless = currentConfig.browser.launchOptions.headless;
if (params.headless !== currentHeadless) {
if (params.headless !== currentHeadless)
changes.push(`headless: ${currentHeadless}${params.headless}`);
}
}
if (params.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}`);
}
}
if (params.userAgent) {
const currentUA = currentConfig.browser.contextOptions.userAgent;
if (params.userAgent !== currentUA) {
if (params.userAgent !== currentUA)
changes.push(`userAgent: ${currentUA || 'default'}${params.userAgent}`);
}
}
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.`);
}
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)`);
}
if (params.locale) {
if (params.locale)
changes.push(`locale: ${params.locale}`);
}
if (params.timezone) {
if (params.timezone)
changes.push(`timezone: ${params.timezone}`);
}
if (params.colorScheme) {
if (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(', ')}`);
}
if (changes.length === 0) {
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}`);
}
},
}),
];

View File

@ -19,6 +19,7 @@ import { defineTabTool } from './tool.js';
import * as javascript from '../javascript.js';
import { outputFile } from '../config.js';
import { ArtifactManagerRegistry } from '../artifactManager.js';
const pdfSchema = z.object({
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) => {
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(`await page.pdf(${javascript.formatObject({ path: fileName })});`);
response.addResult(`Saved page as ${fileName}`);

View File

@ -20,6 +20,7 @@ import { defineTabTool } from './tool.js';
import * as javascript from '../javascript.js';
import { outputFile } from '../config.js';
import { generateLocator } from './utils.js';
import { ArtifactManagerRegistry } from '../artifactManager.js';
import type * as playwright from 'playwright';
@ -53,7 +54,19 @@ const screenshot = defineTabTool({
handle: async (tab, params, response) => {
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 = {
type: fileType,
quality: fileType === 'png' ? undefined : 50,

View File

@ -17,6 +17,7 @@
import path from 'path';
import { z } from 'zod';
import { defineTool } from './tool.js';
import { ArtifactManagerRegistry } from '../artifactManager.js';
const startRecording = defineTool({
capability: 'core',
@ -38,7 +39,17 @@ const startRecording = defineTool({
handle: async (context, params, response) => {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
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
const recordVideoOptions: any = {