@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
text/typescript
#!/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;