- Replace legacy directory structure (api/, db/, functions/, logs/, pooler/) with single docker-compose.yml based self-hosted setup - Add selfhosted-supabase-mcp TypeScript MCP server for database management - Add .dockerignore for Docker build context - Update .gitignore to exclude .env files, volumes/, backups, logs
184 lines
6.7 KiB
TypeScript
184 lines
6.7 KiB
TypeScript
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<typeof UpdateStorageConfigOutputSchema>;
|
|
|
|
// 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<typeof UpdateStorageConfigInputSchema>;
|
|
|
|
// 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<UpdateStorageConfigOutput> => {
|
|
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}`,
|
|
};
|
|
}
|
|
},
|
|
};
|