@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
JavaScript
;
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