UNPKG

@andrewlwn77/s3-upload-mcp-server

Version:

Pure Node.js MCP server for uploading images to AWS S3 with high-performance validation using Sharp and file-type

437 lines (399 loc) 13.3 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { ImageUploadService } from './services/image-upload-service.js'; import { BucketManagementService } from './services/bucket-management-service.js'; import { URLGenerationService } from './services/url-generation-service.js'; import { UniqueNameGenerator } from './utils/unique-name-generator.js'; import { ImageValidator } from './validation/image-validator.js'; import { Logger } from './utils/logger.js'; import { ConfigManager } from './utils/config.js'; export class S3UploadMCPServer { private server: Server; private logger: Logger; private imageUploadService: ImageUploadService; private bucketManagementService: BucketManagementService; private urlGenerationService: URLGenerationService; private uniqueNameGenerator: UniqueNameGenerator; private imageValidator: ImageValidator; constructor() { this.server = new Server({ name: 's3-upload-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, }, }); this.logger = Logger.getInstance(); this.imageUploadService = new ImageUploadService(); this.bucketManagementService = new BucketManagementService(); this.urlGenerationService = new URLGenerationService(); this.uniqueNameGenerator = new UniqueNameGenerator(); this.imageValidator = new ImageValidator(); this.setupToolHandlers(); this.setupErrorHandler(); } private setupToolHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'upload_image_to_s3', description: 'Upload image data directly to S3 bucket and return public URL', inputSchema: { type: 'object', properties: { image_data: { type: 'string', description: 'Base64 encoded image data' }, filename: { type: 'string', description: 'Desired filename for the uploaded image', pattern: '^[a-zA-Z0-9._-]+\\.(jpg|jpeg|png|gif|webp|bmp)$' }, bucket: { type: 'string', description: 'S3 bucket name (optional if S3_BUCKET_NAME env var is set)' }, content_type: { type: 'string', description: 'MIME type of the image', enum: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp'] } }, required: ['image_data', 'filename'], additionalProperties: false } }, { name: 'upload_image_file_to_s3', description: 'Upload image file from local filesystem to S3 bucket', inputSchema: { type: 'object', properties: { file_path: { type: 'string', description: 'Local filesystem path to image file' }, bucket: { type: 'string', description: 'S3 bucket name (optional if S3_BUCKET_NAME env var is set)' }, key: { type: 'string', description: 'S3 object key (path within bucket, optional - will use filename if not provided)' }, preserve_filename: { type: 'boolean', description: 'Whether to preserve original filename or generate unique name', default: true } }, required: ['file_path'], additionalProperties: false } }, { name: 'generate_public_url', description: 'Generate a signed public URL for an existing S3 object', inputSchema: { type: 'object', properties: { bucket: { type: 'string', description: 'S3 bucket name' }, key: { type: 'string', description: 'S3 object key' }, expiration: { type: 'integer', description: 'URL expiration time in seconds', default: 3600, minimum: 300, maximum: 604800 } }, required: ['bucket', 'key'], additionalProperties: false } }, { name: 'create_bucket_if_not_exists', description: 'Create S3 bucket if it doesn\'t exist with proper configuration', inputSchema: { type: 'object', properties: { bucket_name: { type: 'string', description: 'S3 bucket name to create', pattern: '^[a-z0-9][a-z0-9.-]*[a-z0-9]$', minLength: 3, maxLength: 63 }, region: { type: 'string', description: 'AWS region for bucket creation', default: 'us-east-1' }, }, required: ['bucket_name'], additionalProperties: false } }, { name: 'generate_upload_url', description: 'Generate a secure presigned URL for uploading files directly to S3', inputSchema: { type: 'object', properties: { bucket: { type: 'string', description: 'S3 bucket name' }, key: { type: 'string', description: 'S3 object key (path within bucket)' }, content_type: { type: 'string', description: 'MIME type of the file to upload', enum: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp'] }, expiration: { type: 'integer', description: 'URL expiration time in seconds', default: 3600, minimum: 300, maximum: 604800 } }, required: ['bucket', 'key', 'content_type'], additionalProperties: false } }, { name: 'generate_unique_filename', description: 'Generate a unique filename with timestamp and UUID', inputSchema: { type: 'object', properties: { original_name: { type: 'string', description: 'Original filename to base unique name on' }, prefix: { type: 'string', description: 'Optional prefix for the filename' }, include_timestamp: { type: 'boolean', description: 'Whether to include timestamp in filename', default: true } }, required: ['original_name'], additionalProperties: false } }, { name: 'validate_image_format', description: 'Validate image file format and properties', inputSchema: { type: 'object', properties: { file_path: { type: 'string', description: 'Path to image file to validate' }, max_file_size: { type: 'integer', description: 'Maximum allowed file size in bytes', default: 10485760 } }, required: ['file_path'], additionalProperties: false } } ] })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'upload_image_to_s3': return await this.handleUploadImageToS3(args); case 'upload_image_file_to_s3': return await this.handleUploadImageFileToS3(args); case 'generate_public_url': return await this.handleGeneratePublicUrl(args); case 'create_bucket_if_not_exists': return await this.handleCreateBucketIfNotExists(args); case 'generate_upload_url': return await this.handleGenerateUploadUrl(args); case 'generate_unique_filename': return await this.handleGenerateUniqueFilename(args); case 'validate_image_format': return await this.handleValidateImageFormat(args); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; this.logger.error(`Tool execution failed: ${name}`, { error: errorMessage, args }); return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: { code: 'TOOL_EXECUTION_ERROR', message: errorMessage } }) }] }; } }); } private async handleUploadImageToS3(args: any) { const result = await this.imageUploadService.uploadImageData( args.image_data, args.filename, args.bucket, args.content_type ); return { content: [{ type: 'text', text: JSON.stringify(result) }] }; } private async handleUploadImageFileToS3(args: any) { const result = await this.imageUploadService.uploadImageFile( args.file_path, args.bucket, args.key, args.preserve_filename ); return { content: [{ type: 'text', text: JSON.stringify(result) }] }; } private async handleGeneratePublicUrl(args: any) { const result = await this.urlGenerationService.generatePublicUrl( args.bucket, args.key, args.expiration ); return { content: [{ type: 'text', text: JSON.stringify(result) }] }; } private async handleCreateBucketIfNotExists(args: any) { const result = await this.bucketManagementService.createBucketIfNotExists( args.bucket_name, args.region, false ); return { content: [{ type: 'text', text: JSON.stringify(result) }] }; } private async handleGenerateUploadUrl(args: any) { const result = await this.urlGenerationService.generateUploadUrl( args.bucket, args.key, args.content_type, args.expiration ); return { content: [{ type: 'text', text: JSON.stringify(result) }] }; } private async handleGenerateUniqueFilename(args: any) { const result = this.uniqueNameGenerator.generateUniqueFilename( args.original_name, args.prefix, args.include_timestamp ); return { content: [{ type: 'text', text: JSON.stringify(result) }] }; } private async handleValidateImageFormat(args: any) { const validator = new ImageValidator(args.max_file_size); const result = await validator.validateImageFile(args.file_path); return { content: [{ type: 'text', text: JSON.stringify(result) }] }; } private setupErrorHandler(): void { this.server.onerror = (error) => { const errorMessage = error instanceof Error ? error.message : 'Unknown server error'; this.logger.error('Server error occurred', { error: errorMessage }); }; process.on('SIGINT', async () => { this.logger.info('Received SIGINT, shutting down gracefully'); await this.server.close(); process.exit(0); }); process.on('SIGTERM', async () => { this.logger.info('Received SIGTERM, shutting down gracefully'); await this.server.close(); process.exit(0); }); } async start(): Promise<void> { try { // Validate configuration on startup const config = ConfigManager.getInstance().getConfig(); this.logger.info('S3 Upload MCP Server starting', { region: config.region, hasDefaultBucket: !!config.defaultBucket }); const transport = new StdioServerTransport(); await this.server.connect(transport); this.logger.info('S3 Upload MCP Server started successfully'); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown startup error'; this.logger.error('Failed to start S3 Upload MCP Server', { error: errorMessage }); process.exit(1); } } } // Start server if this file is run directly if (require.main === module) { const server = new S3UploadMCPServer(); server.start().catch((error) => { console.error('Failed to start server:', error); process.exit(1); }); } export default S3UploadMCPServer;