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

461 lines 17 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.S3Client = void 0; const fs_1 = require("fs"); const path = __importStar(require("path")); const client_s3_1 = require("@aws-sdk/client-s3"); const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner"); const config_1 = require("../utils/config"); const logger_1 = require("../utils/logger"); class S3Client { constructor() { this.config = config_1.ConfigManager.getInstance().getConfig(); this.logger = logger_1.Logger.getInstance(); this.client = new client_s3_1.S3Client({ region: this.config.region, credentials: { accessKeyId: this.config.accessKeyId, secretAccessKey: this.config.secretAccessKey } }); } mapAwsError(error) { const errorName = error.name || error.__type || 'Unknown'; const errorMessage = error.message || 'Unknown error occurred'; switch (errorName) { case 'NoSuchBucket': return { code: 'BUCKET_NOT_FOUND', message: 'Bucket does not exist' }; case 'BucketAlreadyExists': return { code: 'BUCKET_EXISTS', message: 'Bucket already exists' }; case 'BucketAlreadyOwnedByYou': return { code: 'BUCKET_OWNED', message: 'Bucket already owned by you' }; case 'AccessDenied': return { code: 'ACCESS_DENIED', message: 'Access denied to S3 resource' }; case 'InvalidBucketName': return { code: 'VALIDATION_ERROR', message: 'Invalid bucket name format' }; case 'NotFound': return { code: 'NOT_FOUND', message: 'Resource not found' }; case 'NetworkingError': return { code: 'NETWORK_ERROR', message: 'Network connectivity issue' }; default: return { code: 'S3_ERROR', message: errorMessage }; } } async uploadImageData(imageData, key, bucket, contentType) { try { const command = new client_s3_1.PutObjectCommand({ Bucket: bucket, Key: key, Body: imageData, ContentType: contentType }); const response = await this.client.send(command); const fileSize = imageData.length; const publicUrl = `https://${bucket}.s3.amazonaws.com/${key}`; const etag = response.ETag?.replace(/"/g, '') || ''; this.logger.info('Image uploaded successfully', { bucket, key, size: fileSize, contentType }); return { success: true, public_url: publicUrl, s3_key: key, bucket: bucket, file_size: fileSize, content_type: contentType, etag: etag }; } catch (error) { const mappedError = this.mapAwsError(error); this.logger.error('S3 upload failed', { error: mappedError.message, key, bucket, originalError: error }); return { success: false, error: { code: mappedError.code, message: mappedError.message } }; } } async uploadFile(filePath, key, bucket) { try { // Read file and get stats const fileBuffer = await fs_1.promises.readFile(filePath); const stats = await fs_1.promises.stat(filePath); const originalFilename = path.basename(filePath); // Detect content type based on file extension const ext = path.extname(filePath).toLowerCase(); let contentType = 'application/octet-stream'; const mimeTypes = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp', '.svg': 'image/svg+xml' }; if (mimeTypes[ext]) { contentType = mimeTypes[ext]; } const command = new client_s3_1.PutObjectCommand({ Bucket: bucket, Key: key, Body: fileBuffer, ContentType: contentType }); const response = await this.client.send(command); const publicUrl = `https://${bucket}.s3.amazonaws.com/${key}`; const etag = response.ETag?.replace(/"/g, '') || ''; this.logger.info('File uploaded successfully', { bucket, key, filePath, size: stats.size, contentType }); return { success: true, public_url: publicUrl, s3_key: key, bucket: bucket, file_size: stats.size, content_type: contentType, original_filename: originalFilename, etag: etag }; } catch (error) { const mappedError = this.mapAwsError(error); this.logger.error('S3 file upload failed', { error: mappedError.message, filePath, key, bucket, originalError: error }); return { success: false, error: { code: mappedError.code, message: mappedError.message } }; } } async generatePresignedUrl(bucket, key, expiration = 3600) { try { const command = new client_s3_1.GetObjectCommand({ Bucket: bucket, Key: key }); const url = await (0, s3_request_presigner_1.getSignedUrl)(this.client, command, { expiresIn: expiration }); const expiresAt = new Date(Date.now() + expiration * 1000).toISOString(); this.logger.info('Generated presigned download URL', { bucket, key, expiration }); return { success: true, public_url: url, expires_at: expiresAt, expiration_seconds: expiration }; } catch (error) { const mappedError = this.mapAwsError(error); this.logger.error('Presigned URL generation failed', { error: mappedError.message, bucket, key, originalError: error }); return { success: false, error: { code: mappedError.code, message: mappedError.message } }; } } async createBucket(bucketName, region = 'us-east-1', enablePublicRead = false) { try { let created = false; let alreadyExists = false; // Check if bucket exists try { const headCommand = new client_s3_1.HeadBucketCommand({ Bucket: bucketName }); await this.client.send(headCommand); alreadyExists = true; } catch (error) { if (error.name === 'NotFound' || error.$metadata?.httpStatusCode === 404) { // Bucket doesn't exist, create it const createBucketParams = { Bucket: bucketName }; // Only add LocationConstraint if not us-east-1 if (region !== 'us-east-1') { createBucketParams.CreateBucketConfiguration = { LocationConstraint: region }; } const createCommand = new client_s3_1.CreateBucketCommand(createBucketParams); await this.client.send(createCommand); created = true; } else { throw error; } } let publicReadEnabled = false; if (enablePublicRead && created) { // Apply public read policy const policy = { Version: "2012-10-17", Statement: [ { Sid: "PublicReadGetObject", Effect: "Allow", Principal: "*", Action: "s3:GetObject", Resource: `arn:aws:s3:::${bucketName}/*` } ] }; const policyCommand = new client_s3_1.PutBucketPolicyCommand({ Bucket: bucketName, Policy: JSON.stringify(policy) }); await this.client.send(policyCommand); publicReadEnabled = true; } this.logger.info('Bucket operation completed', { bucketName, region, created, alreadyExists, publicReadEnabled }); return { success: true, bucket_name: bucketName, region: region, created: created, already_exists: alreadyExists, public_read_enabled: publicReadEnabled }; } catch (error) { const mappedError = this.mapAwsError(error); this.logger.error('Bucket creation failed', { error: mappedError.message, bucketName, region, originalError: error }); return { success: false, error: { code: mappedError.code, message: mappedError.message } }; } } async setBucketPublicReadPolicy(bucketName) { try { const policy = { Version: "2012-10-17", Statement: [ { Sid: "PublicReadGetObject", Effect: "Allow", Principal: "*", Action: "s3:GetObject", Resource: `arn:aws:s3:::${bucketName}/*` } ] }; const command = new client_s3_1.PutBucketPolicyCommand({ Bucket: bucketName, Policy: JSON.stringify(policy) }); await this.client.send(command); this.logger.info('Bucket policy applied', { bucketName, policyType: 'public_read' }); return { success: true, bucket_name: bucketName, policy_applied: true, policy_document: policy }; } catch (error) { const mappedError = this.mapAwsError(error); this.logger.error('Bucket policy update failed', { error: mappedError.message, bucketName, originalError: error }); return { success: false, error: { code: mappedError.code, message: mappedError.message } }; } } async generatePresignedUploadUrl(bucket, key, contentType, expiration = 3600) { try { const command = new client_s3_1.PutObjectCommand({ Bucket: bucket, Key: key, ContentType: contentType }); const url = await (0, s3_request_presigner_1.getSignedUrl)(this.client, command, { expiresIn: expiration }); const expiresAt = new Date(Date.now() + expiration * 1000).toISOString(); this.logger.info('Generated presigned upload URL', { bucket, key, contentType, expiration }); return { success: true, upload_url: url, expires_at: expiresAt, expiration_seconds: expiration, bucket: bucket, key: key, content_type: contentType }; } catch (error) { const mappedError = this.mapAwsError(error); this.logger.error('Presigned upload URL generation failed', { error: mappedError.message, bucket, key, contentType, originalError: error }); return { success: false, error: { code: mappedError.code, message: mappedError.message } }; } } async uploadImageDataViaUrl(imageData, key, bucket, contentType) { try { // First, generate the presigned upload URL const urlResult = await this.generatePresignedUploadUrl(bucket, key, contentType, 1800); // 30 minutes if (!urlResult.success || !urlResult.upload_url) { return urlResult; } // Upload the data using the presigned URL via native fetch const response = await fetch(urlResult.upload_url, { method: 'PUT', body: imageData, headers: { 'Content-Type': contentType } }); if (!response.ok) { const errorText = await response.text().catch(() => 'Unknown error'); return { success: false, error: { code: 'HTTP_UPLOAD_ERROR', message: `Upload failed with status ${response.status}: ${errorText}` } }; } const publicUrl = `https://${bucket}.s3.amazonaws.com/${key}`; this.logger.info('Image uploaded via presigned URL', { bucket, key, size: imageData.length, method: 'presigned_url' }); return { success: true, public_url: publicUrl, s3_key: key, bucket: bucket, file_size: imageData.length, content_type: contentType }; } catch (error) { const mappedError = this.mapAwsError(error); this.logger.error('Presigned URL upload failed', { error: mappedError.message, key, bucket, originalError: error }); return { success: false, error: { code: mappedError.code, message: mappedError.message } }; } } } exports.S3Client = S3Client; //# sourceMappingURL=s3-client.js.map