import { z } from 'zod'; import { handleSqlResponse, executeSqlWithFallback, isSqlErrorResponse } from './utils.js'; import type { ToolContext, ToolPrivilegeLevel } from './types.js'; // Schema for updated bucket output const UpdatedBucketSchema = z.object({ id: z.string(), name: z.string(), public: z.boolean(), file_size_limit: z.number().nullable(), allowed_mime_types: z.array(z.string()).nullable(), }); const UpdateStorageConfigOutputSchema = z.object({ success: z.boolean(), bucket: UpdatedBucketSchema.nullable(), message: z.string(), }); type UpdateStorageConfigOutput = z.infer; // Input schema const UpdateStorageConfigInputSchema = z.object({ bucket_id: z.string().describe('The bucket ID to update'), file_size_limit: z.number().min(0).optional().describe('Maximum file size in bytes (0 or null for no limit)'), allowed_mime_types: z.array(z.string()).optional().describe('Array of allowed MIME types (e.g., ["image/png", "image/jpeg"]). Empty array means all types allowed.'), public: z.boolean().optional().describe('Whether the bucket is publicly accessible'), }); type UpdateStorageConfigInput = z.infer; // Static JSON Schema for MCP capabilities const mcpInputSchema = { type: 'object', properties: { bucket_id: { type: 'string', description: 'The bucket ID to update', }, file_size_limit: { type: 'number', minimum: 0, description: 'Maximum file size in bytes (0 or null for no limit)', }, allowed_mime_types: { type: 'array', items: { type: 'string' }, description: 'Array of allowed MIME types (e.g., ["image/png", "image/jpeg"]). Empty array means all types allowed.', }, public: { type: 'boolean', description: 'Whether the bucket is publicly accessible', }, }, required: ['bucket_id'], }; // Tool definition export const updateStorageConfigTool = { name: 'update_storage_config', description: 'Updates storage configuration for a Supabase Storage bucket. Can modify file size limits, allowed MIME types, and public/private status.', privilegeLevel: 'privileged' as ToolPrivilegeLevel, inputSchema: UpdateStorageConfigInputSchema, mcpInputSchema: mcpInputSchema, outputSchema: UpdateStorageConfigOutputSchema, execute: async (input: UpdateStorageConfigInput, context: ToolContext): Promise => { const client = context.selfhostedClient; const { bucket_id, file_size_limit, allowed_mime_types, public: isPublic } = input; // Check if storage schema exists const checkSchemaSql = ` SELECT EXISTS ( SELECT 1 FROM pg_catalog.pg_namespace WHERE nspname = 'storage' ) AS exists `; const schemaCheckResult = await executeSqlWithFallback(client, checkSchemaSql, true); if (!Array.isArray(schemaCheckResult) || schemaCheckResult.length === 0 || !schemaCheckResult[0]?.exists) { return { success: false, bucket: null, message: 'Storage schema not found - Storage may not be configured', }; } // Check if bucket exists const escapedBucketId = bucket_id.replace(/'/g, "''"); const checkBucketSql = ` SELECT EXISTS ( SELECT 1 FROM storage.buckets WHERE id = '${escapedBucketId}' ) AS exists `; const bucketCheckResult = await executeSqlWithFallback(client, checkBucketSql, true); if (!Array.isArray(bucketCheckResult) || bucketCheckResult.length === 0 || !bucketCheckResult[0]?.exists) { return { success: false, bucket: null, message: `Bucket '${bucket_id}' not found`, }; } // Build update query const updates: string[] = []; if (file_size_limit !== undefined) { updates.push(`file_size_limit = ${file_size_limit === 0 ? 'NULL' : file_size_limit}`); } if (allowed_mime_types !== undefined) { if (allowed_mime_types.length === 0) { updates.push('allowed_mime_types = NULL'); } else { const escapedTypes = allowed_mime_types.map((t) => `'${t.replace(/'/g, "''")}'`).join(', '); updates.push(`allowed_mime_types = ARRAY[${escapedTypes}]`); } } if (isPublic !== undefined) { updates.push(`public = ${isPublic}`); } if (updates.length === 0) { return { success: false, bucket: null, message: 'No updates specified. Provide at least one of: file_size_limit, allowed_mime_types, or public', }; } updates.push('updated_at = NOW()'); const updateSql = ` UPDATE storage.buckets SET ${updates.join(', ')} WHERE id = '${escapedBucketId}' RETURNING id, name, public, file_size_limit, allowed_mime_types `; const updateResult = await executeSqlWithFallback(client, updateSql, false); if (isSqlErrorResponse(updateResult)) { return { success: false, bucket: null, message: `Failed to update bucket: ${updateResult.error.message}`, }; } const resultSchema = z.array( z.object({ id: z.string(), name: z.string(), public: z.boolean(), file_size_limit: z.number().nullable(), allowed_mime_types: z.array(z.string()).nullable(), }) ); try { const updatedBuckets = handleSqlResponse(updateResult, resultSchema); if (updatedBuckets.length === 0) { return { success: false, bucket: null, message: 'Update executed but no rows returned', }; } return { success: true, bucket: updatedBuckets[0], message: `Successfully updated bucket '${bucket_id}'`, }; } catch (error) { return { success: false, bucket: null, message: `Failed to parse update result: ${error}`, }; } }, };