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
12 changed files with 727 additions and 93 deletions
|
|
@ -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}`);
|
||||
}
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue