Skip to main content
Glama
cloudflare.ts4.96 kB
import { z } from 'zod'; import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { S3Client, PutObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3'; import { BaseUploadService, UploadServiceConfig, UploadResult, UploadImageArgs } from './types'; export const CloudflareEnvConfigSchema = z.object({ UPLOAD_SERVICE: z.literal('cloudflare').optional().default('cloudflare'), CLOUDFLARE_R2_BUCKET: z.string().min(1, 'CLOUDFLARE_R2_BUCKET is required'), CLOUDFLARE_R2_ACCESS_KEY_ID: z.string().min(1, 'CLOUDFLARE_R2_ACCESS_KEY_ID is required'), CLOUDFLARE_R2_SECRET_ACCESS_KEY: z.string().min(1, 'CLOUDFLARE_R2_SECRET_ACCESS_KEY is required'), CLOUDFLARE_R2_REGION: z.string().optional(), CLOUDFLARE_R2_ENDPOINT: z.string().url('CLOUDFLARE_R2_ENDPOINT must be a valid URL'), }); export type ValidatedCloudflareEnvConfig = z.infer<typeof CloudflareEnvConfigSchema>; // Cloudflare R2 upload service implementation export class CloudflareUploadService extends BaseUploadService { private s3Client: S3Client; constructor(validatedEnvConfig: ValidatedCloudflareEnvConfig) { const serviceConfig: UploadServiceConfig = { service: 'cloudflare', // Explicitly set bucket: validatedEnvConfig.CLOUDFLARE_R2_BUCKET, apiKey: validatedEnvConfig.CLOUDFLARE_R2_ACCESS_KEY_ID, apiSecret: validatedEnvConfig.CLOUDFLARE_R2_SECRET_ACCESS_KEY, region: validatedEnvConfig.CLOUDFLARE_R2_REGION || 'auto', endpoint: validatedEnvConfig.CLOUDFLARE_R2_ENDPOINT, }; super(serviceConfig); this.s3Client = new S3Client({ region: this.config.region, credentials: { accessKeyId: this.config.apiKey!, secretAccessKey: this.config.apiSecret!, }, endpoint: this.config.endpoint!, forcePathStyle: true, }); } async upload(buffer: Buffer, filename: string, args: UploadImageArgs): Promise<UploadResult> { try { const key = args.folder ? `${args.folder}/${filename}` : filename; const parts = filename.split('.'); const fileExtension = parts.length > 1 ? parts.pop()?.toLowerCase() || 'jpg' : 'jpg'; const contentType = this.getContentType(fileExtension); // Check if file exists and handle overwrite if (!args.overwrite) { try { const headCommand = new HeadObjectCommand({ Bucket: this.config.bucket!, Key: key, }); await this.s3Client.send(headCommand); throw new McpError( ErrorCode.InvalidParams, `File ${key} already exists. Set overwrite=true to replace it.` ); } catch (error: any) { // If error is NotFound, file doesn't exist and we can proceed if (error.name !== 'NotFound' && error.name !== 'NoSuchKey') { throw error; } } } const putCommand = new PutObjectCommand({ Bucket: this.config.bucket!, Key: key, Body: buffer, ContentType: contentType, Metadata: args.metadata || {}, // Note: R2 doesn't support ACL in the same way as S3 // Public access is controlled via bucket settings or custom domains }); const result = await this.s3Client.send(putCommand); // Generate URL based on configuration const url = this.generateUrl(key); return { url, filename, size: buffer.length, format: fileExtension, service: 'cloudflare', metadata: { bucket: this.config.bucket, key, endpoint: this.config.endpoint, etag: result.ETag, versionId: result.VersionId, }, }; } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Cloudflare R2 upload failed: ${error instanceof Error ? error.message : String(error)}` ); } } private getContentType(extension: string): string { const mimeTypes: Record<string, string> = { 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png', 'gif': 'image/gif', 'webp': 'image/webp', 'avif': 'image/avif', 'tiff': 'image/tiff', 'heic': 'image/heic', 'heif': 'image/heif', }; return mimeTypes[extension] || 'application/octet-stream'; } private generateUrl(key: string): string { // R2 URL generation options: // 1. Custom domain if baseUrl is provided if (this.config.baseUrl) { return `${this.config.baseUrl}/${key}`; } // 2. Public R2 URL (if bucket has public access configured) // Format: https://pub-<bucket-id>.r2.dev/<key> // Note: This requires the bucket to be configured for public access // 3. Direct endpoint URL (for private access or when using presigned URLs) return `${this.config.endpoint}/${this.config.bucket}/${key}`; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/BoomLinkAi/image-worker-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server