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

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',
@ -55,7 +62,7 @@ export default [
try {
const deviceList = Object.keys(devices).sort();
const deviceCount = deviceList.length;
// Organize devices by category for better presentation
const categories = {
'iPhone': deviceList.filter(d => d.includes('iPhone')),
@ -63,17 +70,17 @@ export default [
'Pixel': deviceList.filter(d => d.includes('Pixel')),
'Galaxy': deviceList.filter(d => d.includes('Galaxy')),
'Desktop': deviceList.filter(d => d.includes('Desktop')),
'Other': deviceList.filter(d =>
!d.includes('iPhone') &&
!d.includes('iPad') &&
!d.includes('Pixel') &&
!d.includes('Galaxy') &&
'Other': deviceList.filter(d =>
!d.includes('iPhone') &&
!d.includes('iPad') &&
!d.includes('Pixel') &&
!d.includes('Galaxy') &&
!d.includes('Desktop')
)
};
let result = `Available devices for emulation (${deviceCount} total):\n\n`;
for (const [category, deviceNames] of Object.entries(categories)) {
if (deviceNames.length > 0) {
result += `**${category}:**\n`;
@ -84,11 +91,11 @@ export default [
result += '\n';
}
}
result += 'Use browser_configure with the "device" parameter to emulate any of these devices.';
response.addResult(result);
} catch (error) {
throw new Error(`Failed to list devices: ${error}`);
}
@ -107,61 +114,61 @@ export default [
try {
const currentConfig = context.config;
const changes: string[] = [];
// 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.');
return;
}
// Apply the configuration changes
await context.updateBrowserConfig({
headless: params.headless,
@ -174,12 +181,127 @@ export default [
colorScheme: params.colorScheme,
permissions: params.permissions,
});
response.addResult(`Browser configuration updated successfully:\n${changes.map(c => `${c}`).join('\n')}\n\nThe browser has been restarted with the new settings.`);
} catch (error) {
throw new Error(`Failed to update browser configuration: ${error}`);
}
},
}),
];
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}`);
}
},
}),
];